Files
klp-oa/klp-ui/src/views/wms/warehouse/overview.vue
砂糖 6f57ea2c3f feat(仓库管理): 优化仓库网格布局并增强库位编码解析
- 在WarehouseGrid组件中添加layerData变化的调试日志
- 将overview.vue中的网格列数从10增加到30
- 重构WarehouseBird组件中的库位编码解析逻辑,支持更灵活的编码格式
- 优化WarehouseInterlaced组件的布局,添加横向滚动容器并改进单元格宽度计算
- 同步列标尺和网格区域的滚动行为
2026-01-07 10:18:26 +08:00

523 lines
17 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="app-container">
<!-- 整体布局 -->
<div class="layout-container">
<!-- 左侧树形结构 -->
<div class="tree-container" v-loading="treeLoading">
<el-tree ref="warehouseTreeRef" :data="warehouseTree" :props="treeProps" node-key="actualWarehouseId"
@node-click="handleNodeClick" :expand-on-click-node="false" highlight-current class="warehouse-tree"
:disabled="isSwitching">
</el-tree>
</div>
<!-- 右侧仓库信息区域 - 替换为 Bird 组件 -->
<div class="warehouse-container" v-if="selectedNodeId" v-loading="rightLoading" element-loading-text="加载中..."
element-loading-spinner="el-icon-loading">
<!-- 导出所有二维码 -->
<!-- <button buttonLoading type="primary" @click="exportAllQrcodes">导出二维码</button> -->
<WarehouseBird
:id="selectedNodeId"
:warehouse-list="warehouseList"
@open-init-dialog="openInitDialog"
@split-warehouse="handleSplitWarehouse"
@merge-warehouse="handleMergeWarehouse"
/>
</div>
<!-- 未选中节点提示 -->
<div class="empty-select-tip" v-if="!selectedNodeId">
请选择左侧仓库分类查看库位信息
</div>
</div>
<!-- 库位初始化弹窗 -->
<el-dialog title="库位初始化" :visible.sync="initDialogVisible" width="700px" destroy-on-close append-to-body center>
<el-form ref="initFormRef" :model="initForm" :rules="initFormRules" label-width="100px" size="mini">
<!-- 可视化网格选择区域 -->
<el-form-item label="行列选择" prop="gridSelect">
<div class="grid-selector-container">
<div class="selector-tip">
拖动/点击选择网格范围当前{{ initForm.rowCount || 0 }} × {{ initForm.columnCount || 0 }}
</div>
<div class="grid-selector" @mousemove="handleGridHover" @click="confirmGridSelect"
@mouseleave="resetGridHover">
<div v-for="row in 40" :key="`grid-row-${row}`" class="grid-selector-row">
<div v-for="col in 30" :key="`grid-col-${col}`" class="grid-selector-cell" :class="{
hovered: row <= hoverRow && col <= hoverCol,
selected: row <= initForm.rowCount && col <= initForm.columnCount
}">
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="编码前缀" prop="prefix">
<el-input v-model="initForm.prefix" disabled placeholder="系统自动生成" />
</el-form-item>
</el-form>
<template slot="footer">
<el-button @click="initDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitInitForm">确认初始化</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { listActualWarehouse, treeActualWarehouseTwoLevel, getActualWarehouse, generateLocations, splitActualWarehouse, mergeActualWarehouse } from "@/api/wms/actualWarehouse";
import WarehouseBird from './components/WarehouseBird.vue';
import jsPDF from 'jspdf';
import QRCode from 'qrcode';
export default {
name: "Overview",
components: { WarehouseBird },
data() {
// 自定义验证规则:正整数
const positiveIntegerValidator = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入正整数'));
} else if (!/^[1-9]\d*$/.test(value)) {
callback(new Error('请输入大于0的正整数'));
} else {
callback();
}
};
// 网格选择验证规则
const gridSelectValidator = (rule, value, callback) => {
if (!this.initForm.rowCount || !this.initForm.columnCount) {
callback(new Error('请先选择网格行列数'));
} else {
callback();
}
};
return {
warehouseTree: [],
treeProps: { label: "actualWarehouseName", children: "children" },
selectedNodeId: "",
selectedNode: null,
warehouseList: [], // 透传给 Bird 组件的原始数据
// 初始化弹窗相关
initDialogVisible: false,
initForm: {
rowCount: '', columnCount: '', layerCount: '', prefix: '', parentId: ''
},
hoverRow: 0,
hoverCol: 0,
initFormRules: {
gridSelect: [{ validator: gridSelectValidator, trigger: 'change' }],
layerCount: [{ validator: positiveIntegerValidator, trigger: 'blur' }],
parentId: [{ required: true, message: '父节点ID不能为空', trigger: 'blur' }]
},
// 加载状态
treeLoading: false,
rightLoading: false,
isSwitching: false,
nodeClickTimer: null,
buttonLoading: false,
};
},
created() {
this.getWarehouseTree();
},
beforeDestroy() {
if (this.nodeClickTimer) clearTimeout(this.nodeClickTimer);
},
methods: {
/**
* 处理分割库位事件
*/
handleSplitWarehouse(payload) {
// this.$message.success(`成功分割库位:${warehouse.actualWarehouseCode}`);
console.log(payload)
this.rightLoading = true;
const loadingInstance = this.$loading({
lock: true,
text: '正在转化为小卷库位,这个操作需要一段时间,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
splitActualWarehouse(payload).then(res => {
this.$message.success(`成功转化为小卷库位`);
this.getWarehouseList(this.selectedNodeId)
}).catch(err => {
this.$message.error(`转化为小卷库位失败:${err.message}`);
}).finally(() => {
this.rightLoading = false;
loadingInstance.close();
})
},
/**
* 处理合并库位事件
*/
handleMergeWarehouse(payload) {
this.rightLoading = true;
const loadingInstance = this.$loading({
lock: true,
text: '正在转化为大卷库位,这个操作需要一段时间,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
mergeActualWarehouse(payload).then(res => {
this.$message.success(`成功转化为大卷库位`);
this.getWarehouseList(this.selectedNodeId)
}).catch(err => {
this.$message.error(`转化为大卷库位失败:${err.message}`);
}).finally(() => {
this.rightLoading = false;
loadingInstance.close();
})
},
// 获取树形数据
getWarehouseTree() {
this.treeLoading = true;
treeActualWarehouseTwoLevel()
.then((res) => { this.warehouseTree = res.data || []; })
.catch((err) => { this.$message.error("获取仓库树形数据失败:" + err.message); })
.finally(() => { this.treeLoading = false; });
},
async exportAllQrcodes() {
this.buttonLoading = true;
if (!this.warehouseList || this.warehouseList.length === 0) {
this.$message.warning('暂无库位数据可导出');
return;
}
const loadingInstance = this.$loading({
lock: true,
text: '正在生成二维码PDF请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
try {
// 初始化PDF (A4纸尺寸: 210mm × 297mm换算成px约595 × 842)
const pdf = new jsPDF({
orientation: 'portrait', // 纵向
unit: 'mm', // 单位:毫米
format: 'a4' // A4格式
});
// 配置项
const qrCodeSize = 30; // 二维码尺寸(mm)
const margin = 10; // 页面边距(mm)
const gap = 10; // 二维码间距(mm)
const textOffset = 5; // 文字与二维码间距(mm)
const maxPerRow = Math.floor((210 - 2 * margin) / (qrCodeSize + gap)); // 每行最多二维码数
const maxPerPage = Math.floor((297 - 2 * margin) / (qrCodeSize + gap + textOffset + 5)) * maxPerRow; // 每页最多二维码数
let currentX = margin; // 当前X坐标
let currentY = margin; // 当前Y坐标
let currentIndex = 0; // 当前处理的库位索引
const list = this.warehouseList.filter(item => item.actualWarehouseCode.includes('X'));
const totalCount = list.length;
// 遍历所有库位生成二维码并添加到PDF
for (const [index, item] of list.entries()) {
currentIndex = index;
// 跳过无ID的项
if (!item.actualWarehouseId) continue;
// 生成二维码 (返回base64格式)
const qrCodeDataUrl = await QRCode.toDataURL(item.actualWarehouseId, {
width: qrCodeSize * 3.78, // 转换mm到px (1mm ≈ 3.78px)
margin: 1,
errorCorrectionLevel: 'H' // 高容错率
});
// 检查是否需要换行
if (currentX + qrCodeSize > 210 - margin) {
currentX = margin;
currentY += qrCodeSize + gap + textOffset + 5;
}
// 检查是否需要分页
if (currentY + qrCodeSize > 297 - margin || (index > 0 && index % maxPerPage === 0)) {
pdf.addPage();
currentX = margin;
currentY = margin;
}
// 添加二维码到PDF
pdf.addImage(qrCodeDataUrl, 'PNG', currentX, currentY, qrCodeSize, qrCodeSize);
// 添加库位名称(自动换行处理)
const name = item.actualWarehouseName || '未知库位';
const fontSize = 8; // 字体大小
pdf.setFontSize(fontSize);
// 计算文字宽度,超出则截断
const textWidth = pdf.getTextWidth(name);
let displayName = name;
if (textWidth > qrCodeSize) {
const maxTextWidth = qrCodeSize;
let tempText = '';
for (let i = 0; i < name.length; i++) {
if (pdf.getTextWidth(tempText + name[i]) > maxTextWidth) {
displayName = tempText + '...';
break;
}
tempText += name[i];
}
}
// 文字居中显示在二维码下方
const textX = currentX + (qrCodeSize - pdf.getTextWidth(displayName)) / 2;
pdf.text(displayName, textX, currentY + qrCodeSize + textOffset);
// 更新X坐标
currentX += qrCodeSize + gap;
}
// 保存PDF
const fileName = `库位二维码_${this.selectedNode.actualWarehouseName}.pdf`;
pdf.save(fileName);
this.$message.success(`成功导出 ${totalCount} 个库位二维码`);
} catch (error) {
console.error('二维码导出失败:', error);
this.$message.error(`二维码导出失败:${error.message}`);
} finally {
this.buttonLoading = false;
loadingInstance.close();
}
},
// 树节点点击
handleNodeClick(node) {
if (this.isSwitching) return;
if (this.nodeClickTimer) clearTimeout(this.nodeClickTimer);
this.nodeClickTimer = setTimeout(() => {
if (!node.children || node.children.length === 0) {
this.isSwitching = true;
this.rightLoading = true;
this.selectedNodeId = node.actualWarehouseId;
this.selectedNode = node;
this.getWarehouseList(node.actualWarehouseId)
.finally(() => {
this.rightLoading = false;
this.isSwitching = false;
});
} else {
this.selectedNodeId = "";
this.selectedNode = null;
this.warehouseList = [];
}
}, 300);
},
// 获取库位列表
getWarehouseList(parentId) {
return listActualWarehouse({ parentId })
.then((res) => { this.warehouseList = res.data || []; })
.catch((err) => {
this.$message.error("获取库位数据失败:" + err.message);
this.warehouseList = [];
});
},
// 打开初始化弹窗
async openInitDialog() {
if (!this.selectedNode) {
this.$message.warning('请先选择左侧仓库分类');
return;
}
try {
await this.$confirm(
'初始化库位将批量生成指定数量的库位数据,此操作不可撤销!请确认是否继续?',
'重要提示',
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
);
} catch { return; }
this.$nextTick(() => {
this.$refs.initFormRef?.resetFields();
this.initForm.rowCount = '';
this.initForm.columnCount = '';
this.hoverRow = 0;
this.hoverCol = 0;
});
const prefix = await this.generateWarehousePrefix(this.selectedNode);
this.initForm = {
rowCount: '', columnCount: '',
prefix: prefix,
parentId: this.selectedNode.actualWarehouseId
};
this.initDialogVisible = true;
},
// 生成编码前缀
async generateWarehousePrefix(node) {
try {
const parentRes = await getActualWarehouse(node.parentId);
const parentCode = parentRes.data.actualWarehouseCode || '';
const nodeCode = node.actualWarehouseCode || '';
return (parentCode + nodeCode).toUpperCase();
} catch (err) {
this.$message.error('生成编码前缀失败,将使用默认前缀');
return 'DEFAULT';
}
},
// 网格选择悬浮
handleGridHover(e) {
const cell = e.target.closest('.grid-selector-cell');
if (!cell) return;
const row = Array.from(cell.parentElement.parentElement.children).indexOf(cell.parentElement) + 1;
const col = Array.from(cell.parentElement.children).indexOf(cell) + 1;
this.hoverRow = Math.min(row, 99);
this.hoverCol = Math.min(col, 99);
},
// 确认网格选择
confirmGridSelect() {
if (this.hoverRow < 1 || this.hoverCol < 1) return;
this.initForm.rowCount = this.hoverRow;
this.initForm.columnCount = this.hoverCol;
this.$refs.initFormRef.validateField('gridSelect');
},
// 重置网格悬浮
resetGridHover() {
this.hoverRow = this.initForm.rowCount || 0;
this.hoverCol = this.initForm.columnCount || 0;
},
// 提交初始化表单
submitInitForm() {
this.$refs.initFormRef.validate(async (valid) => {
if (valid) {
const params = {
rowCount: this.initForm.rowCount,
columnCount: this.initForm.columnCount,
layerCount: this.initForm.layerCount,
prefix: this.initForm.prefix,
parentId: this.initForm.parentId
};
const loadingInstance = this.$loading({
lock: true, text: '正在初始化库位,请稍候...',
spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)'
});
try {
await generateLocations(params);
this.$message.success('库位初始化成功!');
this.initDialogVisible = false;
await this.getWarehouseList(this.selectedNodeId);
} catch (err) {
this.$message.error('库位初始化失败:' + err.message);
} finally {
loadingInstance.close();
}
} else {
this.$message.warning('请完善表单必填信息!');
return false;
}
});
}
},
};
</script>
<style scoped lang="scss">
.app-container {
width: 100%;
height: calc(100vh - 90px);
padding: 16px;
box-sizing: border-box;
background: #f5f7fa;
}
.layout-container {
display: flex;
width: 100%;
height: 100%;
gap: 16px;
.tree-container {
width: 160px;
height: 100%;
background: #fff;
border-radius: 8px;
padding: 16px;
box-sizing: border-box;
overflow-y: auto;
}
.warehouse-container {
flex: 1;
height: 100%;
overflow-y: auto;
}
.empty-select-tip {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: #909399;
font-size: 16px;
background: #fff;
border-radius: 8px;
}
}
// 初始化网格选择器样式
.grid-selector-container {
width: 100%;
padding: 10px;
box-sizing: border-box;
.selector-tip {
margin-bottom: 10px;
font-size: 12px;
color: #606266;
}
.grid-selector {
width: 100%;
max-width: 500px;
max-height: 300px;
overflow: auto;
border: 1px solid #e6e6e6;
background: #fafafa;
.grid-selector-row {
display: flex;
.grid-selector-cell {
width: 20px;
height: 20px;
border: 1px solid #e0e0e0;
box-sizing: border-box;
transition: all 0.1s;
&.hovered {
background: #e8f4ff;
border-color: #409eff;
}
&.selected {
background: #409eff;
border-color: #1e88e5;
}
&:hover {
background: #c6e2ff;
border-color: #409eff;
}
}
}
}
}
</style>