refactor(wms): 重构钢卷追溯组件以支持合卷展示

- 将原有的线性时间轴改造成支持线性段和合卷并排段的布局结构
- 新增 TraceStepBody 组件用于统一渲染追溯步骤内容
- 在后端服务中添加 traceLayout 字段用于返回前端展示结构
- 实现合卷场景下的多列并排展示功能
- 优化步骤排序逻辑,确保按存储顺序正确排列
- 添加合卷前各卷加工过程的并排展示界面
- 实现合卷汇聚节点的特殊展示效果
This commit is contained in:
2026-05-14 15:57:25 +08:00
parent 8075d76c11
commit c53dd4c97e
5 changed files with 505 additions and 131 deletions

View File

@@ -1,123 +1,53 @@
<template>
<div class="trace-result-container">
<!-- 操作步骤基于标准化step渲染 -->
<el-card shadow="hover" class="mb20" v-if="standardSteps && standardSteps.length > 0">
<el-card shadow="hover" class="mb20" v-if="tracePanels && tracePanels.length > 0">
<div slot="header" class="card-header">
<span class="title-dot"></span>
<span class="title-text">钢卷追溯操作步骤</span>
</div>
<el-timeline v-loading="loadingCoilDetails">
<el-timeline-item v-for="(step, index) in standardSteps" :key="index"
:timestamp="`步骤 ${step.original.display_step || step.original.step}`" placement="top">
<el-card shadow="hover">
<div class="step-header">
<span class="step-action">{{ step.action }}</span>
<el-tag size="mini" :type="getStepTagType(step.action)">{{ step.original.operation || step.action
}}</el-tag>
<el-tag size="mini" type="info" v-if="step.operation">
操作人{{ step.operation }}
</el-tag>
</div>
<!-- 标准化步骤详情 - 核心展示旧钢卷/新钢卷关键信息 -->
<div class="step-details">
<!-- 核心操作信息 -->
<el-row class="detail-row" v-if="step.time">
<el-col :span="8" class="detail-label">操作时间</el-col>
<el-col :span="16" class="detail-value">{{ step.time }}</el-col>
</el-row>
<template v-for="(panel, pIdx) in tracePanels">
<!-- 线性段单条时间线 -->
<div v-if="panel.type === 'linear'" :key="'lin-' + pIdx" class="trace-panel-block">
<el-timeline v-loading="loadingCoilDetails">
<el-timeline-item v-for="(rawStep, idx) in panel.steps" :key="pIdx + '-' + idx"
:timestamp="stepTimestamp(rawStep)" placement="top">
<el-card shadow="hover">
<trace-step-body :standard-step="formatStep(rawStep)"
:get-step-tag-type="getStepTagType" :parse-changed-fields="parseChangedFields" />
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<!-- 新增步骤专属显示卷号 -->
<el-row class="detail-row" v-if="step.action === '创建'">
<el-col :span="8" class="detail-label">卷号</el-col>
<el-col :span="16" class="detail-value">{{ step.original.current_coil_no }}</el-col>
</el-row>
<!-- 旧钢卷关键信息操作前- 新增优化展示核心字段hover弹窗更多信息 -->
<el-row class="detail-row" v-if="step.oldCoilInfoList && step.oldCoilInfoList.length">
<el-col :span="8" class="detail-label">操作前钢卷</el-col>
<el-col :span="16" class="detail-value">
<div class="coil-info-item" v-for="(coil, idx) in step.oldCoilInfoList" :key="idx">
<el-popover placement="right" trigger="hover" width="400" v-if="coil.currentCoilNo != '-'"
:loading="loadingCoilDetails">
<div class="coil-detail-item">
<p><b>入场卷号</b>{{ coil.enterCoilNo || '-' }}</p>
<p><b>当前卷号</b>{{ coil.currentCoilNo || '-' }}</p>
<p><b>物料类型</b>{{ coil.materialType || '-' }}</p>
<p><b>物料名称</b>{{ coil.itemName || '-' }}</p>
<p><b>规格</b>{{ coil.specification || '-' }}</p>
<p><b>材质</b>{{ coil.material || '-' }}</p>
<p><b>生产厂家</b>{{ coil.manufacturer || '-' }}</p>
<p><b>净重</b>{{ coil.netWeight }} kg</p>
<p><b>逻辑库区</b>{{ coil.warehouseName || '-' }}</p>
<p><b>实际库存</b>{{ coil.actualWarehouseName || '-' }}</p>
<p><b>镀层质量</b>{{ coil.zincLayer || '-' }}</p>
<p><b>质量状态</b>{{ coil.qualityStatus || '-' }}</p>
<p><b>创建时间</b>{{ coil.createTime || '-' }}</p>
</div>
<div slot="reference" class="coil-info-summary">
{{ coil.currentCoilNo || '-' }} {{ coil.itemName }}{{ coil.specification }}- {{
coil.materialType }} - {{ coil.netWeight }}kg
</div>
</el-popover>
<div class="coil-info-summary coil-info-deleted" v-else>该钢卷已被回滚操作删除无法查询到信息</div>
</div>
</el-col>
</el-row>
<!-- 新钢卷关键信息操作后- 新增优化展示核心字段hover弹窗更多信息 -->
<el-row class="detail-row" v-if="step.newCoilInfoList && step.newCoilInfoList.length">
<el-col :span="8" class="detail-label">操作后钢卷</el-col>
<el-col :span="16" class="detail-value">
<div class="coil-info-item" v-for="(coil, idx) in step.newCoilInfoList" :key="idx">
<el-popover placement="right" trigger="hover" width="400" v-if="coil.currentCoilNo != '-'"
:loading="loadingCoilDetails">
<div class="coil-detail-item">
<p><b>入场卷号</b>{{ coil.enterCoilNo || '-' }}</p>
<p><b>当前卷号</b>{{ coil.currentCoilNo || '-' }}</p>
<p><b>物料类型</b>{{ coil.materialType || '-' }}</p>
<p><b>物料名称</b>{{ coil.itemName || '-' }}</p>
<p><b>规格</b>{{ coil.specification || '-' }}</p>
<p><b>材质</b>{{ coil.material || '-' }}</p>
<p><b>生产厂家</b>{{ coil.manufacturer || '-' }}</p>
<p><b>净重</b>{{ coil.netWeight }} kg</p>
<p><b>逻辑库区</b>{{ coil.warehouseName || '-' }}</p>
<p><b>实际库存</b>{{ coil.actualWarehouseName || '-' }}</p>
<p><b>镀层质量</b>{{ coil.zincLayer || '-' }}</p>
<p><b>质量状态</b>{{ coil.qualityStatus || '-' }}</p>
<p><b>创建时间</b>{{ coil.createTime || '-' }}</p>
</div>
<div slot="reference" class="coil-info-summary">
{{ coil.currentCoilNo || '-' }} {{ coil.itemName }}{{ coil.specification }}- {{
coil.materialType }} - {{ coil.netWeight }}kg
</div>
</el-popover>
<div class="coil-info-summary coil-info-deleted" v-else>该钢卷已被回滚操作删除无法查询到信息</div>
</div>
</el-col>
</el-row>
<!-- 更新专属修改内容保留原有逻辑适配上下文 -->
<el-row class="detail-row" v-if="step.changedFields">
<el-col :span="8" class="detail-label">修改内容</el-col>
<el-col :span="16" class="detail-value">
<div class="changed-field-item" v-for="(item, idx) in parseChangedFields(step.changedFields)"
:key="idx">
{{ item.field }}<span class="old-value">{{ item.oldVal }}</span> <span class="new-value">{{
item.newVal }}</span>
</div>
</el-col>
</el-row>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<!-- 合卷段多列并排 + 合卷汇聚 -->
<div v-else-if="panel.type === 'merge_join'" :key="'mj-' + pIdx" class="trace-panel-block merge-join-block">
<div class="merge-caption">合卷前各卷加工过程并排展示</div>
<el-row :gutter="12" type="flex" class="merge-lanes-row">
<el-col v-for="(lane, li) in panel.lanes" :key="li" :span="laneColSpan(panel.lanes.length)" class="merge-lane-col">
<div class="lane-header-tag">
{{ (panel.laneLabels && panel.laneLabels[li]) || ('来源 ' + (li + 1)) }}
</div>
<div class="lane-steps-stack">
<el-card v-for="(rawStep, si) in lane" :key="si" shadow="hover" class="lane-step-card mb10">
<trace-step-body :standard-step="formatStep(rawStep)"
:get-step-tag-type="getStepTagType" :parse-changed-fields="parseChangedFields" compact />
</el-card>
</div>
</el-col>
</el-row>
<div class="merge-converge">
<el-divider content-position="center">合卷汇聚</el-divider>
<el-card shadow="hover">
<trace-step-body :standard-step="formatStep(panel.mergeStep)"
:get-step-tag-type="getStepTagType" :parse-changed-fields="parseChangedFields" />
</el-card>
</div>
</div>
</template>
</el-card>
<!-- 无记录提示 -->
<div class="empty-tip" v-if="!standardSteps || standardSteps.length === 0">
<div class="empty-tip" v-if="!tracePanels || tracePanels.length === 0">
<el-empty description="未找到相关钢卷记录"></el-empty>
</div>
</div>
@@ -125,9 +55,11 @@
<script>
import { listMaterialCoil } from '@/api/wms/coil';
import TraceStepBody from './TraceStepBody.vue';
export default {
name: 'CoilTraceResult',
components: { TraceStepBody },
props: {
// 追溯结果数据
traceResult: {
@@ -144,21 +76,28 @@ export default {
};
},
computed: {
// 生成标准化步骤列表(核心基于原始steps转换新增旧/新钢卷信息列表
// 生成标准化步骤列表(用于批量拉取钢卷详情等
standardSteps() {
if (!this.traceResult || !this.traceResult.steps || this.traceResult.steps.length === 0) {
return [];
}
// 遍历原始步骤,转换为标准化步骤(包含旧/新钢卷关键信息)
return this.traceResult.steps.map(step => this.formatStep(step));
}
},
/** 后端拆好的线性段 / 合卷并排段;无 traceLayout 时退化为单条线性 */
tracePanels() {
if (this.traceResult && Array.isArray(this.traceResult.traceLayout) && this.traceResult.traceLayout.length) {
return this.traceResult.traceLayout;
}
if (this.traceResult && this.traceResult.steps && this.traceResult.steps.length) {
return [{ type: 'linear', steps: this.traceResult.steps }];
}
return [];
},
},
watch: {
traceResult: {
handler(newVal) {
if (newVal) {
console.log('追溯结果更新,已生成标准化步骤:', this.standardSteps);
// 步骤更新后,防抖获取钢卷详情(避免频繁请求)
this.debounceFetchCoilDetails();
} else {
// 无追溯结果时,清空缓存
@@ -169,6 +108,14 @@ export default {
}
},
methods: {
stepTimestamp(rawStep) {
const d = rawStep.display_step != null ? rawStep.display_step : rawStep.step;
return `步骤 ${d}`;
},
laneColSpan(laneCount) {
const n = Math.max(1, laneCount || 1);
return Math.max(1, Math.floor(24 / n));
},
// 根据操作类型获取标签样式
getStepTagType(action) {
const typeMap = {
@@ -295,7 +242,6 @@ export default {
standardStep.newCoilIds = originalStep.current_coil_id ? [originalStep.current_coil_id.trim()] : [];
break;
case '更新':
console.log('更新操作后钢卷ID:', originalStep.new_coil_id, originalStep);
standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : [];
break;
case '分卷':
@@ -417,7 +363,6 @@ export default {
step.restoredCoilInfo = step.newCoilInfoList[0] || null;
}
});
console.log('更新后的标准化步骤:', this.standardSteps);
} else {
this.$message.warning('获取钢卷详情失败');
}
@@ -466,6 +411,59 @@ export default {
margin-bottom: 20px;
}
.trace-panel-block {
margin-bottom: 24px;
}
.trace-panel-block:last-child {
margin-bottom: 0;
}
.merge-join-block {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
background: #fafbfc;
}
.merge-caption {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
font-weight: 500;
}
.merge-lanes-row {
align-items: flex-start;
}
.merge-lane-col {
min-width: 0;
}
.lane-header-tag {
font-size: 13px;
font-weight: 600;
color: #409eff;
margin-bottom: 8px;
padding: 6px 10px;
background: #ecf5ff;
border-radius: 4px;
word-break: break-all;
}
.lane-steps-stack {
min-height: 40px;
}
.lane-step-card.mb10 {
margin-bottom: 10px;
}
.merge-converge {
margin-top: 16px;
}
/* 操作步骤样式增强 */
.step-header {
display: flex;

View File

@@ -0,0 +1,196 @@
<template>
<div :class="compact ? 'step-details step-details-compact' : 'step-details'">
<div class="step-header">
<span class="step-action">{{ standardStep.action }}</span>
<el-tag size="mini" :type="getStepTagType(standardStep.action)">
{{ standardStep.original.operation || standardStep.action }}
</el-tag>
<el-tag size="mini" type="info" v-if="standardStep.operation">
操作人{{ standardStep.operation }}
</el-tag>
</div>
<el-row class="detail-row" v-if="standardStep.time && !compact">
<el-col :span="8" class="detail-label">操作时间</el-col>
<el-col :span="16" class="detail-value">{{ standardStep.time }}</el-col>
</el-row>
<el-row class="detail-row" v-if="compact && standardStep.time">
<el-col :span="24" class="detail-value text-muted">{{ standardStep.time }}</el-col>
</el-row>
<el-row class="detail-row" v-if="standardStep.action === '创建'">
<el-col :span="compact ? 24 : 8" class="detail-label">卷号</el-col>
<el-col :span="compact ? 24 : 16" class="detail-value">{{ standardStep.original.current_coil_no }}</el-col>
</el-row>
<el-row class="detail-row" v-if="standardStep.oldCoilInfoList && standardStep.oldCoilInfoList.length">
<el-col :span="compact ? 24 : 8" class="detail-label">操作前钢卷</el-col>
<el-col :span="compact ? 24 : 16" class="detail-value">
<div class="coil-info-item" v-for="(coil, idx) in standardStep.oldCoilInfoList" :key="idx">
<el-popover placement="right" trigger="hover" width="400" v-if="coil.currentCoilNo != '-'">
<div class="coil-detail-item">
<p><b>入场卷号</b>{{ coil.enterCoilNo || '-' }}</p>
<p><b>当前卷号</b>{{ coil.currentCoilNo || '-' }}</p>
<p><b>物料类型</b>{{ coil.materialType || '-' }}</p>
<p><b>物料名称</b>{{ coil.itemName || '-' }}</p>
<p><b>规格</b>{{ coil.specification || '-' }}</p>
<p><b>材质</b>{{ coil.material || '-' }}</p>
<p><b>生产厂家</b>{{ coil.manufacturer || '-' }}</p>
<p><b>净重</b>{{ coil.netWeight }} kg</p>
<p><b>逻辑库区</b>{{ coil.warehouseName || '-' }}</p>
<p><b>实际库存</b>{{ coil.actualWarehouseName || '-' }}</p>
<p><b>镀层质量</b>{{ coil.zincLayer || '-' }}</p>
<p><b>质量状态</b>{{ coil.qualityStatus || '-' }}</p>
<p><b>创建时间</b>{{ coil.createTime || '-' }}</p>
</div>
<div slot="reference" class="coil-info-summary">
{{ coil.currentCoilNo || '-' }} {{ coil.itemName }}{{ coil.specification }}-{{ coil.materialType }}-{{ coil.netWeight }}kg
</div>
</el-popover>
<div class="coil-info-summary coil-info-deleted" v-else>该钢卷已被回滚操作删除无法查询到信息</div>
</div>
</el-col>
</el-row>
<el-row class="detail-row" v-if="standardStep.newCoilInfoList && standardStep.newCoilInfoList.length">
<el-col :span="compact ? 24 : 8" class="detail-label">操作后钢卷</el-col>
<el-col :span="compact ? 24 : 16" class="detail-value">
<div class="coil-info-item" v-for="(coil, idx) in standardStep.newCoilInfoList" :key="idx">
<el-popover placement="right" trigger="hover" width="400" v-if="coil.currentCoilNo != '-'">
<div class="coil-detail-item">
<p><b>入场卷号</b>{{ coil.enterCoilNo || '-' }}</p>
<p><b>当前卷号</b>{{ coil.currentCoilNo || '-' }}</p>
<p><b>物料类型</b>{{ coil.materialType || '-' }}</p>
<p><b>物料名称</b>{{ coil.itemName || '-' }}</p>
<p><b>规格</b>{{ coil.specification || '-' }}</p>
<p><b>材质</b>{{ coil.material || '-' }}</p>
<p><b>生产厂家</b>{{ coil.manufacturer || '-' }}</p>
<p><b>净重</b>{{ coil.netWeight }} kg</p>
<p><b>逻辑库区</b>{{ coil.warehouseName || '-' }}</p>
<p><b>实际库存</b>{{ coil.actualWarehouseName || '-' }}</p>
<p><b>镀层质量</b>{{ coil.zincLayer || '-' }}</p>
<p><b>质量状态</b>{{ coil.qualityStatus || '-' }}</p>
<p><b>创建时间</b>{{ coil.createTime || '-' }}</p>
</div>
<div slot="reference" class="coil-info-summary">
{{ coil.currentCoilNo || '-' }} {{ coil.itemName }}{{ coil.specification }}-{{ coil.materialType }}-{{ coil.netWeight }}kg
</div>
</el-popover>
<div class="coil-info-summary coil-info-deleted" v-else>该钢卷已被回滚操作删除无法查询到信息</div>
</div>
</el-col>
</el-row>
<el-row class="detail-row" v-if="standardStep.changedFields">
<el-col :span="compact ? 24 : 8" class="detail-label">修改内容</el-col>
<el-col :span="compact ? 24 : 16" class="detail-value">
<div class="changed-field-item" v-for="(item, idx) in parseChangedFields(standardStep.changedFields)" :key="idx">
{{ item.field }}<span class="old-value">{{ item.oldVal }}</span> <span class="new-value">{{ item.newVal }}</span>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'TraceStepBody',
props: {
standardStep: { type: Object, required: true },
getStepTagType: { type: Function, required: true },
parseChangedFields: { type: Function, required: true },
compact: { type: Boolean, default: false },
},
};
</script>
<style scoped>
.step-header {
display: flex;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
}
.step-action {
font-size: 15px;
font-weight: 600;
color: #333;
margin-right: 8px;
}
.step-details {
padding: 10px 0;
}
.step-details-compact .step-header {
margin-bottom: 8px;
}
.detail-row {
margin-bottom: 12px;
align-items: flex-start;
}
.detail-label {
color: #666;
font-size: 14px;
padding-top: 4px;
}
.detail-value {
color: #333;
font-size: 14px;
}
.text-muted {
color: #909399;
font-size: 13px;
}
.changed-field-item {
margin-bottom: 4px;
line-height: 1.5;
}
.old-value {
color: #F56C6C;
}
.new-value {
color: #67C23A;
}
.coil-detail-item {
font-size: 13px;
line-height: 1.8;
color: #333;
}
.coil-detail-item p {
margin: 0;
padding: 2px 0;
}
.coil-detail-item p b {
color: #666;
min-width: 80px;
}
.coil-info-item {
margin-bottom: 8px;
display: inline-block;
margin-right: 12px;
}
.coil-info-summary {
cursor: pointer;
color: #409EFF;
background: #f5faff;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #e6f7ff;
white-space: nowrap;
}
.coil-info-deleted {
color: #F56C6C;
background: #fff5f5;
}
</style>