feat: 实时数据持久化到计划/实绩 + 质量管理改版(钢卷信息+异常记录+继承)
- 在产计划持久化实时数据快照(run_data),物料跟踪每次轮询上报;生产完成写入实绩 process_data - 实绩页点击行展开生产阶段数据 + 实时数据快照(测点列表) - 质量管理默认进入异常管理,改为全宽:顶部选卷 + 钢卷信息(5列) + 异常记录表 - 异常记录新增「继承来源」列与「继承」按钮(从来源卷复制缺陷),进入即预置6行 - qc_defect 增加 inherit_source 字段 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export const movePlan = (id, position) => request.patch(`/plan/${id}/move`, null
|
||||
export const getPositions = () => request.get('/plan/positions/all')
|
||||
export const commitProducing = id => request.patch(`/plan/${id}/commit`) // 投入生产
|
||||
export const getSaddle = () => request.get('/plan/saddle/current')
|
||||
export const saveRuntime = (id, data) => request.patch(`/plan/${id}/runtime`, data)
|
||||
export const seedPlans = (count = 50) => request.post('/plan/seed', null, { params: { count } })
|
||||
export const getLastPlanTemplate = () => request.get('/plan/last-template')
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPlans, startProducing } from '@/api'
|
||||
import { getPlans, startProducing, saveRuntime } from '@/api'
|
||||
function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
|
||||
function fix(v, n = 1) { return Number(v).toFixed(n) }
|
||||
|
||||
@@ -560,6 +560,20 @@ export default {
|
||||
const pp = this.producingPlan
|
||||
if (pp && pp.cold_coil_no) this.current.coil_no = pp.cold_coil_no
|
||||
this.prodBase = pp ? { id: pp.id, len: pp.run_length_m || 0, at: Date.now(), speed: pp.run_speed || 0 } : null
|
||||
// 持久化实时数据快照到在产计划
|
||||
if (pp) this.persistRuntime(pp)
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
async persistRuntime(pp) {
|
||||
try {
|
||||
const snapshot = {
|
||||
time: new Date().toISOString(),
|
||||
coil_no: pp.cold_coil_no || pp.plan_no,
|
||||
line_speed: Number(this.current.speed.toFixed(1)),
|
||||
weld_position: Number((this.weld.position * 100).toFixed(1)),
|
||||
items: this.rtItems.map(it => ({ label: it.label, value: it.val, unit: it.unit })),
|
||||
}
|
||||
await saveRuntime(pp.id, snapshot)
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
async movePlan(p) {
|
||||
|
||||
@@ -117,6 +117,20 @@
|
||||
<div class="sg-row"><span class="sg-k">成品质量</span><span class="sg-v">{{ fmt(selectedRow.product_quality) }} <i>%</i></span></div>
|
||||
<div class="sg-row"><span class="sg-k">操作员</span><span class="sg-v">{{ selectedRow.operator || '—' }}</span></div>
|
||||
</div>
|
||||
|
||||
<template v-if="selectedRow.process_data && selectedRow.process_data.items && selectedRow.process_data.items.length">
|
||||
<div class="card-header" style="border-top:1px solid var(--border);">
|
||||
实时数据快照
|
||||
<span class="ch-badge">{{ selectedRow.process_data.items.length }} 测点</span>
|
||||
<span class="td-muted" style="margin-left:auto;font-size:11px;">采集 {{ fmtTime(selectedRow.process_data.time) }}</span>
|
||||
</div>
|
||||
<div class="rt-grid">
|
||||
<div v-for="(it, i) in selectedRow.process_data.items" :key="i" class="rt-cell">
|
||||
<span class="rt-l">{{ it.label }}</span>
|
||||
<span class="rt-v">{{ it.value }}<i>{{ it.unit }}</i></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
@@ -354,6 +368,10 @@ export default {
|
||||
.sg-row { display: flex; align-items: center; gap: 10px; font-size: 12px; min-height: 22px; }
|
||||
.sg-k { color: $text-muted; min-width: 64px; }
|
||||
.sg-v { color: $text-primary; font-family: $font-mono; font-weight: 600; i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; font-size: 11px; margin-left: 2px; } }
|
||||
.rt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px 24px; padding: 10px 18px 16px; }
|
||||
.rt-cell { display: flex; justify-content: space-between; gap: 10px; font-size: 11.5px; border-bottom: 1px dashed $border; padding: 2px 0; }
|
||||
.rt-l { color: $text-secondary; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rt-v { color: $sms-teal; font-family: $font-mono; font-weight: 600; white-space: nowrap; i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; margin-left: 3px; } }
|
||||
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
|
||||
.data-table.compact th, .data-table.compact td { padding: 4px 6px; font-size: 11.5px; }
|
||||
|
||||
@@ -90,38 +90,24 @@
|
||||
|
||||
<!-- ═══════════════════════════════════════ 异常管理 Tab ═══════════════════════════════════════ -->
|
||||
<template v-if="activeTab === 'abnormal'">
|
||||
<div class="abn-layout">
|
||||
<!-- 左侧:钢卷列表 -->
|
||||
<div class="abn-sidebar">
|
||||
<div class="sidebar-header">
|
||||
钢卷列表
|
||||
<span class="add-btn" title="刷新" @click="fetchCoils">⟳</span>
|
||||
</div>
|
||||
<div class="sidebar-search">
|
||||
<input v-model="coilQuery.coil_no" class="kv-input" placeholder="搜索卷号..." style="width:100%;" @keyup.enter="fetchCoils" />
|
||||
</div>
|
||||
<div class="cl-list">
|
||||
<div
|
||||
v-for="c in coils"
|
||||
:key="c.id"
|
||||
:class="['cl-item', { active: selectedCoil && selectedCoil.id === c.id }]"
|
||||
@click="selectCoil(c)"
|
||||
>
|
||||
<div class="cl-name">{{ c.coil_no }}</div>
|
||||
<div class="cl-meta">
|
||||
<span class="td-muted" style="font-size:10px;">{{ c.steel_grade || '—' }}</span>
|
||||
<span class="td-muted" style="font-size:10px;">{{ c.spec_thickness ? c.spec_thickness + '×' + (c.spec_width || '?') : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!coils.length" class="cl-empty">暂无钢卷</div>
|
||||
<!-- 顶部:选择钢卷 -->
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:10px 14px;">
|
||||
<div class="flex-row" style="gap:12px;flex-wrap:wrap;">
|
||||
<span class="kv-label">钢卷</span>
|
||||
<select v-model="selectedCoilNo" class="kv-input" style="width:240px;" @change="onCoilChange">
|
||||
<option value="">请选择钢卷</option>
|
||||
<option v-for="c in coils" :key="c.id" :value="c.coil_no">{{ c.coil_no }} · {{ c.steel_grade || '—' }}</option>
|
||||
</select>
|
||||
<input v-model="coilQuery.coil_no" class="kv-input" style="width:160px;" placeholder="搜索卷号" @keyup.enter="fetchCoils" />
|
||||
<button class="btn btn-outline" @click="fetchCoils">刷新列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:异常管理面板 -->
|
||||
<div class="abn-main">
|
||||
<div v-if="!selectedCoil" class="empty-tip">请从左侧选择钢卷</div>
|
||||
<div v-if="!selectedCoil" class="card"><div class="card-body"><div class="empty-tip">请选择钢卷</div></div></div>
|
||||
|
||||
<template v-else>
|
||||
<template v-else>
|
||||
<!-- 钢卷信息 -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
@@ -167,9 +153,10 @@
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span>异常记录 <span class="ch-badge">{{ defects.length }} 行</span></span>
|
||||
<div class="flex-row" style="gap:8px;">
|
||||
<button class="btn btn-primary" style="padding:2px 14px;font-size:11px;" :disabled="saving" @click="saveDefects">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
<button class="btn btn-outline" style="padding:2px 12px;font-size:11px;" @click="openInherit">继承</button>
|
||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="addDefectRow">+ 新增行</button>
|
||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="loadDefects">⟳ 刷新</button>
|
||||
<button class="btn btn-primary" style="padding:2px 14px;font-size:11px;" :disabled="saving" @click="saveDefects">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
@@ -185,6 +172,7 @@
|
||||
<th style="width:180px;">断面位置</th>
|
||||
<th style="width:160px;">缺陷代码</th>
|
||||
<th style="width:110px;">程度</th>
|
||||
<th style="width:90px;">继承来源</th>
|
||||
<th style="width:60px;">主缺陷</th>
|
||||
<th style="min-width:120px;">缺陷图片</th>
|
||||
<th style="width:60px;">操作</th>
|
||||
@@ -218,6 +206,7 @@
|
||||
<input type="radio" :value="opt.value" v-model="d.degree" />{{ opt.label }}
|
||||
</label>
|
||||
</td>
|
||||
<td class="td-muted" style="text-align:center;">{{ d.inherit_source || '—' }}</td>
|
||||
<td style="text-align:center;"><input type="checkbox" v-model="d.is_main" /></td>
|
||||
<td><input v-model="d.image_url" class="kv-input" style="width:100%;" placeholder="图片URL" /></td>
|
||||
<td>
|
||||
@@ -226,16 +215,34 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!defects.length">
|
||||
<td colspan="12" class="td-muted" style="text-align:center;padding:18px;">暂无异常,点击「+ 新增行」开始录入</td>
|
||||
<td colspan="13" class="td-muted" style="text-align:center;padding:18px;">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- ─── 继承来源选择弹窗 ─── -->
|
||||
<div v-if="inheritDialog.visible" class="modal-mask" @click.self="inheritDialog.visible=false">
|
||||
<div class="modal-box" style="width:420px;">
|
||||
<div class="modal-header">继承缺陷 — 选择来源钢卷<span class="modal-close" @click="inheritDialog.visible=false">✕</span></div>
|
||||
<div class="modal-body">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">来源钢卷</div>
|
||||
<select v-model="inheritDialog.source" class="kv-input">
|
||||
<option value="">请选择</option>
|
||||
<option v-for="c in coils" :key="c.id" :value="c.coil_no" v-show="c.coil_no !== selectedCoilNo">{{ c.coil_no }} · {{ c.steel_grade || '—' }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="inheritDialog.visible=false">取消</button>
|
||||
<button class="btn btn-primary" @click="doInherit">确定继承</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ─── 新增/编辑任务弹窗 ─── -->
|
||||
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
|
||||
@@ -344,6 +351,7 @@ function blankDefect() {
|
||||
defect_code: '',
|
||||
degree: '',
|
||||
is_main: false,
|
||||
inherit_source: '',
|
||||
image_url: '',
|
||||
}
|
||||
}
|
||||
@@ -352,8 +360,10 @@ export default {
|
||||
name: 'Quality',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'tasks',
|
||||
activeTab: 'abnormal',
|
||||
saving: false,
|
||||
selectedCoilNo: '',
|
||||
inheritDialog: { visible: false, source: '' },
|
||||
|
||||
// 任务
|
||||
taskData: [], taskTotal: 0,
|
||||
@@ -377,7 +387,7 @@ export default {
|
||||
return '—'
|
||||
},
|
||||
},
|
||||
created() { this.fetchTasks() },
|
||||
created() { this.fetchCoils(); this.fetchTasks() },
|
||||
methods: {
|
||||
// ── 任务 ──────────────────────────────────────
|
||||
async fetchTasks() {
|
||||
@@ -435,8 +445,38 @@ export default {
|
||||
},
|
||||
async selectCoil(c) {
|
||||
this.selectedCoil = c
|
||||
this.selectedCoilNo = c ? c.coil_no : ''
|
||||
await this.loadDefects()
|
||||
},
|
||||
async onCoilChange() {
|
||||
const c = this.coils.find(x => x.coil_no === this.selectedCoilNo)
|
||||
if (c) await this.selectCoil(c)
|
||||
else { this.selectedCoil = null; this.defects = [] }
|
||||
},
|
||||
openInherit() {
|
||||
if (!this.selectedCoil) return
|
||||
this.inheritDialog = { visible: true, source: '' }
|
||||
},
|
||||
async doInherit() {
|
||||
const src = this.inheritDialog.source
|
||||
if (!src) { this.$message.error('请选择继承来源钢卷'); return }
|
||||
try {
|
||||
const res = await getQcDefectsByCoil(src)
|
||||
const rows = (res.data || []).map(d => ({
|
||||
defect_desc: d.defect_desc || '',
|
||||
start_position: d.start_position || 0,
|
||||
end_position: d.end_position || 0,
|
||||
upper_surface: !!d.upper_surface, lower_surface: !!d.lower_surface,
|
||||
side_op: !!d.side_op, side_middle: !!d.side_middle, side_drive: !!d.side_drive,
|
||||
defect_code: d.defect_code || '', degree: d.degree || '',
|
||||
is_main: !!d.is_main, inherit_source: src, image_url: d.image_url || '',
|
||||
}))
|
||||
if (!rows.length) { this.$message.warning('来源钢卷无缺陷记录'); return }
|
||||
this.defects = rows
|
||||
this.inheritDialog.visible = false
|
||||
this.$message.success(`已继承 ${rows.length} 条缺陷`)
|
||||
} catch (e) { this.$message.error('继承失败') }
|
||||
},
|
||||
async reloadCoil() {
|
||||
if (!this.selectedCoil) return
|
||||
try {
|
||||
@@ -460,10 +500,11 @@ export default {
|
||||
defect_code: d.defect_code || '',
|
||||
degree: d.degree || '',
|
||||
is_main: !!d.is_main,
|
||||
inherit_source: d.inherit_source || '',
|
||||
image_url: d.image_url || '',
|
||||
}))
|
||||
if (!this.defects.length) this.addDefectRow()
|
||||
} catch (e) { this.defects = [blankDefect()] }
|
||||
while (this.defects.length < 6) this.defects.push(blankDefect())
|
||||
} catch (e) { this.defects = Array.from({ length: 6 }, () => blankDefect()) }
|
||||
},
|
||||
addDefectRow() { this.defects.push(blankDefect()) },
|
||||
removeRow(i) { this.defects.splice(i, 1) },
|
||||
@@ -496,8 +537,9 @@ export default {
|
||||
defect_code: d.defect_code || null,
|
||||
degree: d.degree || null,
|
||||
is_main: !!d.is_main,
|
||||
inherit_source: d.inherit_source || null,
|
||||
image_url: d.image_url || null,
|
||||
}))
|
||||
})).filter(d => d.defect_desc || d.defect_code || d.start_position || d.end_position)
|
||||
await bulkSaveQcDefects({ coil_no: this.selectedCoil.coil_no, defects: list })
|
||||
this.$message.success('保存成功')
|
||||
await this.loadDefects()
|
||||
|
||||
Reference in New Issue
Block a user