Files
bj_power/bj_power_mes/frontend/src/pages/Trace.vue
T
SunYF d3eda6b588 feat&refactor: 2026-09-22 半成品业务模型重构与功能完善
- 重构半成品流转逻辑:进度权威源改为 workpiece.currentStationNo,新增 completedText 快照字段,废除 doneProcessCodes
- 清理物料品类中半成品类型,存量数据幂等清理,调整物料管理方式文案为批次/序列号
- 新增日排产状态专用更新接口,修复工单工艺校验逻辑
- 新增物料档案代理接口、深路径文件下载接口
- 优化前端界面文案与工位选择逻辑,补充追溯页半成品流转展示
- 调整WMS端物料品类校验与入库接口契约
- 新增/更新数据库表结构与实体类代码
2026-09-22 14:42:52 +08:00

230 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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>
<el-card>
<el-form inline>
<el-form-item label="工件SN">
<el-input v-model="sn" style="width:220px" placeholder="输入SN查询追溯" @keyup.enter="search" />
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="orderNo" style="width:200px" placeholder="按工单号列出工件(模糊)" @keyup.enter="searchByOrder" />
</el-form-item>
<el-form-item><el-button type="primary" @click="search">追溯查询</el-button></el-form-item>
<el-form-item><el-button @click="searchByOrder">按工单查询</el-button></el-form-item>
<el-form-item>
<el-button type="warning" :disabled="!sn" @click="printCard">打印流程卡</el-button>
</el-form-item>
</el-form>
<el-card v-if="orderRows.length" shadow="never" style="margin-bottom:12px">
<template #header>工单{{ orderNo }}下的工件共 {{ orderRows.length }} 追溯查看单个工件</template>
<el-table :data="orderRows" border size="small" max-height="240">
<el-table-column prop="sn" label="工件SN" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="currentProcess" label="当前工位" width="100" align="center" />
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="pickSn(row.sn)">追溯</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-alert v-if="error" :title="error" type="error" show-icon closable style="margin-bottom:10px" />
<template v-if="data">
<el-descriptions :column="4" border style="margin-bottom:12px" title="工件信息">
<el-descriptions-item label="工件SN">{{ data.workpiece?.sn }}</el-descriptions-item>
<el-descriptions-item label="所属工单">{{ data.workpiece?.orderNo }}</el-descriptions-item>
<el-descriptions-item v-if="data.workpiece?.productCode || data.order?.productCode" label="产品型号">
{{ data.workpiece?.productCode || data.order?.productCode }}{{ (data.workpiece?.productName || data.order?.productName) ? '' + (data.workpiece?.productName || data.order?.productName) + '' : '' }}
</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusText(data.workpiece?.status) }}</el-descriptions-item>
<el-descriptions-item v-if="data.workpiece?.currentProcess" label="当前工位">{{ data.workpiece?.currentProcess }}</el-descriptions-item>
<el-descriptions-item v-if="data.workpiece?.onlineAt" label="进线时间">{{ fmt(data.workpiece?.onlineAt) }}</el-descriptions-item>
<el-descriptions-item v-if="data.workpiece?.doneAt" label="完工时间">{{ fmt(data.workpiece?.doneAt) }}</el-descriptions-item>
</el-descriptions>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="追溯链路:产品型号(物)→ 工单(批次任务)→ 工件(实例SN)。本页展示单个工件的完整生命周期。" />
<el-tabs>
<el-tab-pane label="工位时间线">
<el-timeline>
<el-timeline-item v-for="p in (data.processTimeline||[])" :key="p.id"
:timestamp="p.operator ? (p.operator+' · '+fmt(p.endedAt)) : fmt(p.endedAt)"
:type="p.result==='NG'?'danger':'success'">
{{ p.processName }}工位 {{ p.stationNo || p.processCode }})— {{ p.result }}
</el-timeline-item>
</el-timeline>
</el-tab-pane>
<el-tab-pane label="步骤考核数据">
<el-table :data="data.stepData||[]" border size="small">
<el-table-column prop="stepName" label="步骤" min-width="110" />
<el-table-column prop="criterionName" label="考核项" min-width="110" />
<el-table-column prop="value" label="数值" width="100" />
<el-table-column prop="isOK" label="合格" width="80">
<template #default="{row}"><el-tag :type="row.isOK?'success':'danger'">{{ row.isOK?'合格':'不合格' }}</el-tag></template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="90" />
</el-table>
</el-tab-pane>
<el-tab-pane label="拧紧数据">
<el-table :data="data.torqueRecords||[]" border size="small">
<el-table-column prop="stationNo" label="工位" width="80" />
<el-table-column prop="torque" label="扭矩" width="100" />
<el-table-column prop="angle" label="角度" width="90" />
<el-table-column prop="result" label="结果" width="80" />
<el-table-column prop="operator" label="操作人" width="90" />
<el-table-column prop="time" label="时间" min-width="160" />
</el-table>
</el-tab-pane>
<el-tab-pane label="成品关联">
<p>批次号{{ (data.associationTrace?.batchItems||[]).join(', ') || '无' }}</p>
<p>电气件SN{{ (data.associationTrace?.serialItems||[]).join(', ') || '无' }}</p>
</el-tab-pane>
<el-tab-pane label="暂存退库/重上线">
<el-alert type="info" :closable="false" style="margin-bottom:10px"
title="半成品流转记录:暂存退库=入 WMS 库、重上线=出库继续执行;「完成至」为流转时的进度快照(最后报工工位)。" />
<el-table :data="data.semiFlows||[]" border size="small">
<el-table-column label="动作" width="110" align="center">
<template #default="{row}">
<el-tag :type="row.action==='INBOUND' ? 'warning' : 'success'">{{ row.action==='INBOUND' ? '暂存退库' : '重上线' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completedText" label="完成至工位" min-width="150">
<template #default="{row}">{{ row.completedText || '尚未报工' }}</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="110" />
<el-table-column prop="targetDock" label="目标接驳台" width="110">
<template #default="{row}">{{ row.targetDock || '-' }}</template>
</el-table-column>
<el-table-column label="时间" min-width="165">
<template #default="{row}">{{ fmt(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="装机绑定">
<el-alert :type="data.bindTrace?.complete ? 'success' : 'warning'" :closable="false" style="margin-bottom:10px"
:title="data.bindTrace?.complete ? '装配物料已全部绑齐 ✓' : ('装配物料未绑齐:' + (data.bindTrace?.missing || ''))" show-icon />
<el-table :data="data.bindTrace?.binds||[]" border size="small">
<el-table-column prop="processCode" label="工位" width="70" />
<el-table-column prop="materialCode" label="图号" min-width="110" />
<el-table-column prop="materialName" label="物料名称" min-width="110" />
<el-table-column prop="spec" label="规格" min-width="90" />
<el-table-column prop="bindType" label="类型" width="70">
<template #default="{row}">{{ row.bindType === 'SN' ? '序列号' : '批次' }}</template>
</el-table-column>
<el-table-column prop="bindValue" label="绑定值(SN/批次)" min-width="140" />
<el-table-column prop="operator" label="绑定人" width="90" />
<el-table-column prop="createdAt" label="绑定时间" min-width="165">
<template #default="{row}">{{ fmt(row.createdAt) }}</template>
</el-table-column>
</el-table>
<el-divider content-position="left">物料组成递归展开成品零部件到原材料</el-divider>
<el-table :data="flatBom" border size="small" row-key="_key" default-expand-all>
<el-table-column label="物料" min-width="180">
<template #default="{row}">
<span :style="{paddingLeft: (row._depth*18)+'px'}">{{ row.materialName }} <small>({{ row.materialCode }})</small></span>
</template>
</el-table-column>
<el-table-column prop="spec" label="规格" min-width="90" />
<el-table-column prop="qty" label="单台用量" width="100" />
<el-table-column label="管理方式" width="100">
<template #default="{row}">{{ row.manageMode === '2' ? '序列号' : '批次' }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</template>
</el-card>
<PageHelp :data="helpTrace" page="Trace" />
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { openPrintableHtml } from '../utils/print'
import PageHelp from '../components/PageHelp.vue'
import { helpTrace } from '../help'
const sn = ref('')
const orderNo = ref('')
const orderRows = ref([])
const data = ref(null)
const error = ref('')
// 按工单号列出该工单下的全部工件,点击行内「追溯」查看单个工件生命周期(客户第12条:记录查看加工单号查询)
async function searchByOrder() {
const no = orderNo.value.trim()
if (!no) {
ElMessage.warning('请输入工单号')
return
}
try {
orderRows.value = (await request.get('/workpieces', { params: { orderNo: no } })) || []
if (!orderRows.value.length) ElMessage.info('该工单下暂无工件')
} catch {
orderRows.value = []
}
}
function pickSn(v) {
sn.value = v
search()
}
async function search() {
if (!sn.value) return
error.value = ''
data.value = null
try {
data.value = await request.get('/trace', { params: { sn: sn.value } })
} catch (e) {
error.value = e.message || '查询失败'
}
}
async function printCard() {
if (!sn.value) return
// 打印前数据完整性预检
let missing = []
try {
const check = await request.get('/process-card', { params: { sn: sn.value, check: 1 } })
missing = check?.missing || []
} catch {
/* 预检失败不阻塞打印 */
}
if (missing.length) {
try {
await ElMessageBox.confirm(
`以下数据不完整,是否继续打印?\n\n${missing.map((m) => '· ' + m).join('\n')}`,
'打印流程卡', {
type: 'warning',
confirmButtonText: '继续打印',
cancelButtonText: '取消'
})
} catch {
return // 用户取消
}
}
// 直接 window.open 带不上 Bearer 头会被 401,改由 axios 取回 HTML 后写入新窗口
await openPrintableHtml('/process-card', { sn: sn.value })
}
// 把递归的物料组成树扁平化为带层级缩进的列表,避免 el-table 树形 row-key 冲突
const flatBom = computed(() => {
const out = []
const walk = (nodes, depth, prefix) => {
;(nodes || []).forEach((n, i) => {
const key = prefix + '-' + n.materialCode + '-' + i
out.push(Object.assign({}, n, { _depth: depth, _key: key }))
if (n.children && n.children.length) walk(n.children, depth + 1, key)
})
}
walk(data.value?.bindTrace?.bomTree || [], 0, 'r')
return out
})
function statusText(st) {
const map = { CREATED: '已创建', ONLINE: '生产中', DONE: '已完工', SCRAPPED: '已报废' }
return map[st] || st || '-'
}
function fmt(v) { return v ? String(v).replace('T', ' ').slice(0, 19) : '-' }
</script>