Files
klp-oa/klp-ui/src/views/wms/warehouse/components/WarehouseInterlaced.vue
砂糖 0ced7e5a9f feat(api): 为L2 API添加HTTP协议前缀
refactor(warehouse): 重构仓库管理页面为卡片布局并优化交互
- 移除树形表格改用卡片布局
- 添加启用/禁用快捷开关
- 优化备注显示样式
- 简化表单逻辑

feat(utils): 新增WebSocket管理器类
- 支持多连接管理
- 自动重连机制
- 状态监控功能

style(views): 清理注释掉的代码和未使用的组件
2025-12-22 10:18:58 +08:00

583 lines
18 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">
<!-- 列标尺区域 - 添加切换按钮 -->
<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"
: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: () => ({})
}
},
data() {
return {
dialogOpen: false,
currentWarehouse: null,
containerWidth: 0,
resizeTimer: null,
currentContextWarehouse: null,
rulerRowHeight: 80,
// rulerMaxRow: 0,
isMounted: false
};
},
computed: {
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) {
const currentLevel = this.getColumnLevel(column);
// 切换level3↔4
const newLevel = currentLevel === 3 ? 4 : 3;
// 向父组件发送切换事件由父组件更新columns数据
// 如果切换为合并状态调用handleMergeWarehouse方法
// 需要二次确认
console.log(this.columns[column])
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 maxSplitCount = 5; // 最多拆分5个
for (let i = 0; i < maxSplitCount; i++) {
if (layer1Warehouses[i]) splitIds.push(layer1Warehouses[i].actualWarehouseId);
if (layer2Warehouses[i]) splitIds.push(layer2Warehouses[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>