feat: 质保书图表多样化(柱/折线/面积) + 质量管理去掉检验任务

- 质保书过程图表按单位分配柱状/折线/面积图,不再只有柱状
- 质量管理删除「检验任务」Tab与相关代码
- 左侧钢卷列表改为来自实际生产实绩(production records),钢卷信息映射实绩字段
- 异常记录不再默认预置6行,进入加载已有缺陷,点「新增行」逐条添加

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 17:00:41 +08:00
parent ba8a2fd69a
commit 94abe0f16f
2 changed files with 63 additions and 260 deletions

View File

@@ -339,21 +339,37 @@ export default {
const u = it.unit || '其他'
;(groups[u] = groups[u] || []).push({ label: it.label, value: v })
})
return Object.entries(groups).map(([unit, items], gi) => ({
unit,
option: {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 44, right: 12, top: 16, bottom: 78 },
xAxis: {
type: 'category',
data: items.map(i => i.label.replace(/(开卷机|九辊矫直机|切头剪|酸洗槽|漂洗槽|三辊张力|平整机|静电涂油机|卷取机|夹送辊|挤干辊)\s?/g, '')),
axisLabel: { rotate: 40, fontSize: 9, color: '#666' },
axisLine: { lineStyle: { color: '#ddd' } },
const KIND = { 'm/min': 'area', 'kN': 'line', '°C': 'bar', 'A': 'bar', 'Hz': 'line', 'kN·m': 'bar', 'g/L': 'area' }
return Object.entries(groups).map(([unit, items], gi) => {
const color = CHART_COLORS[gi % CHART_COLORS.length]
const data = items.map(i => i.value)
const kind = KIND[unit] || (gi % 2 ? 'line' : 'bar')
let series
if (kind === 'bar') {
series = { type: 'bar', data, itemStyle: { color, borderRadius: [3, 3, 0, 0] }, barMaxWidth: 18 }
} else {
series = {
type: 'line', data, smooth: true, symbol: 'circle', symbolSize: 6,
itemStyle: { color }, lineStyle: { color, width: 2 },
areaStyle: kind === 'area' ? { color, opacity: 0.12 } : undefined,
}
}
return {
unit,
option: {
tooltip: { trigger: 'axis', axisPointer: { type: kind === 'bar' ? 'shadow' : 'line' } },
grid: { left: 44, right: 12, top: 16, bottom: 78 },
xAxis: {
type: 'category', boundaryGap: kind === 'bar',
data: items.map(i => i.label.replace(/(开卷机|九辊矫直机|切头剪|酸洗槽|漂洗槽|三辊张力|平整机|静电涂油机|卷取机|夹送辊|挤干辊)\s?/g, '')),
axisLabel: { rotate: 40, fontSize: 9, color: '#666' },
axisLine: { lineStyle: { color: '#ddd' } },
},
yAxis: { type: 'value', name: unit, nameTextStyle: { color: '#999', fontSize: 10 }, axisLabel: { color: '#666', fontSize: 9 }, splitLine: { lineStyle: { color: '#eee' } } },
series: [series],
},
yAxis: { type: 'value', name: unit, nameTextStyle: { color: '#999', fontSize: 10 }, axisLabel: { color: '#666', fontSize: 9 }, splitLine: { lineStyle: { color: '#eee' } } },
series: [{ type: 'bar', data: items.map(i => i.value), itemStyle: { color: CHART_COLORS[gi % CHART_COLORS.length] }, barMaxWidth: 18 }],
},
}))
}
})
},
},
created() { this.fetchData() },

View File

@@ -1,93 +1,5 @@
<template>
<div>
<!-- 标签页 -->
<div class="tab-bar">
<span :class="['tab-item', { active: activeTab === 'tasks' }]" @click="activeTab = 'tasks'">检验任务</span>
<span :class="['tab-item', { active: activeTab === 'abnormal' }]" @click="switchToAbnormal">异常管理</span>
</div>
<!-- 检验任务 Tab -->
<template v-if="activeTab === 'tasks'">
<!-- 过滤栏 -->
<div class="card">
<div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row">
<span class="kv-label">任务编号</span>
<input v-model="taskQuery.task_code" class="kv-input" style="width:130px;" @keyup.enter="fetchTasks" />
</div>
<div class="flex-row">
<span class="kv-label">卷号</span>
<input v-model="taskQuery.coil_no" class="kv-input" style="width:120px;" @keyup.enter="fetchTasks" />
</div>
<div class="flex-row">
<span class="kv-label">状态</span>
<select v-model="taskQuery.status" class="kv-input" style="width:100px;">
<option value="">全部</option>
<option value="0">待检验</option>
<option value="1">检验中</option>
<option value="2">待审核</option>
<option value="3">完成</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">结果</span>
<select v-model="taskQuery.result" class="kv-input" style="width:100px;">
<option value="">全部</option>
<option value="qualified">合格</option>
<option value="unqualified">不合格</option>
</select>
</div>
<div class="flex-row">
<button class="btn btn-primary" @click="fetchTasks">查询</button>
<button class="btn btn-outline" @click="openTaskDialog()"> 新增任务</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
检验任务列表
<span class="ch-badge"> {{ taskTotal }} </span>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<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 taskData" :key="row.id">
<td class="td-num">{{ row.task_code }}</td>
<td class="td-num">{{ row.coil_no || '—' }}</td>
<td>{{ row.scheme_name || '—' }}</td>
<td class="td-muted">{{ row.inspect_user || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
<td><span :class="['badge', taskStatusBadge(row.status)]">{{ taskStatusLabel(row.status) }}</span></td>
<td>
<span v-if="row.result" :class="['badge', row.result === 'qualified' ? 'badge-green' : 'badge-red']">
{{ row.result === 'qualified' ? '合格' : '不合格' }}
</span>
<span v-else class="td-muted"></span>
</td>
<td class="td-muted">{{ fmtTime(row.created_at) }}</td>
<td>
<span class="action-link" @click="openTaskDialog(row)">编辑</span>
<span class="action-link" style="color:#da3633;" @click="deleteTask(row)">删除</span>
</td>
</tr>
<tr v-if="!taskData.length">
<td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- 异常管理 Tab -->
<template v-if="activeTab === 'abnormal'">
<div class="abn-layout">
@@ -107,10 +19,10 @@
:class="['cl-item', { active: selectedCoil && selectedCoil.id === c.id }]"
@click="selectCoil(c)"
>
<div class="cl-name">{{ c.coil_no }}</div>
<div class="cl-name">{{ c.sub_coil_no || 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>
<span class="td-muted" style="font-size:10px;">{{ c.outlet_thickness ? fmtNum(c.outlet_thickness, 2) + '×' + fmtNum(c.outlet_width, 0) : '' }}</span>
</div>
</div>
<div v-if="!coils.length" class="cl-empty">暂无钢卷</div>
@@ -130,16 +42,21 @@
</div>
<div class="card-body">
<div class="kv-grid">
<div class="kv-cell"><span class="kv-label">入场卷号</span><span class="kv-value">{{ selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">订单</span><span class="kv-value">{{ selectedCoil.order_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">卷号</span><span class="kv-value">{{ selectedCoil.sub_coil_no || selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">热卷</span><span class="kv-value">{{ selectedCoil.hot_coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">钢种</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">/</span><span class="kv-value">{{ (selectedCoil.shift || '—') + ' / ' + (selectedCoil.team != null ? selectedCoil.team : '—') }}</span></div>
<div class="kv-cell"><span class="kv-label">规格[mm]</span><span class="kv-value">{{ specStr }}</span></div>
<div class="kv-cell"><span class="kv-label">目标厚度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.target_thickness, 2) }}</span></div>
<div class="kv-cell"><span class="kv-label">目标宽[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.target_width, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">内径[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.inner_diameter, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">毛重[t]</span><span class="kv-value">{{ weightT(selectedCoil.gross_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">净重[t]</span><span class="kv-value">{{ weightT(selectedCoil.net_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">状态</span><span class="kv-value">{{ coilStatusLabel(selectedCoil.status) }}</span></div>
<div class="kv-cell"><span class="kv-label">来料厚度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.incoming_thickness, 2) }}</span></div>
<div class="kv-cell"><span class="kv-label">出口厚[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.outlet_thickness, 2) }}</span></div>
<div class="kv-cell"><span class="kv-label">来料宽度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.incoming_width, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">出口宽度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.outlet_width, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">来料重量[t]</span><span class="kv-value">{{ fmtNum(selectedCoil.incoming_weight, 3) }}</span></div>
<div class="kv-cell"><span class="kv-label">称重重量[t]</span><span class="kv-value">{{ fmtNum(selectedCoil.weighed_weight, 3) }}</span></div>
<div class="kv-cell"><span class="kv-label">成品长度[m]</span><span class="kv-value">{{ fmtNum(selectedCoil.product_length, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">吨钢长度</span><span class="kv-value">{{ fmtNum(selectedCoil.length_per_ton, 2) }}</span></div>
<div class="kv-cell"><span class="kv-label">下线时间</span><span class="kv-value">{{ fmtTime(selectedCoil.offline_time) }}</span></div>
<div class="kv-cell"><span class="kv-label">状态</span><span class="kv-value">{{ statusLabel(selectedCoil.status) }}</span></div>
<div class="kv-cell"><span class="kv-label">备注</span><span class="kv-value">{{ selectedCoil.remark || '—' }}</span></div>
</div>
</div>
@@ -246,7 +163,7 @@
<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>
<option v-for="c in coils" :key="c.id" :value="coilKey(c)" v-show="coilKey(c) !== selectedCoilNo">{{ coilKey(c) }} · {{ c.steel_grade || '' }}</option>
</select>
</div>
</div>
@@ -257,86 +174,15 @@
</div>
</div>
<!-- 新增/编辑任务弹窗 -->
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
<div class="modal-box" style="width:560px;">
<div class="modal-header">
{{ editTask ? '编辑任务 #' + editTask.id : '新增检验任务' }}
<span class="modal-close" @click="taskDialogVisible = false"></span>
</div>
<div class="modal-body">
<div class="grid-2" style="gap:12px;">
<div class="form-field">
<div class="kv-label">任务编号 *</div>
<input v-model="taskForm.task_code" class="kv-input" :disabled="!!editTask" />
</div>
<div class="form-field">
<div class="kv-label">卷号</div>
<input v-model="taskForm.coil_no" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">任务类型</div>
<select v-model="taskForm.task_type" class="kv-input">
<option value="">请选择</option>
<option value="incoming">来料检验</option>
<option value="process">过程检验</option>
<option value="final">成品检验</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">方案名称</div>
<input v-model="taskForm.scheme_name" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">检验人员</div>
<input v-model="taskForm.inspect_user" class="kv-input" />
</div>
<div v-if="editTask" class="form-field">
<div class="kv-label">状态</div>
<select v-model="taskForm.status" class="kv-input">
<option :value="0">待检验</option>
<option :value="1">检验中</option>
<option :value="2">待审核</option>
<option :value="3">完成</option>
</select>
</div>
<div v-if="editTask" class="form-field">
<div class="kv-label">检验结果</div>
<select v-model="taskForm.result" class="kv-input">
<option value="">待定</option>
<option value="qualified">合格</option>
<option value="unqualified">不合格</option>
</select>
</div>
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">备注</div>
<textarea v-model="taskForm.remark" class="kv-input" rows="2"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="taskDialogVisible = false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="saveTask">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {
getQcTasks, createQcTask, updateQcTask, deleteQcTask,
getCoils, getCoil,
getProductionRecords,
getQcDefectsByCoil, bulkSaveQcDefects,
} from '@/api'
const TASK_STATUS = {
0: { label: '待检验', badge: 'badge-gray' },
1: { label: '检验中', badge: 'badge-yellow' },
2: { label: '待审核', badge: 'badge' },
3: { label: '完成', badge: 'badge-green' },
}
const DEFECT_CODES = [
{ value: 'S', label: '表面缺陷S' },
{ value: 'E', label: '边部问题E' },
@@ -379,14 +225,8 @@ export default {
inheritDialog: { visible: false, source: '' },
imgPreview: '',
// 任务
taskData: [], taskTotal: 0,
taskQuery: { page: 1, page_size: 20, task_code: '', coil_no: '', status: '', result: '' },
taskDialogVisible: false, editTask: null, taskForm: {},
// 异常管理
coils: [],
coilQuery: { coil_no: '', page: 1, page_size: 50 },
coilQuery: { coil_no: '', page: 1, page_size: 100 },
selectedCoil: null,
defects: [],
@@ -397,76 +237,27 @@ export default {
computed: {
specStr() {
const c = this.selectedCoil || {}
if (c.spec_thickness && c.spec_width) return `${c.spec_thickness}*${c.spec_width}`
if (c.outlet_thickness && c.outlet_width) return `${Number(c.outlet_thickness).toFixed(2)}*${Number(c.outlet_width).toFixed(0)}`
return '—'
},
},
created() { this.fetchCoils(); this.fetchTasks() },
created() { this.fetchCoils() },
methods: {
// ── 任务 ──────────────────────────────────────
async fetchTasks() {
const params = { page: this.taskQuery.page, page_size: this.taskQuery.page_size }
if (this.taskQuery.task_code) params.task_code = this.taskQuery.task_code
if (this.taskQuery.coil_no) params.coil_no = this.taskQuery.coil_no
if (this.taskQuery.status !== '') params.status = this.taskQuery.status
if (this.taskQuery.result) params.result = this.taskQuery.result
try {
const res = await getQcTasks(params)
this.taskData = res.data.items
this.taskTotal = res.data.total
} catch (e) { /* ignore */ }
},
openTaskDialog(row = null) {
this.editTask = row
this.taskForm = row ? { ...row } : { task_code: '', status: 0 }
this.taskDialogVisible = true
},
async saveTask() {
if (!this.taskForm.task_code) { this.$message.error('任务编号不能为空'); return }
this.saving = true
try {
if (this.editTask) {
await updateQcTask(this.editTask.id, this.taskForm)
} else {
await createQcTask(this.taskForm)
}
this.$message.success('保存成功')
this.taskDialogVisible = false
this.fetchTasks()
} finally { this.saving = false }
},
async deleteTask(row) {
if (!confirm(`确认删除任务 ${row.task_code}`)) return
try {
await deleteQcTask(row.id)
this.$message.success('已删除')
this.fetchTasks()
} catch (e) { this.$message.error('删除失败') }
},
// ── 异常管理 ──────────────────────────────────
async switchToAbnormal() {
this.activeTab = 'abnormal'
if (!this.coils.length) await this.fetchCoils()
},
async fetchCoils() {
try {
const params = { page: 1, page_size: 50 }
const params = { page: 1, page_size: 100 }
if (this.coilQuery.coil_no) params.coil_no = this.coilQuery.coil_no
const res = await getCoils(params)
const res = await getProductionRecords(params)
this.coils = res.data.items || []
} catch (e) { this.coils = [] }
},
coilKey(c) { return c ? (c.sub_coil_no || c.coil_no) : '' },
async selectCoil(c) {
this.selectedCoil = c
this.selectedCoilNo = c ? c.coil_no : ''
this.selectedCoilNo = this.coilKey(c)
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: '' }
@@ -492,16 +283,16 @@ export default {
} catch (e) { this.$message.error('继承失败') }
},
async reloadCoil() {
if (!this.selectedCoil) return
try {
const res = await getCoil(this.selectedCoil.coil_no)
this.selectedCoil = res.data
} catch (e) { /* ignore */ }
await this.fetchCoils()
if (this.selectedCoil) {
const fresh = this.coils.find(c => c.id === this.selectedCoil.id)
if (fresh) this.selectedCoil = fresh
}
},
async loadDefects() {
if (!this.selectedCoil) return
try {
const res = await getQcDefectsByCoil(this.selectedCoil.coil_no)
const res = await getQcDefectsByCoil(this.coilKey(this.selectedCoil))
this.defects = (res.data || []).map(d => ({
defect_desc: d.defect_desc || '',
start_position: d.start_position || 0,
@@ -517,8 +308,7 @@ export default {
inherit_source: d.inherit_source || '',
image_url: d.image_url || '',
}))
while (this.defects.length < 6) this.defects.push(blankDefect())
} catch (e) { this.defects = Array.from({ length: 6 }, () => blankDefect()) }
} catch (e) { this.defects = [] }
},
addDefectRow() { this.defects.push(blankDefect()) },
removeRow(i) { this.defects.splice(i, 1) },
@@ -554,7 +344,7 @@ export default {
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 })
await bulkSaveQcDefects({ coil_no: this.coilKey(this.selectedCoil), defects: list })
this.$message.success('保存成功')
await this.loadDefects()
} catch (e) {
@@ -588,10 +378,7 @@ export default {
// ── 工具 ─────────────────────────────────────
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
fmtNum(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
coilStatusLabel(s) { return ({ waiting: '等待入线', on_line: '在线处理', finished: '处理完成', abnormal: '异常' })[s] || s || '—' },
taskStatusLabel(s) { return TASK_STATUS[s]?.label || s },
taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' },
weightT(kg) { return kg ? (kg / 1000).toFixed(3) : '—' },
statusLabel(s) { return ({ UNWEIGH: '未称重', PRODUCT: '已产出' })[s] || s || '—' },
},
}
</script>