feat(仓库管理): 实现两级懒加载树结构并重构库位选择组件

- 在 actualWarehouse.js 中新增两级树结构 API
- 重构 real.vue 使用懒加载方式展示仓库树
- 将 ActualWarehouseSelect 组件从 el-select 改为 el-cascader 实现级联选择
- 优化树形表格的展开逻辑和加载性能
This commit is contained in:
砂糖
2025-11-25 10:58:32 +08:00
parent 5d4eac555a
commit 67dbb34c28
3 changed files with 180 additions and 117 deletions

View File

@@ -70,3 +70,12 @@ export function delActualWarehouse(actualWarehouseId) {
method: 'delete' method: 'delete'
}) })
} }
// 获取两级的树结构
export function treeActualWarehouseTwoLevel(query) {
return request({
url: '/wms/actualWarehouse/levelTwo',
method: 'get',
params: query
})
}

View File

@@ -1,56 +1,46 @@
<template> <template>
<div :class="['actual-warehouse-select', { 'is-block': block }]" :style="wrapperStyle"> <div
<el-select :class="['actual-warehouse-select', { 'is-block': block }]"
v-model="innerValue" :style="wrapperStyle"
>
<el-cascader
ref="cascader"
v-model="innerPath"
:props="cascaderProps"
:placeholder="placeholder" :placeholder="placeholder"
:clearable="clearable" :clearable="clearable"
filterable :show-all-levels="true"
:filter-method="handleFilter" :emit-path="true"
:disabled="disabled" style="width: 100%;"
size="small"
class="actual-warehouse-select__inner"
@change="handleChange" @change="handleChange"
@visible-change="handleVisibleChange" />
>
<el-option
v-for="option in displayOptions"
:key="option.actualWarehouseId"
:label="option.fullLabel"
:value="option.actualWarehouseId"
>
<div class="option-content">
<span class="level-tag">{{ levelLabels[option.level] }}</span>
<span class="option-text">{{ option.fullLabel }}</span>
</div>
</el-option>
</el-select>
</div> </div>
</template> </template>
<script> <script>
import { listActualWarehouseTree } from '@/api/wms/actualWarehouse'; import { listActualWarehouse } from '@/api/wms/actualWarehouse';
const LEVEL_LABELS = {
1: '一级',
2: '二级',
3: '三级'
};
export default { export default {
name: 'ActualWarehouseSelect', name: 'ActualWarehouseSelect',
props: { props: {
// 对外仍然是「单个 ID」即第三级 actualWarehouseId
value: { value: {
type: [Number, String, null], type: [Number, String, null],
default: null default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
default: '请选择实际库区/库位' default: '请选择实际库位'
}, },
clearable: { clearable: {
type: Boolean, type: Boolean,
default: true default: true
}, },
// 已经不再显示“最高级”
showTop: {
type: Boolean,
default: false
},
block: { block: {
type: Boolean, type: Boolean,
default: false default: false
@@ -59,99 +49,147 @@ export default {
type: [String, Number], type: [String, Number],
default: 240 default: 240
}, },
disabled: { size: {
type: Boolean, type: String,
default: false default: 'small'
} }
}, },
data() { data() {
return { return {
innerValue: this.value, // 级联组件内部使用的「路径值」,例如 [一级ID, 二级ID, 三级ID]
flatOptions: [], innerPath: [],
displayOptions: [], // 记录所有已加载节点(如果后面要根据 ID 反查路径,可复用)
levelLabels: LEVEL_LABELS allLoadedNodes: []
}; };
}, },
computed: { computed: {
wrapperStyle() { wrapperStyle() {
const widthValue = this.block ? '100%' : (typeof this.width === 'number' ? `${this.width}px` : this.width); const widthValue = this.block
? '100%'
: (typeof this.width === 'number' ? `${this.width}px` : this.width);
return { width: widthValue }; return { width: widthValue };
},
// el-cascader 的 props 配置
cascaderProps() {
return {
lazy: true,
lazyLoad: this.loadNode, // 懒加载方法
checkStrictly: false, // 只允许选叶子节点
value: 'value',
label: 'label',
children: 'children'
};
} }
}, },
watch: { watch: {
// 外部把 value 置空时,同步清空面板
value(val) { value(val) {
this.innerValue = val; if (val == null || val === '') {
this.innerPath = [];
}
} }
}, },
created() {
this.fetchTree();
},
methods: { methods: {
async fetchTree() { /**
try { * 级联懒加载
const res = await listActualWarehouseTree(); * node.level:
this.flatOptions = this.flattenTree(res.data || []); * 0还没选任何东西parentId = 0 -> 加载一级)
this.displayOptions = [...this.flatOptions]; * 1一级节点加载二级
} catch (error) { * 2二级节点加载三级三级设为叶子不再展开
console.error('获取实际库区树失败', error); */
} loadNode(node, resolve) {
}, const { level, value } = node;
flattenTree(nodes, parentPath = [], level = 1) { // 超过第三级就不再加载
const result = []; if (level >= 3) {
nodes.forEach(node => { resolve([]);
const path = [...parentPath, node.actualWarehouseName];
const current = {
actualWarehouseId: node.actualWarehouseId,
parentId: node.parentId || 0,
level,
fullLabel: path.join(' / '),
descendantIds: [],
children: node.children || []
};
result.push(current);
if (node.children && node.children.length) {
const children = this.flattenTree(node.children, path, level + 1);
children.forEach(child => {
current.descendantIds.push(child.actualWarehouseId);
current.descendantIds.push(...child.descendantIds);
});
result.push(...children);
}
});
return result;
},
handleFilter(keyword) {
const text = (keyword || '').trim();
if (!text) {
this.displayOptions = [...this.flatOptions];
return; return;
} }
const matchedIds = new Set();
this.flatOptions.forEach(option => { const parentId = level === 0 ? 0 : value;
if (option.fullLabel.includes(text)) {
matchedIds.add(option.actualWarehouseId); listActualWarehouse({ parentId }).then(res => {
option.descendantIds.forEach(id => matchedIds.add(id)); const list = (res.data || []).map(item => {
} const nextLevel = level + 1;
const isLeafLevel = nextLevel >= 3; // 第三级为叶子,不能再展开
const nodeData = {
value: item.actualWarehouseId,
label: item.actualWarehouseName,
leaf: isLeafLevel,
// 只有第三级可选;一、二级全部 disabled
disabled: nextLevel == 3 && !item.isEnabled,
// 保留原始数据和层级
raw: item,
level: nextLevel
};
this.registerNode(nodeData);
return nodeData;
});
resolve(list);
}).catch(err => {
console.error('加载仓库树失败:', err);
resolve([]);
}); });
this.displayOptions = this.flatOptions.filter(option => matchedIds.has(option.actualWarehouseId));
}, },
handleVisibleChange(visible) { /** 把节点放进缓存数组里,后面如需扩展可用 */
if (!visible) { registerNode(node) {
this.displayOptions = [...this.flatOptions]; const id = node.value;
const idx = this.allLoadedNodes.findIndex(
n => String(n.value) === String(id)
);
if (idx === -1) {
this.allLoadedNodes.push(node);
} else {
this.$set(this.allLoadedNodes, idx, {
...this.allLoadedNodes[idx],
...node
});
} }
}, },
handleChange(val) { /**
this.$emit('input', val); * 选中变化:
const node = this.flatOptions.find(option => option.actualWarehouseId === val); * value 是路径数组,如 [一级ID, 二级ID, 三级ID]
this.$emit('select', node); * 我们对外只抛最后一个第三级ID
*/
handleChange(value) {
if (!Array.isArray(value) || value.length === 0) {
this.$emit('input', null);
this.$emit('select', null);
return;
}
const leafId = value[value.length - 1];
// 拿到当前选中节点,获取完整路径信息
const nodes = this.$refs.cascader.getCheckedNodes();
let payload = null;
if (nodes && nodes.length > 0) {
const node = nodes[0];
const pathNodes = node.path || [];
payload = {
id: leafId,
pathIds: pathNodes.map(n => n.value),
pathLabels: pathNodes.map(n => n.label),
// 原始 data如果需要后台做别的处理可以用
rawPath: pathNodes.map(n => n.data || n.raw || {})
};
}
this.$emit('input', leafId);
this.$emit('select', payload);
}, },
/** 外部刷新:如果以后需要强制重载,可以扩展这里(目前不需要缓存) */
refresh() { refresh() {
this.fetchTree(); this.innerPath = [];
this.allLoadedNodes = [];
// el-cascader 的懒加载不需要预载,这里清空即可
} }
} }
}; };
@@ -160,30 +198,15 @@ export default {
<style scoped> <style scoped>
.actual-warehouse-select { .actual-warehouse-select {
display: inline-block; display: inline-block;
width: 100%;
} }
.actual-warehouse-select.is-block { .actual-warehouse-select.is-block {
width: 100%; width: 100%;
} }
.actual-warehouse-select__inner { /* 让级联宽度占满容器,其他高度、边框等跟 el-select 共用全局 .el-input__inner 样式 */
.actual-warehouse-select .el-cascader {
width: 100%; width: 100%;
} }
.option-content {
display: flex;
align-items: center;
gap: 8px;
}
.level-tag {
font-size: 12px;
color: #909399;
width: 34px;
}
.option-text {
font-size: 13px;
color: #606266;
}
</style> </style>

View File

@@ -65,8 +65,11 @@
:data="filteredTreeData" :data="filteredTreeData"
row-key="actualWarehouseId" row-key="actualWarehouseId"
:default-expand-all="isExpandAll" :default-expand-all="isExpandAll"
:expand-row-keys="expandRowKeys"
class="warehouse-table" class="warehouse-table"
:tree-props="{ children: 'children' }" :tree-props="{ children: 'children' }"
lazy
:load="handleLoad"
> >
<el-table-column label="层级" align="center"> <el-table-column label="层级" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -331,10 +334,12 @@ import {
addActualWarehouse, addActualWarehouse,
updateActualWarehouse, updateActualWarehouse,
createActualWarehouseHierarchy, createActualWarehouseHierarchy,
importActualWarehouse importActualWarehouse,
treeActualWarehouseTwoLevel
} from "@/api/wms/actualWarehouse"; } from "@/api/wms/actualWarehouse";
import QRCode from "../print/components/QRCode.vue"; import QRCode from "../print/components/QRCode.vue";
import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect"; import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect";
import { listActualWarehouse } from "../../../api/wms/actualWarehouse";
const LEVELS = [1, 2, 3]; const LEVELS = [1, 2, 3];
const LEVEL_LABELS = { const LEVEL_LABELS = {
@@ -350,7 +355,7 @@ export default {
return { return {
showSearch: true, showSearch: true,
loading: false, loading: false,
isExpandAll: true, isExpandAll: false,
warehouseTree: [], warehouseTree: [],
filteredCache: [], filteredCache: [],
nodeIndex: {}, nodeIndex: {},
@@ -416,6 +421,9 @@ export default {
return this.warehouseTree; return this.warehouseTree;
} }
return this.filterTree(this.warehouseTree, keyword, levelFilter); return this.filterTree(this.warehouseTree, keyword, levelFilter);
},
expandRowKeys() {
return this.warehouseTree.map(item => item.actualWarehouseId);
} }
}, },
created() { created() {
@@ -442,9 +450,17 @@ export default {
}, },
getTreeData() { getTreeData() {
this.loading = true; this.loading = true;
listActualWarehouseTree({ actualWarehouseName: this.queryParams.keyword }) treeActualWarehouseTwoLevel({ actualWarehouseName: this.queryParams.keyword })
.then(res => { .then(res => {
const list = res.data || []; const list = res.data.map(item => {
// 便利item的children中的每一项
item.children = item.children || [];
item.children.forEach(child => {
delete child.children;
child.hasChildren = true;
});
return item;
}) || [];
this.decorateTree(list); this.decorateTree(list);
}) })
.finally(() => { .finally(() => {
@@ -500,6 +516,21 @@ export default {
this.toggleTreeRows(this.filteredTreeData, this.isExpandAll); this.toggleTreeRows(this.filteredTreeData, this.isExpandAll);
}); });
}, },
handleLoad(treeData, treeNode, resolve) {
console.log(treeData, treeNode);
listActualWarehouse({ parentId: treeData.actualWarehouseId })
.then(res => {
const list = res.data.map(item => {
item.hasChildren = false;
item.level = treeData.level + 1;
return item;
}) || [];
resolve(list);
})
.catch(() => {
resolve([]);
});
},
toggleTreeRows(nodes, expand) { toggleTreeRows(nodes, expand) {
const table = this.$refs.treeTable; const table = this.$refs.treeTable;
if (!table || !Array.isArray(nodes)) return; if (!table || !Array.isArray(nodes)) return;