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> <template>
<div class="trace-result-container"> <div class="trace-result-container">
<!-- 操作步骤基于标准化step渲染 --> <el-card shadow="hover" class="mb20" v-if="tracePanels && tracePanels.length > 0">
<el-card shadow="hover" class="mb20" v-if="standardSteps && standardSteps.length > 0">
<div slot="header" class="card-header"> <div slot="header" class="card-header">
<span class="title-dot"></span> <span class="title-dot"></span>
<span class="title-text">钢卷追溯操作步骤</span> <span class="title-text">钢卷追溯操作步骤</span>
</div> </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>
<!-- 标准化步骤详情 - 核心展示旧钢卷/新钢卷关键信息 --> <template v-for="(panel, pIdx) in tracePanels">
<div class="step-details"> <!-- 线性段单条时间线 -->
<!-- 核心操作信息 --> <div v-if="panel.type === 'linear'" :key="'lin-' + pIdx" class="trace-panel-block">
<el-row class="detail-row" v-if="step.time"> <el-timeline v-loading="loadingCoilDetails">
<el-col :span="8" class="detail-label">操作时间</el-col> <el-timeline-item v-for="(rawStep, idx) in panel.steps" :key="pIdx + '-' + idx"
<el-col :span="16" class="detail-value">{{ step.time }}</el-col> :timestamp="stepTimestamp(rawStep)" placement="top">
</el-row> <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 === '创建'"> <div v-else-if="panel.type === 'merge_join'" :key="'mj-' + pIdx" class="trace-panel-block merge-join-block">
<el-col :span="8" class="detail-label">卷号</el-col> <div class="merge-caption">合卷前各卷加工过程并排展示</div>
<el-col :span="16" class="detail-value">{{ step.original.current_coil_no }}</el-col> <el-row :gutter="12" type="flex" class="merge-lanes-row">
</el-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">
<!-- 旧钢卷关键信息操作前- 新增优化展示核心字段hover弹窗更多信息 --> {{ (panel.laneLabels && panel.laneLabels[li]) || ('来源 ' + (li + 1)) }}
<el-row class="detail-row" v-if="step.oldCoilInfoList && step.oldCoilInfoList.length"> </div>
<el-col :span="8" class="detail-label">操作前钢卷</el-col> <div class="lane-steps-stack">
<el-col :span="16" class="detail-value"> <el-card v-for="(rawStep, si) in lane" :key="si" shadow="hover" class="lane-step-card mb10">
<div class="coil-info-item" v-for="(coil, idx) in step.oldCoilInfoList" :key="idx"> <trace-step-body :standard-step="formatStep(rawStep)"
<el-popover placement="right" trigger="hover" width="400" v-if="coil.currentCoilNo != '-'" :get-step-tag-type="getStepTagType" :parse-changed-fields="parseChangedFields" compact />
:loading="loadingCoilDetails"> </el-card>
<div class="coil-detail-item"> </div>
<p><b>入场卷号</b>{{ coil.enterCoilNo || '-' }}</p> </el-col>
<p><b>当前卷号</b>{{ coil.currentCoilNo || '-' }}</p> </el-row>
<p><b>物料类型</b>{{ coil.materialType || '-' }}</p> <div class="merge-converge">
<p><b>物料名称</b>{{ coil.itemName || '-' }}</p> <el-divider content-position="center">合卷汇聚</el-divider>
<p><b>规格</b>{{ coil.specification || '-' }}</p> <el-card shadow="hover">
<p><b>材质</b>{{ coil.material || '-' }}</p> <trace-step-body :standard-step="formatStep(panel.mergeStep)"
<p><b>生产厂家</b>{{ coil.manufacturer || '-' }}</p> :get-step-tag-type="getStepTagType" :parse-changed-fields="parseChangedFields" />
<p><b>净重</b>{{ coil.netWeight }} kg</p> </el-card>
<p><b>逻辑库区</b>{{ coil.warehouseName || '-' }}</p> </div>
<p><b>实际库存</b>{{ coil.actualWarehouseName || '-' }}</p> </div>
<p><b>镀层质量</b>{{ coil.zincLayer || '-' }}</p> </template>
<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>
</el-card> </el-card>
<!-- 无记录提示 --> <div class="empty-tip" v-if="!tracePanels || tracePanels.length === 0">
<div class="empty-tip" v-if="!standardSteps || standardSteps.length === 0">
<el-empty description="未找到相关钢卷记录"></el-empty> <el-empty description="未找到相关钢卷记录"></el-empty>
</div> </div>
</div> </div>
@@ -125,9 +55,11 @@
<script> <script>
import { listMaterialCoil } from '@/api/wms/coil'; import { listMaterialCoil } from '@/api/wms/coil';
import TraceStepBody from './TraceStepBody.vue';
export default { export default {
name: 'CoilTraceResult', name: 'CoilTraceResult',
components: { TraceStepBody },
props: { props: {
// 追溯结果数据 // 追溯结果数据
traceResult: { traceResult: {
@@ -144,21 +76,28 @@ export default {
}; };
}, },
computed: { computed: {
// 生成标准化步骤列表(核心基于原始steps转换新增旧/新钢卷信息列表 // 生成标准化步骤列表(用于批量拉取钢卷详情等
standardSteps() { standardSteps() {
if (!this.traceResult || !this.traceResult.steps || this.traceResult.steps.length === 0) { if (!this.traceResult || !this.traceResult.steps || this.traceResult.steps.length === 0) {
return []; return [];
} }
// 遍历原始步骤,转换为标准化步骤(包含旧/新钢卷关键信息)
return this.traceResult.steps.map(step => this.formatStep(step)); 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: { watch: {
traceResult: { traceResult: {
handler(newVal) { handler(newVal) {
if (newVal) { if (newVal) {
console.log('追溯结果更新,已生成标准化步骤:', this.standardSteps);
// 步骤更新后,防抖获取钢卷详情(避免频繁请求)
this.debounceFetchCoilDetails(); this.debounceFetchCoilDetails();
} else { } else {
// 无追溯结果时,清空缓存 // 无追溯结果时,清空缓存
@@ -169,6 +108,14 @@ export default {
} }
}, },
methods: { 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) { getStepTagType(action) {
const typeMap = { const typeMap = {
@@ -295,7 +242,6 @@ export default {
standardStep.newCoilIds = originalStep.current_coil_id ? [originalStep.current_coil_id.trim()] : []; standardStep.newCoilIds = originalStep.current_coil_id ? [originalStep.current_coil_id.trim()] : [];
break; break;
case '更新': case '更新':
console.log('更新操作后钢卷ID:', originalStep.new_coil_id, originalStep);
standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : []; standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : [];
break; break;
case '分卷': case '分卷':
@@ -417,7 +363,6 @@ export default {
step.restoredCoilInfo = step.newCoilInfoList[0] || null; step.restoredCoilInfo = step.newCoilInfoList[0] || null;
} }
}); });
console.log('更新后的标准化步骤:', this.standardSteps);
} else { } else {
this.$message.warning('获取钢卷详情失败'); this.$message.warning('获取钢卷详情失败');
} }
@@ -466,6 +411,59 @@ export default {
margin-bottom: 20px; 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 { .step-header {
display: flex; 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>

View File

@@ -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 coilId 钢卷ID
* @param currentCoilNo 当前钢卷号(可选参数,用于查询特定子钢卷) * @param currentCoilNo 当前钢卷号(可选参数,用于查询特定子钢卷)

View File

@@ -99,7 +99,7 @@ public interface IWmsMaterialCoilService {
* *
* @param coilId 钢卷ID * @param coilId 钢卷ID
* @param currentCoilNo 当前钢卷号(可选,用于查询特定子钢卷) * @param currentCoilNo 当前钢卷号(可选,用于查询特定子钢卷)
* @return 溯源结果(包含二维码信息数据库记录) * @return 溯源结果(包含二维码信息、按存储顺序排列的 steps、traceLayout 并排展示结构、数据库记录)
*/ */
Map<String, Object> queryTrace(Long coilId, String currentCoilNo); Map<String, Object> queryTrace(Long coilId, String currentCoilNo);

View File

@@ -2095,10 +2095,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
// 合并所有参与合卷的原始钢卷的历史steps // 合并所有参与合卷的原始钢卷的历史steps
List<Map<String, Object>> steps = new ArrayList<>(); List<Map<String, Object>> steps = new ArrayList<>();
// 从参与合卷的原始钢卷中获取二维码信息并合并 // 从参与合卷的原始钢卷中获取二维码信息并合并,并记录每个父卷携带的历史步数(用于溯源并排展示)
List<Integer> parentHistStepCounts = new ArrayList<>();
if (originalCoils != null && !originalCoils.isEmpty()) { if (originalCoils != null && !originalCoils.isEmpty()) {
for (WmsMaterialCoilBo originalCoilBo : originalCoils) { for (WmsMaterialCoilBo originalCoilBo : originalCoils) {
if (originalCoilBo.getCoilId() != null) { if (originalCoilBo.getCoilId() != null) {
int sizeBefore = steps.size();
// 查询原始钢卷的二维码信息 // 查询原始钢卷的二维码信息
WmsMaterialCoil originalCoil = baseMapper.selectById(originalCoilBo.getCoilId()); WmsMaterialCoil originalCoil = baseMapper.selectById(originalCoilBo.getCoilId());
if (originalCoil != null && originalCoil.getQrcodeRecordId() != null) { 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_nos", String.join(",", originalCoilNos));
mergeStep.put("parent_coil_ids", String.join(",", originalCoilIds)); 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("new_current_coil_no", mergedCoilBo.getCurrentCoilNo());
mergeStep.put("operator", LoginHelper.getUsername()); // 操作者 mergeStep.put("operator", LoginHelper.getUsername()); // 操作者
steps.add(mergeStep); steps.add(mergeStep);
@@ -2468,11 +2472,14 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps"); 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> allCoilNos = new HashSet<>();
Set<String> operatorUsernames = new HashSet<>(); Set<String> operatorUsernames = new HashSet<>();
if (steps != null) { if (!orderedSteps.isEmpty()) {
for (Map<String, Object> step : steps) { for (Map<String, Object> step : orderedSteps) {
extractCoilNo(step, "current_coil_no", allCoilNos); extractCoilNo(step, "current_coil_no", allCoilNos);
extractCoilNo(step, "new_current_coil_no", allCoilNos); extractCoilNo(step, "new_current_coil_no", allCoilNos);
extractCoilNo(step, "old_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); Map<String, String> operatorNicknameMap = getOperatorNicknames(operatorUsernames);
List<Map<String, Object>> allSteps = new ArrayList<>(steps != null ? steps : new ArrayList<>()); List<Map<String, Object>> allSteps = orderedSteps;
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);
});
for (int i = 0; i < allSteps.size(); i++) { for (int i = 0; i < allSteps.size(); i++) {
allSteps.get(i).put("display_step", i + 1); allSteps.get(i).put("display_step", i + 1);
allSteps.get(i).put("original_step", allSteps.get(i).get("step")); 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_serial", qrRecord.getSerialNumber());
allSteps.get(i).put("qrcode_id", qrRecord.getRecordId()); 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; Set<String> filteredCoilNos = allCoilNos;
if (currentCoilNo != null && !currentCoilNo.trim().isEmpty()) { if (currentCoilNo != null && !currentCoilNo.trim().isEmpty()) {
final String filterValue = currentCoilNo; final String filterValue = currentCoilNo;
@@ -2567,6 +2569,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
resultMap.put("qrcode", qrRecord); resultMap.put("qrcode", qrRecord);
resultMap.put("steps", allSteps); resultMap.put("steps", allSteps);
resultMap.put("traceLayout", traceLayout);
resultMap.put("records", result); resultMap.put("records", result);
return resultMap; 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的钢卷数量和重量 * 按库区分组统计每种物品类型和物品ID的钢卷数量和重量