- 重构半成品流转逻辑:进度权威源改为 workpiece.currentStationNo,新增 completedText 快照字段,废除 doneProcessCodes - 清理物料品类中半成品类型,存量数据幂等清理,调整物料管理方式文案为批次/序列号 - 新增日排产状态专用更新接口,修复工单工艺校验逻辑 - 新增物料档案代理接口、深路径文件下载接口 - 优化前端界面文案与工位选择逻辑,补充追溯页半成品流转展示 - 调整WMS端物料品类校验与入库接口契约 - 新增/更新数据库表结构与实体类代码
230 lines
12 KiB
Vue
230 lines
12 KiB
Vue
<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> |