feat: 同步本地未提交的前后端更新(plan/quality/material/inspection/production 等模块)
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">冷卷号</span>
|
||||
<input v-model="query.cold_coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">日期</span>
|
||||
<input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" />
|
||||
@@ -33,42 +37,50 @@
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>计划号</th><th>计划日期</th><th>班次</th>
|
||||
<th>计划(卷)</th><th>计划重量(kg)</th>
|
||||
<th>实际(卷)</th><th>实际重量(kg)</th>
|
||||
<th>完成率</th><th>优先级</th><th>状态</th><th>创建人</th><th>操作</th>
|
||||
<th style="width:48px;">序号</th>
|
||||
<th>冷卷号</th>
|
||||
<th>热卷号</th>
|
||||
<th>钢种</th>
|
||||
<th>来料厚度</th>
|
||||
<th>产品厚度</th>
|
||||
<th>偏差上限</th>
|
||||
<th>偏差下限</th>
|
||||
<th>来料宽度</th>
|
||||
<th>产品宽度</th>
|
||||
<th>包装要求</th>
|
||||
<th>卷径</th>
|
||||
<th>分卷数</th>
|
||||
<th>下工序</th>
|
||||
<th>计划时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in tableData" :key="row.id">
|
||||
<td class="td-num">{{ row.plan_no }}</td>
|
||||
<td class="td-muted">{{ fmtDate(row.plan_date) }}</td>
|
||||
<td>{{ row.shift ? row.shift + '班' : '—' }}</td>
|
||||
<td class="td-num">{{ row.plan_quantity }}</td>
|
||||
<td class="td-num">{{ row.plan_weight }}</td>
|
||||
<td class="td-num">{{ row.actual_quantity }}</td>
|
||||
<td class="td-num">{{ row.actual_weight }}</td>
|
||||
<td>
|
||||
<div v-if="row.plan_quantity > 0">
|
||||
<div class="prog-bar-wrap" style="width:70px;display:inline-block;vertical-align:middle;margin-right:6px;">
|
||||
<div class="prog-bar-fill" :style="{ width: completionRate(row) + '%', background: rateColor(row) }"></div>
|
||||
</div>
|
||||
<span :style="{ color: rateColor(row) }">{{ completionRate(row) }}%</span>
|
||||
</div>
|
||||
<span v-else class="td-muted">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['badge', row.priority >= 8 ? 'badge-red' : row.priority >= 5 ? 'badge-yellow' : 'badge-gray']">P{{ row.priority }}</span>
|
||||
</td>
|
||||
<tr v-for="(row, idx) in tableData" :key="row.id">
|
||||
<td class="td-num">{{ idx + 1 }}</td>
|
||||
<td class="td-num">{{ row.cold_coil_no || row.plan_no || '—' }}</td>
|
||||
<td class="td-num">{{ row.hot_coil_no || '—' }}</td>
|
||||
<td>{{ row.steel_grade || '—' }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.incoming_thickness) }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.product_thickness) }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.deviation_upper, 3) }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.deviation_lower, 3) }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.incoming_width, 0) }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.product_width, 0) }}</td>
|
||||
<td>{{ row.packaging_req || '—' }}</td>
|
||||
<td class="td-num">{{ fmtNum(row.coil_diameter, 0) }}</td>
|
||||
<td class="td-num">{{ row.split_count != null ? row.split_count : 1 }}</td>
|
||||
<td class="td-num">{{ row.next_process != null ? row.next_process : '—' }}</td>
|
||||
<td class="td-muted">{{ fmtTime(row.plan_date) }}</td>
|
||||
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
|
||||
<td class="td-muted">{{ row.created_by || '—' }}</td>
|
||||
<td>
|
||||
<span class="action-link" @click="openDialog(row)">编辑</span>
|
||||
<span v-if="row.status === 'draft'" class="action-link" style="color:var(--accent-green)" @click="confirmPlan(row)">确认</span>
|
||||
<span v-if="row.status === 'ready'" class="action-link" style="color:var(--accent-green)" @click="confirmPlan(row)">上线</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!tableData.length && !loading">
|
||||
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
||||
<td colspan="17" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -76,47 +88,82 @@
|
||||
</div>
|
||||
|
||||
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
|
||||
<div class="modal-box" style="width:640px;">
|
||||
<div class="modal-box" style="width:780px;">
|
||||
<div class="modal-header">
|
||||
{{ editRow ? '编辑计划' : '新增计划' }}
|
||||
<span class="modal-close" @click="dialogVisible=false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-2" style="gap:12px;">
|
||||
<div class="grid-3" style="gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">计划号 *</div>
|
||||
<input v-model="form.plan_no" class="kv-input" :disabled="!!editRow" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">计划日期</div>
|
||||
<input v-model="form.plan_date" type="date" class="kv-input" />
|
||||
<div class="kv-label">冷卷号</div>
|
||||
<input v-model="form.cold_coil_no" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">班次</div>
|
||||
<select v-model="form.shift" class="kv-input">
|
||||
<option value="">不限</option>
|
||||
<option v-for="s in ['甲','乙','丙','丁']" :key="s" :value="s">{{ s }}班</option>
|
||||
<div class="kv-label">热卷号</div>
|
||||
<input v-model="form.hot_coil_no" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">钢种</div>
|
||||
<input v-model="form.steel_grade" class="kv-input" placeholder="QTGLG-2019" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">来料厚度 (mm)</div>
|
||||
<input v-model.number="form.incoming_thickness" type="number" step="0.01" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">产品厚度 (mm)</div>
|
||||
<input v-model.number="form.product_thickness" type="number" step="0.01" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">偏差上限</div>
|
||||
<input v-model.number="form.deviation_upper" type="number" step="0.001" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">偏差下限</div>
|
||||
<input v-model.number="form.deviation_lower" type="number" step="0.001" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">来料宽度 (mm)</div>
|
||||
<input v-model.number="form.incoming_width" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">产品宽度 (mm)</div>
|
||||
<input v-model.number="form.product_width" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">包装要求</div>
|
||||
<select v-model="form.packaging_req" class="kv-input">
|
||||
<option value="">—</option>
|
||||
<option value="裸包">裸包</option>
|
||||
<option value="筒包">筒包</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">优先级 (1-10)</div>
|
||||
<input v-model.number="form.priority" type="number" min="1" max="10" class="kv-input" />
|
||||
<div class="kv-label">卷径 (mm)</div>
|
||||
<input v-model.number="form.coil_diameter" type="number" step="1" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">计划数量 (卷)</div>
|
||||
<input v-model.number="form.plan_quantity" type="number" class="kv-input" />
|
||||
<div class="kv-label">分卷数</div>
|
||||
<input v-model.number="form.split_count" type="number" min="1" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">计划重量 (kg)</div>
|
||||
<input v-model.number="form.plan_weight" type="number" class="kv-input" />
|
||||
<div class="kv-label">下工序</div>
|
||||
<input v-model="form.next_process" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">主要钢种</div>
|
||||
<input v-model="form.steel_grade" class="kv-input" />
|
||||
<div class="kv-label">计划时间</div>
|
||||
<input v-model="form.plan_date" type="datetime-local" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">规格范围</div>
|
||||
<input v-model="form.spec_range" class="kv-input" />
|
||||
<div class="kv-label">状态</div>
|
||||
<select v-model="form.status" class="kv-input">
|
||||
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,11 +180,10 @@
|
||||
import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm } from '@/api'
|
||||
|
||||
const STATUS_MAP = {
|
||||
draft: { label: '草稿', badge: 'badge-gray' },
|
||||
confirmed: { label: '已确认', badge: 'badge-blue' },
|
||||
in_progress: { label: '执行中', badge: 'badge-green' },
|
||||
completed: { label: '完成', badge: 'badge-gray' },
|
||||
cancelled: { label: '取消', badge: 'badge-red' },
|
||||
ready: { label: '准备好', badge: 'badge-green' },
|
||||
online: { label: '在线', badge: 'badge-yellow' },
|
||||
producing: { label: '生产中', badge: 'badge-yellow' },
|
||||
produced: { label: '产出', badge: 'badge-blue' },
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -146,41 +192,62 @@ export default {
|
||||
return {
|
||||
loading: false, saving: false,
|
||||
tableData: [], total: 0,
|
||||
query: { page: 1, page_size: 20, status: '', start_date: '', end_date: '' },
|
||||
query: { page: 1, page_size: 50, status: '', cold_coil_no: '', start_date: '', end_date: '' },
|
||||
statusOptions: Object.entries(STATUS_MAP).map(([value, { label }]) => ({ value, label })),
|
||||
dialogVisible: false, editRow: null, form: { priority: 5 },
|
||||
dialogVisible: false, editRow: null, form: { split_count: 1, status: 'ready' },
|
||||
}
|
||||
},
|
||||
created() { this.fetchData() },
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
const params = { ...this.query }
|
||||
if (params.start_date) params.start_date += 'T00:00:00'
|
||||
if (params.end_date) params.end_date += 'T23:59:59'
|
||||
try { const res = await getPlans(params); this.tableData = res.data.items; this.total = res.data.total } finally { this.loading = false }
|
||||
const params = { page: this.query.page, page_size: this.query.page_size }
|
||||
if (this.query.status) params.status = this.query.status
|
||||
if (this.query.start_date) params.start_date = this.query.start_date + 'T00:00:00'
|
||||
if (this.query.end_date) params.end_date = this.query.end_date + 'T23:59:59'
|
||||
try {
|
||||
const res = await getPlans(params)
|
||||
let items = res.data.items
|
||||
if (this.query.cold_coil_no) {
|
||||
items = items.filter(x => (x.cold_coil_no || '').includes(this.query.cold_coil_no))
|
||||
}
|
||||
this.tableData = items
|
||||
this.total = res.data.total
|
||||
} finally { this.loading = false }
|
||||
},
|
||||
fmtDate(t) { return t ? t.slice(0, 10) : '—' },
|
||||
statusLabel(s) { return STATUS_MAP[s]?.label || s },
|
||||
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
||||
fmtNum(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' },
|
||||
statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' },
|
||||
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
|
||||
completionRate(row) { return row.plan_quantity > 0 ? Math.min(100, Math.round(row.actual_quantity / row.plan_quantity * 100)) : 0 },
|
||||
rateColor(row) {
|
||||
const r = this.completionRate(row)
|
||||
return r >= 90 ? 'var(--accent-green)' : r >= 70 ? 'var(--accent-yellow)' : 'var(--accent-red)'
|
||||
openDialog(row = null) {
|
||||
this.editRow = row
|
||||
if (row) {
|
||||
const r = { ...row }
|
||||
if (r.plan_date) r.plan_date = r.plan_date.slice(0, 16)
|
||||
this.form = r
|
||||
} else {
|
||||
this.form = { plan_no: '', split_count: 1, status: 'ready', plan_date: this.nowDT() }
|
||||
}
|
||||
this.dialogVisible = true
|
||||
},
|
||||
nowDT() {
|
||||
const d = new Date(); const p = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`
|
||||
},
|
||||
openDialog(row = null) { this.editRow = row; this.form = row ? { ...row } : { priority: 5, plan_quantity: 0, plan_weight: 0 }; this.dialogVisible = true },
|
||||
async confirmPlan(row) {
|
||||
if (!confirm(`确认计划 ${row.plan_no}?`)) return
|
||||
if (!confirm(`将计划 ${row.plan_no} 上线?`)) return
|
||||
await apiConfirm(row.id)
|
||||
this.$message.success('已确认')
|
||||
this.$message.success('已上线')
|
||||
this.fetchData()
|
||||
},
|
||||
async save() {
|
||||
if (!this.form.plan_no) { this.$message.error('计划号不能为空'); return }
|
||||
if (!this.form.plan_date) { this.$message.error('计划时间不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
const d = { ...this.form }
|
||||
if (d.plan_date && !d.plan_date.includes('T')) d.plan_date += 'T00:00:00'
|
||||
if (d.plan_date && !d.plan_date.includes(':')) d.plan_date += 'T00:00:00'
|
||||
else if (d.plan_date && d.plan_date.length === 16) d.plan_date += ':00'
|
||||
if (this.editRow) await updatePlan(this.editRow.id, d)
|
||||
else await createPlan(d)
|
||||
this.$message.success('保存成功')
|
||||
@@ -195,6 +262,7 @@ export default {
|
||||
@import '@/assets/styles/variables';
|
||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 12px; &:hover { text-decoration: underline; } }
|
||||
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
|
||||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
|
||||
|
||||
Reference in New Issue
Block a user