refactor(wms): 重构钢卷追溯组件以支持合卷展示
- 将原有的线性时间轴改造成支持线性段和合卷并排段的布局结构 - 新增 TraceStepBody 组件用于统一渲染追溯步骤内容 - 在后端服务中添加 traceLayout 字段用于返回前端展示结构 - 实现合卷场景下的多列并排展示功能 - 优化步骤排序逻辑,确保按存储顺序正确排列 - 添加合卷前各卷加工过程的并排展示界面 - 实现合卷汇聚节点的特殊展示效果
This commit is contained in:
@@ -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;
|
||||
|
||||
196
klp-ui/src/views/wms/coil/panels/TraceStepBody.vue
Normal file
196
klp-ui/src/views/wms/coil/panels/TraceStepBody.vue
Normal 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>
|
||||
@@ -318,7 +318,9 @@ public class WmsMaterialCoilController extends BaseController {
|
||||
|
||||
/**
|
||||
* 钢卷溯源查询
|
||||
* 根据钢卷ID查询二维码,解析content中的steps,然后根据steps中的钢卷号反向查询数据库
|
||||
* 根据钢卷ID查询二维码,解析content中的steps,然后根据steps中的钢卷号反向查询数据库。
|
||||
* 返回 data.steps 为按二维码存储顺序排列的全量步骤(合卷场景下勿依赖原 step 序号排序,已带 display_step);
|
||||
* data.traceLayout 为前端展示结构:linear(单时间线)与 merge_join(合卷前多列并排 + 合卷汇聚)交替。
|
||||
*
|
||||
* @param coilId 钢卷ID
|
||||
* @param currentCoilNo 当前钢卷号(可选参数,用于查询特定子钢卷)
|
||||
|
||||
@@ -99,7 +99,7 @@ public interface IWmsMaterialCoilService {
|
||||
*
|
||||
* @param coilId 钢卷ID
|
||||
* @param currentCoilNo 当前钢卷号(可选,用于查询特定子钢卷)
|
||||
* @return 溯源结果(包含二维码信息和数据库记录)
|
||||
* @return 溯源结果(包含二维码信息、按存储顺序排列的 steps、traceLayout 并排展示结构、数据库记录)
|
||||
*/
|
||||
Map<String, Object> queryTrace(Long coilId, String currentCoilNo);
|
||||
|
||||
|
||||
@@ -2095,10 +2095,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
// 合并所有参与合卷的原始钢卷的历史steps
|
||||
List<Map<String, Object>> steps = new ArrayList<>();
|
||||
|
||||
// 从参与合卷的原始钢卷中获取二维码信息并合并
|
||||
// 从参与合卷的原始钢卷中获取二维码信息并合并,并记录每个父卷携带的历史步数(用于溯源并排展示)
|
||||
List<Integer> parentHistStepCounts = new ArrayList<>();
|
||||
if (originalCoils != null && !originalCoils.isEmpty()) {
|
||||
for (WmsMaterialCoilBo originalCoilBo : originalCoils) {
|
||||
if (originalCoilBo.getCoilId() != null) {
|
||||
int sizeBefore = steps.size();
|
||||
// 查询原始钢卷的二维码信息
|
||||
WmsMaterialCoil originalCoil = baseMapper.selectById(originalCoilBo.getCoilId());
|
||||
if (originalCoil != null && originalCoil.getQrcodeRecordId() != null) {
|
||||
@@ -2114,6 +2116,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
}
|
||||
}
|
||||
}
|
||||
parentHistStepCounts.add(steps.size() - sizeBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2139,6 +2142,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
}
|
||||
mergeStep.put("parent_coil_nos", String.join(",", originalCoilNos));
|
||||
mergeStep.put("parent_coil_ids", String.join(",", originalCoilIds));
|
||||
mergeStep.put("parent_hist_step_counts", parentHistStepCounts);
|
||||
mergeStep.put("new_current_coil_no", mergedCoilBo.getCurrentCoilNo());
|
||||
mergeStep.put("operator", LoginHelper.getUsername()); // 操作者
|
||||
steps.add(mergeStep);
|
||||
@@ -2468,11 +2472,14 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
|
||||
|
||||
// 合卷会把多条产线的 steps 顺序拼接,各线局部 step 均为 1..n,存在重复序号;必须保留 JSON 数组顺序,不能按 step 字段排序
|
||||
List<Map<String, Object>> orderedSteps = steps != null ? new ArrayList<>(steps) : new ArrayList<>();
|
||||
|
||||
Set<String> allCoilNos = new HashSet<>();
|
||||
Set<String> operatorUsernames = new HashSet<>();
|
||||
|
||||
if (steps != null) {
|
||||
for (Map<String, Object> step : steps) {
|
||||
if (!orderedSteps.isEmpty()) {
|
||||
for (Map<String, Object> step : orderedSteps) {
|
||||
extractCoilNo(step, "current_coil_no", allCoilNos);
|
||||
extractCoilNo(step, "new_current_coil_no", allCoilNos);
|
||||
extractCoilNo(step, "old_current_coil_no", allCoilNos);
|
||||
@@ -2493,19 +2500,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
|
||||
Map<String, String> operatorNicknameMap = getOperatorNicknames(operatorUsernames);
|
||||
|
||||
List<Map<String, Object>> allSteps = new ArrayList<>(steps != null ? steps : new ArrayList<>());
|
||||
|
||||
allSteps.sort((a, b) -> {
|
||||
Integer stepA = (Integer) a.get("step");
|
||||
Integer stepB = (Integer) b.get("step");
|
||||
if (stepA == null) stepA = 0;
|
||||
if (stepB == null) stepB = 0;
|
||||
return stepA.compareTo(stepB);
|
||||
});
|
||||
List<Map<String, Object>> allSteps = orderedSteps;
|
||||
|
||||
for (int i = 0; i < allSteps.size(); i++) {
|
||||
allSteps.get(i).put("display_step", i + 1);
|
||||
allSteps.get(i).put("original_step", allSteps.get(i).get("step"));
|
||||
allSteps.get(i).put("storage_index", i);
|
||||
allSteps.get(i).put("qrcode_serial", qrRecord.getSerialNumber());
|
||||
allSteps.get(i).put("qrcode_id", qrRecord.getRecordId());
|
||||
|
||||
@@ -2517,6 +2517,8 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> traceLayout = buildTraceLayout(allSteps);
|
||||
|
||||
Set<String> filteredCoilNos = allCoilNos;
|
||||
if (currentCoilNo != null && !currentCoilNo.trim().isEmpty()) {
|
||||
final String filterValue = currentCoilNo;
|
||||
@@ -2567,6 +2569,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("qrcode", qrRecord);
|
||||
resultMap.put("steps", allSteps);
|
||||
resultMap.put("traceLayout", traceLayout);
|
||||
resultMap.put("records", result);
|
||||
|
||||
return resultMap;
|
||||
@@ -2643,6 +2646,181 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
}
|
||||
}
|
||||
|
||||
private static int stepNumberValue(Map<String, Object> step) {
|
||||
Object s = step.get("step");
|
||||
if (s == null) {
|
||||
return 0;
|
||||
}
|
||||
if (s instanceof Number) {
|
||||
return ((Number) s).intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(s.toString().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMergeTraceStep(Map<String, Object> step) {
|
||||
Object op = step.get("operation");
|
||||
return op != null && "合卷".equals(op.toString());
|
||||
}
|
||||
|
||||
private static List<String> splitCommaTokens(Object value) {
|
||||
if (value == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(value.toString().split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Integer> readParentHistStepCounts(Map<String, Object> mergeStep) {
|
||||
Object raw = mergeStep.get("parent_hist_step_counts");
|
||||
if (!(raw instanceof List)) {
|
||||
return null;
|
||||
}
|
||||
List<?> list = (List<?>) raw;
|
||||
List<Integer> out = new ArrayList<>();
|
||||
for (Object o : list) {
|
||||
if (o instanceof Number) {
|
||||
out.add(((Number) o).intValue());
|
||||
} else if (o != null) {
|
||||
try {
|
||||
out.add(Integer.parseInt(o.toString().trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
out.add(0);
|
||||
}
|
||||
}
|
||||
return out.isEmpty() ? null : out;
|
||||
}
|
||||
|
||||
private static List<Integer> inferLaneSizesByStepReset(List<Map<String, Object>> buffer) {
|
||||
if (buffer == null || buffer.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Integer> lengths = new ArrayList<>();
|
||||
int runStart = 0;
|
||||
Integer prev = null;
|
||||
for (int i = 0; i < buffer.size(); i++) {
|
||||
int cur = stepNumberValue(buffer.get(i));
|
||||
if (prev != null && (cur < prev || (cur == 1 && prev > 1))) {
|
||||
lengths.add(i - runStart);
|
||||
runStart = i;
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
lengths.add(buffer.size() - runStart);
|
||||
return lengths;
|
||||
}
|
||||
|
||||
private static List<Integer> distributeEvenly(int total, int parts) {
|
||||
if (parts <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Integer> r = new ArrayList<>();
|
||||
int base = total / parts;
|
||||
int rem = total % parts;
|
||||
for (int i = 0; i < parts; i++) {
|
||||
r.add(base + (i < rem ? 1 : 0));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private List<List<Map<String, Object>>> sliceBufferIntoMergeLanes(List<Map<String, Object>> buffer,
|
||||
Map<String, Object> mergeStep) {
|
||||
List<String> parentIds = splitCommaTokens(mergeStep.get("parent_coil_ids"));
|
||||
List<String> parentNos = splitCommaTokens(mergeStep.get("parent_coil_nos"));
|
||||
int n = Math.max(parentIds.size(), parentNos.size());
|
||||
if (n <= 0) {
|
||||
n = 1;
|
||||
}
|
||||
if (buffer == null) {
|
||||
buffer = Collections.emptyList();
|
||||
}
|
||||
int total = buffer.size();
|
||||
List<Integer> counts = readParentHistStepCounts(mergeStep);
|
||||
if (counts == null || counts.size() != n || counts.stream().mapToInt(Integer::intValue).sum() != total) {
|
||||
List<Integer> inferred = inferLaneSizesByStepReset(buffer);
|
||||
if (inferred.size() == n) {
|
||||
counts = inferred;
|
||||
} else {
|
||||
counts = distributeEvenly(total, n);
|
||||
}
|
||||
}
|
||||
List<List<Map<String, Object>>> lanes = new ArrayList<>();
|
||||
int offset = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int len = i < counts.size() ? Math.max(0, counts.get(i)) : 0;
|
||||
int end = Math.min(offset + len, total);
|
||||
lanes.add(new ArrayList<>(buffer.subList(offset, end)));
|
||||
offset = end;
|
||||
}
|
||||
if (offset < total) {
|
||||
if (!lanes.isEmpty()) {
|
||||
lanes.get(lanes.size() - 1).addAll(buffer.subList(offset, total));
|
||||
} else {
|
||||
lanes.add(new ArrayList<>(buffer.subList(0, total)));
|
||||
}
|
||||
}
|
||||
return lanes;
|
||||
}
|
||||
|
||||
private static List<String> buildMergeLaneLabels(Map<String, Object> mergeStep, int laneCount) {
|
||||
List<String> nos = splitCommaTokens(mergeStep.get("parent_coil_nos"));
|
||||
List<String> ids = splitCommaTokens(mergeStep.get("parent_coil_ids"));
|
||||
List<String> labels = new ArrayList<>();
|
||||
for (int i = 0; i < laneCount; i++) {
|
||||
String no = i < nos.size() ? nos.get(i) : null;
|
||||
String id = i < ids.size() ? ids.get(i) : null;
|
||||
if (StringUtils.isNotBlank(no)) {
|
||||
labels.add(no);
|
||||
} else if (StringUtils.isNotBlank(id)) {
|
||||
labels.add("ID:" + id);
|
||||
} else {
|
||||
labels.add("来源 " + (i + 1));
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将按存储顺序排列的 steps 拆成「线性段 + 合卷并排段」,供前端展示。
|
||||
*/
|
||||
private List<Map<String, Object>> buildTraceLayout(List<Map<String, Object>> orderedSteps) {
|
||||
List<Map<String, Object>> layout = new ArrayList<>();
|
||||
if (orderedSteps == null || orderedSteps.isEmpty()) {
|
||||
return layout;
|
||||
}
|
||||
List<Map<String, Object>> buffer = new ArrayList<>();
|
||||
for (Map<String, Object> s : orderedSteps) {
|
||||
if (isMergeTraceStep(s)) {
|
||||
Map<String, Object> panel = new HashMap<>();
|
||||
panel.put("type", "merge_join");
|
||||
List<List<Map<String, Object>>> lanes = sliceBufferIntoMergeLanes(new ArrayList<>(buffer), s);
|
||||
panel.put("lanes", lanes);
|
||||
panel.put("laneLabels", buildMergeLaneLabels(s, lanes.size()));
|
||||
panel.put("mergeStep", s);
|
||||
layout.add(panel);
|
||||
buffer.clear();
|
||||
} else {
|
||||
buffer.add(s);
|
||||
}
|
||||
}
|
||||
if (!buffer.isEmpty()) {
|
||||
Map<String, Object> linear = new HashMap<>();
|
||||
linear.put("type", "linear");
|
||||
linear.put("steps", new ArrayList<>(buffer));
|
||||
layout.add(linear);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询各个库区中不同类型的钢卷分布情况
|
||||
* 按库区分组,统计每种物品类型和物品ID的钢卷数量和重量
|
||||
|
||||
Reference in New Issue
Block a user