Files
klp-oa/klp-ui/src/views/wms/warehouse/components/WarehouseInterlaced.vue
砂糖 e1fbb7805f feat(CoilSelector): 添加可拖拽库位视图和品质列显示
refactor(WarehouseInterlaced): 修复props格式问题
fix(DuGeTag): 修正电话号码格式
2026-03-11 10:47:49 +08:00

743 lines
24 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>
<!-- 1. 新增横向滚动容器包裹列标尺和网格区域 -->
<div class="scroll-wrapper" ref="scrollWrapper">
<!-- 列标尺区域 - 添加切换按钮 -->
<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="canToggle && 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, error: warehouse.isEnabled === 0 && warehouse.currentCoilNo == null }" @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, error: warehouse.isEnabled === 0 && warehouse.currentCoilNo == null }" :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>
</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-button v-if="canRelease && currentWarehouse.isEnabled === 0" @click="handleReleaseWarehouse(currentWarehouse)">释放库位</el-button>
</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: ''
},
canToggle: {
default: true,
type: Boolean
},
canRelease: {
default: false,
type: Boolean
},
},
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() {
// 2. 优化cellWidth计算最小宽度从120调整为80避免列数过多时宽度过大导致溢出
if (!this.containerWidth || this.sortedColumnKeys.length === 0) return 160;
// 如果列数超过10列则宽度固定为160px
if (this.sortedColumnKeys.length > 10) return 160;
const availableWidth = Math.max(0, this.containerWidth - 30); // 30是行标尺宽度
const calcWidth = availableWidth / this.sortedColumnKeys.length;
// 最小宽度80px最大不限制交给滚动容器处理
return calcWidth;
},
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) {
console.log('columns 变化:', newVal);
if (this.isMounted) {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.calcContainerWidth();
}, 50);
}
}
}
},
mounted() {
this.isMounted = true;
this.calcContainerWidth();
window.addEventListener('resize', this.handleResize);
// 3. 绑定滚动同步事件:列标尺和网格区域同步滚动
const scrollWrapper = this.$refs.scrollWrapper;
if (scrollWrapper) {
scrollWrapper.addEventListener('scroll', this.syncScroll);
}
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
clearTimeout(this.resizeTimer);
this.isMounted = false;
// 移除滚动监听
const scrollWrapper = this.$refs.scrollWrapper;
if (scrollWrapper) {
scrollWrapper.removeEventListener('scroll', this.syncScroll);
}
},
methods: {
handleReleaseWarehouse(warehouse) {
if (!this.canRelease) {
this.$message.error('当前库位不支持释放操作');
return;
}
this.$modal.confirm(`确认释放库位 ${warehouse.actualWarehouseCode}` ,{
title: '确认释放库位',
message: `确认释放库位 ${warehouse.actualWarehouseCode}`,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 向父组件发送释放事件由父组件更新columns数据
this.dialogOpen = false;
this.$emit('release-warehouse', warehouse);
}).catch(() => {
// 用户取消操作
});
},
// 4. 同步滚动方法(确保列标尺和网格滚动位置一致)
syncScroll(e) {
const target = e.target;
// 确保滚动行为同步此处主要是统一滚动容器的滚动无需额外处理因为都在同一个scroll-wrapper里
this.$nextTick(() => {
const colRuler = target.querySelector('.col-ruler');
if (colRuler) {
colRuler.scrollLeft = target.scrollLeft;
}
});
},
// 获取指定列的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;
// 主容器隐藏横向溢出滚动交给内部scroll-wrapper
overflow: hidden;
}
// 5. 新增滚动容器样式:统一管理列标尺和网格的横向滚动
.scroll-wrapper {
width: 100%;
height: calc(100% - 42px); // 42是alert提示框高度可根据实际调整
overflow-x: auto; // 横向滚动,纵向隐藏
overflow-y: hidden;
scrollbar-width: thin; // 优化滚动条样式Firefox
scrollbar-color: #ccc #f5f7fa;
// 优化Chrome滚动条样式
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-track {
background: #f5f7fa;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
&:hover {
background: #999;
}
}
}
.col-ruler {
display: flex;
height: 30px;
line-height: 30px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
width: fit-content; // 6. 列标尺宽度自适应内容而非100%
box-sizing: border-box;
// 移除overflow: hidden让滚动容器控制溢出
// 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: fit-content; // 7. 网格容器宽度自适应内容
height: calc(100% - 30px);
box-sizing: border-box;
// 移除overflow: hidden交给scroll-wrapper控制
// 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: flex;
width: fit-content; // 8. 网格宽度自适应
// 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交给scroll-wrapper控制
// overflow: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.column-container {
display: flex;
width: var(--half-cell-width);
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;
min-width: var(--half-cell-width);
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;
}
}
&.error {
background: #ffcdd2;
color: #f44336;
cursor: not-allowed;
opacity: 0.8;
}
&: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>