Files
klp-oa/klp-ui/src/views/wms/warehouse/components/WarehouseInterlaced.vue
砂糖 f1637501b2 fix(wms): 修复复合架拆分按钮显示逻辑及更新库位编码注释
修复复合架拆分按钮仅在支持拆分的列显示的问题
更新库位编码解析注释以反映最新格式要求
2025-12-30 10:36:44 +08:00

644 lines
20 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>
<div class="multi-layer-grid" ref="gridContainer">
<el-alert v-if="!isComposite" type="warning" title="当前库区不支持复合架" show-icon></el-alert>
<!-- 列标尺区域 - 添加切换按钮 -->
<div class="col-ruler" :style="{ '--cell-width': `${cellWidth}px` }">
<div class="ruler-empty"></div>
<div v-for="col in sortedColumnKeys" :key="`col-${col}`" class="ruler-item">
<!-- 列标尺文本 -->
<span class="column-number">{{ col }}</span>
<!-- 拆分/合并切换按钮 -->
<button class="split-merge-toggle"
v-if="isComposite && splitColumns.includes(Number(col))"
:class="{ 'is-split': getColumnLevel(col) === 4, 'is-merge': getColumnLevel(col) === 3 }"
@click.stop="handleColumnToggle(col)" :title="getColumnLevel(col) === 3 ? '点击切换为小卷状态' : '点击切换为大卷状态'">
<i class="el-icon-s-tools"></i>
<span class="toggle-text">{{ getColumnLevel(col) === 3 ? '大卷状态' : '小卷状态' }}</span>
</button>
</div>
</div>
<div class="row-grid-wrapper">
<div class="row-ruler" :style="{ '--total-height': `${rulerTotalHeight}px` }">
<div v-for="row in rulerMaxRow" :key="`row-${row}`" class="ruler-item">
{{ row }}
</div>
</div>
<div class="grid-container" :style="{
'--half-cell-width': `${halfCellWidth}px`,
'--cell-width': `${cellWidth}px`,
'--column-count': sortedColumnKeys.length * 2,
'--total-height': `${rulerTotalHeight}px`
}">
<template v-for="column in sortedColumnKeys">
<div class="column-container layer-1-container" v-if="columnWarehouseData[column]" :style="{
gridRow: '1 / -1',
gridColumn: `${(column - 1) * 2 + 1} / ${(column - 1) * 2 + 2}`,
'--item-height': `${columnWarehouseData[column].cellHeight}px`
}">
<div v-for="warehouse in columnWarehouseData[column].layer1"
:key="`warehouse-1-${warehouse.actualWarehouseId}`" class="warehouse-cell layer-1"
:class="{ disabled: warehouse.isEnabled === 0 }" @click.stop="handleCellClick(warehouse)">
<div class="cell-name">
<div class="cell-line1">{{ warehouse.actualWarehouseName || '-' }}</div>
<div class="cell-line2">{{ warehouse.currentCoilNo || '-' }}</div>
</div>
</div>
</div>
<div class="column-container layer-2-container" v-if="columnWarehouseData[column]" :style="{
gridRow: '1 / -1',
gridColumn: `${(column - 1) * 2 + 2} / ${(column - 1) * 2 + 3}`,
'--item-height': `${columnWarehouseData[column].cellHeight}px`,
'--offset-value': `${columnWarehouseData[column].cellHeight * 0.5}px`,
height: `${(columnWarehouseData[column].layer2.length * columnWarehouseData[column].cellHeight)}px`
}">
<div v-for="warehouse in columnWarehouseData[column].layer2"
:key="`warehouse-2-${warehouse.actualWarehouseId}`" class="warehouse-cell layer-2"
:class="{ disabled: warehouse.isEnabled === 0 }" :style="{
transform: `translateY(var(--offset-value))`,
position: 'relative'
}" @click.stop="handleCellClick(warehouse)">
<div class="cell-name">
<div class="cell-line1">{{ warehouse.actualWarehouseName || '-' }}</div>
<div class="cell-line2">{{ warehouse.currentCoilNo || '-' }}</div>
</div>
</div>
</div>
</template>
</div>
</div>
<el-dialog title="钢卷库位详情" :visible.sync="dialogOpen" width="600px" append-to-body>
<el-descriptions :column="2" border size="small" v-if="currentWarehouse">
<el-descriptions-item label="库位编码">{{ currentWarehouse.actualWarehouseCode }}</el-descriptions-item>
<el-descriptions-item label="库位名称">{{ currentWarehouse.actualWarehouseName || '无' }}</el-descriptions-item>
<el-descriptions-item label="钢卷编号">{{ currentWarehouse.currentCoilNo || '无' }}</el-descriptions-item>
<el-descriptions-item label="所属层级">{{ currentWarehouse.parsedInfo.layer || '未知' }}</el-descriptions-item>
<el-descriptions-item label="行号">{{ currentWarehouse.parsedInfo.row || '未知' }}</el-descriptions-item>
<el-descriptions-item label="列号">{{ currentWarehouse.parsedInfo.column || '未知' }}</el-descriptions-item>
<el-descriptions-item label="一级编码">{{ currentWarehouse.parsedInfo.warehouseFirst || '未知'
}}</el-descriptions-item>
<el-descriptions-item label="二级编码">{{ currentWarehouse.parsedInfo.warehouseSecond || '未知'
}}</el-descriptions-item>
<el-descriptions-item label="启用状态">
<el-tag :type="currentWarehouse.isEnabled === 1 ? 'success' : 'danger'">
{{ currentWarehouse.isEnabled === 1 ? '未占用' : currentWarehouse.currentCoilNo }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="库位码">
<QRCode :content="currentWarehouse.actualWarehouseId" :size="60" />
</el-descriptions-item>
<el-descriptions-item label="备注信息" span="2">{{ currentWarehouse.remark || '无' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import QRCode from '@/components/QRCode/index.vue';
export default {
name: "SteelCoilWarehouse",
components: {
QRCode
},
props: {
columns: {
type: Object,
required: true,
default: () => ({})
},
id: {
type: String,
default: ''
}
},
data() {
return {
// id转换位复合架规则如果不包含在内则不可使用复合架
compositeRules: {
// 测试库区
'2002207449418686465': {
col: 5,
big: 19,
small: 29,
rows: [0, 1, 2, 3, 4],
// 支持拆分的列
splitColumns: [1, 2, 3, 4, 5]
},
// 成品2库B区F2B
'1998933646134919170': {
col: 7,
big: 29 + 28,
small: 43 + 42,
rows: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26],
// 支持拆分的列
splitColumns: [1, 2, 3]
}
},
dialogOpen: false,
currentWarehouse: null,
containerWidth: 0,
resizeTimer: null,
currentContextWarehouse: null,
rulerRowHeight: 80,
// rulerMaxRow: 0,
isMounted: false,
};
},
computed: {
// 当前的库区是否支持复合架
isComposite() {
if (!this.compositeRules[this.id]) {
console.log('当前库区不支持复合架');
return false;
}
// 检车规则是否合法,
// 1. (small - big) / 2 = rows.length
if ((this.compositeRules[this.id].small - this.compositeRules[this.id].big) / 2 !== this.compositeRules[this.id].rows.length) {
console.log('检车规则不合法,(small - big) / 2 !== rows.length');
return false;
}
// 2. row中最大的值小于等于big
const maxRow = Math.max(...this.compositeRules[this.id].rows);
if (maxRow > this.compositeRules[this.id].big) {
console.log('检车规则不合法row中最大的值大于big');
return false;
}
return this.compositeRules[this.id];
},
splitColumns() {
if (!this.compositeRules[this.id]) {
console.log('当前库区不支持复合架');
return [];
}
return this.compositeRules[this.id].splitColumns || [];
},
sortedColumnKeys() {
return Object.keys(this.columns)
.map(key => Number(key))
.sort((a, b) => a - b);
},
rulerTotalHeight() {
return this.rulerMaxRow * this.rulerRowHeight;
},
cellWidth() {
if (!this.containerWidth || this.sortedColumnKeys.length === 0) return 60;
const availableWidth = Math.max(0, this.containerWidth - 30);
return availableWidth / this.sortedColumnKeys.length;
},
halfCellWidth() {
return this.cellWidth / 2;
},
columnWarehouseData() {
const result = {};
this.sortedColumnKeys.forEach(column => {
const columnData = this.columns[column] || {};
const layer1 = (columnData.layer1 || []).sort((a, b) => a.parsedInfo.row - b.parsedInfo.row);
const layer2 = (columnData.layer2 || []).sort((a, b) => a.parsedInfo.row - b.parsedInfo.row);
const totalRows = layer1.length;
const cellHeight = totalRows === 0
? this.rulerRowHeight
: (totalRows > this.rulerMaxRow
? this.rulerTotalHeight / totalRows
: this.rulerRowHeight);
result[column] = {
layer1,
layer2,
cellHeight,
totalRows
};
});
return result;
},
rulerMaxRow() {
// 取所有列中level为3的第一层的最大行数
// 如果不存在level为3的列则不执行filter操作直接去最大值
// 如果存在level为3的列则取layer1且level为3的最大行数
const columns = Object.values(this.columns)
const columnsOnlyLevel3 = columns.filter(col => col.layer1?.[0]?.parsedInfo?.level === 3);
let maxRows = []
if (columnsOnlyLevel3.length > 0) {
maxRows = columnsOnlyLevel3
.map(col => col.layer1?.length || 0);
} else {
maxRows = columns
.map(col => col.layer1?.length || 0);
}
return maxRows.length > 0 ? Math.max(...maxRows) : 0;
}
},
watch: {
columns: {
immediate: true,
deep: true,
handler(newVal) {
if (this.isMounted) {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.calcContainerWidth();
}, 50);
}
}
}
},
mounted() {
this.isMounted = true;
this.calcContainerWidth();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
clearTimeout(this.resizeTimer);
this.isMounted = false;
},
methods: {
// 获取指定列的level值3=合并4=拆分)
getColumnLevel(column) {
const columnData = this.columns[column] || {};
// 优先取列级别的level若无则取第一个库位的level
if (columnData.level) return columnData.level;
const firstWarehouse = columnData.layer1?.[0] || columnData.layer2?.[0];
return firstWarehouse?.parsedInfo?.level || 3; // 默认合并状态
},
// 处理列拆分/合并状态切换
handleColumnToggle(column) {
// 检查当前ID是否被包含在复合架规则中
if (!this.compositeRules[this.id]) {
this.$message.error('当前库位不支持复合架操作');
return;
}
const currentLevel = this.getColumnLevel(column);
// 切换level3↔4
const newLevel = currentLevel === 3 ? 4 : 3;
// 向父组件发送切换事件由父组件更新columns数据
// 如果切换为合并状态调用handleMergeWarehouse方法
// 需要二次确认
const columnData = this.columns[column] || {};
const columnFlag = columnData.layer1?.[0]?.actualWarehouseCode?.substring(0, 4) || ''; // 取前四位
this.$confirm(`确认将列 ${column} 切换为${newLevel === 3 ? '大卷' : '小卷'}状态吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 构造出请求数据
const payload = {
// 列的前四位例如F2A1
columnFlag,
}
console.log(payload, newLevel)
if (newLevel === 3) {
// 获取待分割的ID, 对应列第一层的前五个和第二层的前五个
const splitIds = [];
const layer1Warehouses = columnData.layer1 || [];
const layer2Warehouses = columnData.layer2 || [];
for (let i = 0; i < layer1Warehouses.length; i++) {
if (layer1Warehouses[i]) splitIds.push(layer1Warehouses[i].parentId);
}
for (let i = 0; i < layer2Warehouses.length; i++) {
if (layer2Warehouses[i]) splitIds.push(layer2Warehouses[i].parentId);
}
// 1. 先统计每个 parentId 出现的次数
// const countMap = splitIds.reduce((map, id) => {
// map.set(id, (map.get(id) || 0) + 1);
// return map;
// }, new Map());
// 2. 过滤出至少出现两次的 parentId再去重map的key本身唯一去重可省略但保留更严谨
// payload.locationIds = [...new Set(
// splitIds.filter(id => countMap.get(id) >= 2)
// )];
payload.locationIds = [...new Set(splitIds)]
console.log(payload)
this.handleMergeWarehouse(payload);
} else if (newLevel === 4) {
// 获取待分割的ID, 对应列第一层的前五个和第二层的前五个
const splitIds = [];
const layer1Warehouses = columnData.layer1 || [];
const layer2Warehouses = columnData.layer2 || [];
// 将对应的行拆分
const currentRule = this.compositeRules[this.id];
const rows = currentRule?.rows || []; // 最多拆分5个
for (let i = 0; i < rows.length; i++) {
if (layer1Warehouses[i]) splitIds.push(layer1Warehouses[rows[i]].actualWarehouseId);
if (layer2Warehouses[i]) splitIds.push(layer2Warehouses[rows[i]].actualWarehouseId);
}
payload.splitIds = splitIds;
console.log(payload)
// 如果切换为拆分状态调用handleSplitWarehouse方法
this.handleSplitWarehouse(payload);
} else {
// 其他情况,数据异常
this.$message.error(`${column} 切换状态异常,当前状态为${currentLevel}`);
// this.handleMergeWarehouse(column);
return;
}
})
},
getInterlacedWarehouseIds(warehouse) {
if (!warehouse?.parsedInfo) return [];
const { column, row } = warehouse.parsedInfo;
const columnData = this.columns[column];
if (!columnData) return [];
const ids = [];
const layer1Warehouse = columnData.layer1.find(w => w.parsedInfo.row === row);
const layer2Warehouse = columnData.layer2.find(w => w.parsedInfo.row === row);
if (layer1Warehouse) ids.push(layer1Warehouse.actualWarehouseId);
if (layer2Warehouse) ids.push(layer2Warehouse.actualWarehouseId);
return ids;
},
calcContainerWidth() {
if (!this.$refs.gridContainer) {
this.containerWidth = 0;
return;
}
this.containerWidth = this.$refs.gridContainer.clientWidth || 0;
},
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.calcContainerWidth();
}, 100);
},
handleCellClick(warehouse) {
console.log('格子点击触发,仓库数据:', warehouse);
this.currentWarehouse = { ...warehouse };
this.dialogOpen = true;
},
handleSplitWarehouse(payload) {
this.$emit('split-warehouse', payload);
},
handleMergeWarehouse(payload) {
this.$emit('merge-warehouse', payload);
},
}
};
</script>
<style scoped lang="scss">
.multi-layer-grid {
width: 100%;
height: 100%;
box-sizing: border-box;
margin: 0;
padding: 0;
}
.col-ruler {
display: flex;
height: 30px;
line-height: 30px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
width: 100%;
box-sizing: border-box;
overflow: hidden;
.ruler-empty {
width: 30px;
text-align: center;
font-weight: 600;
color: #606266;
border-right: 1px solid #e4e7ed;
flex-shrink: 0;
}
.ruler-item {
width: var(--cell-width);
text-align: center;
font-weight: 600;
color: #606266;
border-right: 1px solid #e4e7ed;
box-sizing: border-box;
flex-shrink: 0;
position: relative; // 为切换按钮定位做准备
// 列号文本
.column-number {
display: inline-block;
z-index: 1;
position: relative;
}
// 拆分/合并切换按钮
.split-merge-toggle {
position: absolute;
top: 0;
right: 0;
transform: translateY(0);
background: #fff;
border: 1px solid #e4e7ed;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 2px 6px;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 2px;
opacity: 0.7;
transition: all 0.2s ease;
color: #606266;
&:hover {
opacity: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
// 拆分状态样式
&.is-split {
background: #e8f5e9;
border-color: #2e7d32;
color: #2e7d32;
}
// 合并状态样式
&.is-merge {
background: #fff3e0;
border-color: #e65100;
color: #e65100;
}
.el-icon-s-tools {
font-size: 10px;
}
.toggle-text {
white-space: nowrap;
}
}
}
}
.row-grid-wrapper {
display: flex;
width: 100%;
height: calc(100% - 30px);
box-sizing: border-box;
overflow: hidden;
}
.row-ruler {
width: 30px;
background: #f5f7fa;
border-right: 1px solid #e4e7ed;
flex-shrink: 0;
overflow: hidden;
height: var(--total-height);
.ruler-item {
height: 80px;
line-height: 80px;
text-align: center;
font-weight: 600;
color: #606266;
border-bottom: 1px solid #e4e7ed;
box-sizing: border-box;
}
}
.grid-container {
display: grid;
width: 100%;
grid-template-columns: repeat(var(--column-count), minmax(0, var(--half-cell-width)));
grid-auto-rows: var(--total-height);
gap: 0px;
box-sizing: border-box;
margin: 0;
padding: 0;
height: var(--total-height);
overflow: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.column-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: 1px;
box-sizing: border-box;
border: 1px solid #e4e7ed;
border-radius: 4px;
&.layer-1-container {
background-color: #fff3e020;
height: 100%;
}
&.layer-2-container {
background-color: #e8f5e920;
height: 100%;
}
}
.warehouse-cell {
height: var(--item-height);
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
position: relative;
z-index: 1;
overflow: hidden;
margin: 1px 0;
transform-origin: top center;
&.layer-1:not(.disabled) {
background: #fff3e0;
color: #e65100;
}
&.layer-2:not(.disabled) {
background: #e8f5e9;
color: #2e7d32;
z-index: 1;
}
&.disabled {
background: #fafafa;
color: #909399;
cursor: not-allowed;
opacity: 0.8;
&:hover {
border-color: #e4e7ed;
background: #fafafa;
z-index: 1;
}
}
&:not(.disabled):hover {
border-color: #90caf9;
background: #f0f8ff;
z-index: 2;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.cell-name {
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 4px;
box-sizing: border-box;
.cell-line1 {
font-size: 12px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.cell-line2 {
font-size: 11px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.disabled & .cell-line1,
.disabled & .cell-line2 {
color: #909399;
}
.layer-1:not(.disabled) & .cell-line1,
.layer-1:not(.disabled) & .cell-line2 {
color: #e65100;
}
.layer-2:not(.disabled) & .cell-line1,
.layer-2:not(.disabled) & .cell-line2 {
color: #2e7d32;
}
}
}
</style>