Files
fad_oa/ruoyi-ui/src/views/rm/installPrep/progress.vue

204 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- Stats -->
<div class="dashboard-grid">
<div class="stat-card"><div class="label">安装总进度</div><div class="value" style="color:#1890ff;">{{ overallPct }}%</div><div class="sub">{{ doneCount }}/{{ progress.length }} 项完成</div></div>
<div class="stat-card green"><div class="label">已完成</div><div class="value">{{ doneCount }}</div><div class="sub"></div></div>
<div class="stat-card orange"><div class="label">进行中</div><div class="value">{{ progCount }}</div><div class="sub"></div></div>
<div class="stat-card"><div class="label">未开始</div><div class="value">{{ pendingCount }}</div><div class="sub"></div></div>
</div>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="handleAdd">+ 添加进度项</el-button>
</div>
<!-- Bar chart -->
<div style="font-weight:600;font-size:13px;margin-bottom:8px;">📊 安装进度条形图</div>
<div v-if="progress.length === 0" style="text-align:center;color:#aaa;padding:20px;">暂无安装进度数据</div>
<div v-for="(p,i) in progress" :key="i" style="margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
<span style="font-weight:600;">
{{ p.itemName }}
<el-tag v-if="isOverdue(p)" size="mini" type="danger" style="margin-left:4px;">逾期</el-tag>
</span>
<span style="color:#6c757d;">{{ p.planStart||'?' }}~{{ p.planEnd||'?' }} · {{ statusLabel(p) }} · {{ barPercent(p) }}%</span>
</div>
<div style="height:16px;background:#eee;border-radius:4px;overflow:hidden;">
<div :style="'height:100%;width:'+barPercent(p)+'%;border-radius:4px;background:'+barColor(p)+';transition:width 0.5s;'"></div>
</div>
<div v-if="p.actualStart||p.actualEnd" style="font-size:9px;color:#6c757d;margin-top:1px;">
实际: {{ p.actualStart||'-' }}~{{ p.actualEnd||'-' }}
</div>
<div v-if="isOverdue(p) && p.delayReason" style="font-size:10px;color:#e74c3c;margin-top:1px;">
延误原因{{ p.delayReason }}
</div>
</div>
<el-divider></el-divider>
<!-- Detail table -->
<div style="font-weight:600;font-size:13px;margin-bottom:8px;">📋 安装进度明细</div>
<el-table :data="progress" v-loading="loading" stripe border size="small" style="width:100%;">
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="itemName" label="安装项目" min-width="130" show-overflow-tooltip />
<el-table-column prop="planStart" label="计划开始" width="90" />
<el-table-column prop="planEnd" label="计划结束" width="90" />
<el-table-column prop="actualStart" label="实际开始" width="90" />
<el-table-column prop="actualEnd" label="实际结束" width="90" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.status==='done'?'success':s.row.status==='progress'?'primary':'info'" size="mini">
{{ s.row.status==='done'?'已完成':s.row.status==='progress'?'进行中':'未开始' }}
</el-tag>
<el-tag v-if="isOverdue(s.row)" size="mini" type="danger" style="margin-left:2px;">逾期</el-tag>
</template>
</el-table-column>
<el-table-column prop="delayReason" label="延误原因" width="120" show-overflow-tooltip />
<el-table-column label="📷图片" width="55" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.images" size="mini" type="success">📷 {{ (s.row.images||'').split(';').filter(Boolean).length }}</el-tag>
<span v-else style="color:#bbb;">📷 0</span>
</template>
</el-table-column>
<el-table-column label="🎬视频" width="55" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.videos" size="mini" type="primary">🎬 {{ (s.row.videos||'').split(';').filter(Boolean).length }}</el-tag>
<span v-else style="color:#bbb;">🎬 0</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-group"><label>安装项目名称</label><el-input v-model="form.itemName" /></div>
<div class="form-row">
<div class="form-group"><label>计划开始</label><el-date-picker v-model="form.planStart" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
<div class="form-group"><label>计划结束</label><el-date-picker v-model="form.planEnd" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>实际开始</label><el-date-picker v-model="form.actualStart" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
<div class="form-group"><label>实际结束</label><el-date-picker v-model="form.actualEnd" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>状态</label><el-select v-model="form.status" style="width:100%;"><el-option label="未开始" value="pending" /><el-option label="进行中" value="progress" /><el-option label="已完成" value="done" /></el-select></div>
<div class="form-group"><label>延误原因</label><el-input v-model="form.delayReason" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>📷 图片文件名;分隔</label><el-input v-model="form.images" /></div>
<div class="form-group"><label>🎬 视频文件名;分隔</label><el-input v-model="form.videos" /></div>
</div>
<div class="form-group"><label>备注</label><el-input v-model="form.remark" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible=false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallProgressAll, addInstallProgress, updateInstallProgress, delInstallProgress } from '@/api/rm/installProgress'
export default {
name: 'InstallProgress',
props: { projectId: { type: [Number, String], default: null } },
watch: { projectId: { immediate: true, handler(v) { if (v) this.loadData() } } },
data() {
return {
loading: false,
progress: [],
dialogVisible: false,
dialogTitle: '',
form: this.cleanForm(),
rules: { itemName: [{ required: true, message: '请填写安装项目名称', trigger: 'blur' }] }
}
},
computed: {
doneCount() { return this.progress.filter(p => p.status==='done').length },
progCount() { return this.progress.filter(p => p.status==='progress').length },
pendingCount() { return this.progress.filter(p => p.status==='pending'||!p.status).length },
overallPct() {
const n = this.progress.length
return n ? Math.round(this.doneCount/n*100) : 0
}
},
methods: {
cleanForm() { return { itemName:'', planStart:'', planEnd:'', actualStart:'', actualEnd:'', status:'pending', delayReason:'', images:'', videos:'', remark:'' } },
loadData() {
if (!this.projectId) return
this.loading = true
listInstallProgressAll({ projectId: this.projectId }).then(res => {
this.progress = res.data || []
}).finally(() => { this.loading = false })
},
isOverdue(p) {
if (p.status==='done' || !p.planEnd) return false
return new Date(p.planEnd+'T00:00:00') < new Date()
},
statusLabel(p) {
if (p.status==='done') return '已完成'
if (p.status==='progress') return '进行中'
return '未开始'
},
barPercent(p) {
if (p.status==='done') return 100
if (p.status==='progress') {
if (p.planStart && p.planEnd) {
const s = new Date(p.planStart+'T00:00:00')
const e = new Date(p.planEnd+'T00:00:00')
const now = new Date()
if (now < s) return 0
const total = e - s
if (total <= 0) return 50
return Math.min(100, Math.round((now - s)/total*100))
}
return 30
}
return 0
},
barColor(p) {
if (p.status==='done') return '#28a745'
if (this.isOverdue(p)) return '#e74c3c'
if (p.status==='progress') return '#1890ff'
return '#ddd'
},
handleAdd() { this.dialogTitle='添加进度项'; this.form=this.cleanForm(); this.dialogVisible=true },
handleEdit(row) { this.dialogTitle='编辑进度项'; this.form={...row}; this.dialogVisible=true },
handleDelete(row) {
this.$confirm('确认删除?','提示',{type:'warning'}).then(() => {
delInstallProgress(row.progressId).then(() => { this.loadData() })
}).catch(() => {})
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.projectId }
const act = data.progressId ? updateInstallProgress(data) : addInstallProgress(data)
act.then(() => { this.dialogVisible=false; this.loadData() })
})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-bottom: 12px; }
.stat-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 10px; text-align: center; }
.stat-card .label { font-size: 11px; color: #6c757d; }
.stat-card .value { font-size: 20px; font-weight: 700; color: #1a5a9e; }
.stat-card.green .value { color: #28a745; }
.stat-card.orange .value { color: #e67e22; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>