前后端修改

This commit is contained in:
2025-11-25 01:13:26 +08:00
parent f0a1598fa3
commit 28d0852a81
18 changed files with 1574 additions and 440 deletions

View File

@@ -1,156 +1,189 @@
<template>
<div style="width: 100%; max-width: 300px;">
<treeselect
:max-height="200"
<div :class="['actual-warehouse-select', { 'is-block': block }]" :style="wrapperStyle">
<el-select
v-model="innerValue"
:options="warehouseOptions"
:normalizer="normalizer"
:placeholder="placeholder"
:clearable="clearable"
search-nested
:load-options="loadOptions"
@input="onInput"
/>
filterable
:filter-method="handleFilter"
:disabled="disabled"
size="small"
class="actual-warehouse-select__inner"
@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>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect';
import '@riophae/vue-treeselect/dist/vue-treeselect.css';
import { listActualWarehouse } from '@/api/wms/actualWarehouse';
import { LOAD_CHILDREN_OPTIONS } from '@riophae/vue-treeselect';
import { listActualWarehouseTree } from '@/api/wms/actualWarehouse';
const LEVEL_LABELS = {
1: '一级',
2: '二级',
3: '三级'
};
export default {
name: 'ActualWarehouseSelect',
components: { Treeselect },
props: {
value: {
type: [Number, String, null], // 仅保留单选类型
type: [Number, String, null],
default: null
},
placeholder: {
type: String,
default: '请选择库区/仓库/库位'
default: '请选择实际库区/库位'
},
clearable: {
type: Boolean,
default: true
},
showTop: {
block: {
type: Boolean,
default: true // 是否显示顶级节点actualWarehouseId=0
default: false
},
width: {
type: [String, Number],
default: 240
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
innerValue: this.value,
warehouseOptions: [], // 初始选项(含未加载子节点的节点,标记 children: null
loadedChildren: {}, // 缓存已加载的子节点key=父节点IDvalue=子节点数组
allLoadedNodes: [] // 存储所有已加载的节点,用于快速查找完整对象
flatOptions: [],
displayOptions: [],
levelLabels: LEVEL_LABELS
};
},
computed: {
wrapperStyle() {
const widthValue = this.block ? '100%' : (typeof this.width === 'number' ? `${this.width}px` : this.width);
return { width: widthValue };
}
},
watch: {
value(val) {
this.innerValue = val;
},
}
},
mounted() {
this.initOptions();
created() {
this.fetchTree();
},
methods: {
/** 初始化顶级选项 */
initOptions() {
// 重置状态
this.loadedChildren = {};
this.allLoadedNodes = [];
this.warehouseOptions = [];
if (this.showTop) {
// 显示顶级节点:标记 children: null 表示需要懒加载子节点
const topNode = {
actualWarehouseId: 0,
actualWarehouseName: '顶级节点',
children: null, // 关键:标记为未加载子节点
isDisabled: false
};
this.warehouseOptions.push(topNode);
this.allLoadedNodes.push(topNode);
} else {
// 不显示顶级节点:直接加载 parentId=0 的节点作为顶级(初始标记为未加载)
this.warehouseOptions.push({
actualWarehouseId: 'temp-parent-0', // 临时父节点ID仅用于触发首次加载
actualWarehouseName: '加载中...',
children: null,
isDisabled: true
});
// 触发首次加载 parentId=0 的节点
this.loadOptions({
action: LOAD_CHILDREN_OPTIONS,
parentNode: { id: 0 },
callback: (children) => {
this.warehouseOptions = children;
}
});
async fetchTree() {
try {
const res = await listActualWarehouseTree();
this.flatOptions = this.flattenTree(res.data || []);
this.displayOptions = [...this.flatOptions];
} catch (error) {
console.error('获取实际库区树失败', error);
}
},
/** 懒加载核心方法 */
loadOptions({ action, parentNode, callback, instanceId }) {
// 仅处理 "加载子节点" 动作
if (action !== LOAD_CHILDREN_OPTIONS) {
callback();
flattenTree(nodes, parentPath = [], level = 1) {
const result = [];
nodes.forEach(node => {
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;
}
console.log('加载子节点请求参数:', parentNode);
const parentId = parentNode.actualWarehouseId; // 当前父节点ID对应 actualWarehouseId
// 2. 调用接口加载子节点parentId 作为查询条件)
listActualWarehouse({ parentId }).then(response => {
const children = response.data.map(item => ({
...item,
isDisabled: !item.isEnabled, // 禁用未启用的节点
children: item.hasChildren ?? true ? null : [] // 有子节点则标记 children: null需懒加载否则空数组
}));
// 如果没有子节点了则不添加children属性
if (children.length === 0) {
delete parentNode.children;
callback()
return;
const matchedIds = new Set();
this.flatOptions.forEach(option => {
if (option.fullLabel.includes(text)) {
matchedIds.add(option.actualWarehouseId);
option.descendantIds.forEach(id => matchedIds.add(id));
}
// 5. 给父节点赋值子节点treeselect 自动渲染)
parentNode.children = children;
// 6. 回调通知加载成功
callback();
}).catch((error) => {
console.error('加载子节点失败:', error);
callback(new Error('加载失败,请重试'));
});
this.displayOptions = this.flatOptions.filter(option => matchedIds.has(option.actualWarehouseId));
},
/** 节点格式标准化 */
normalizer(node) {
return {
id: node.actualWarehouseId, // 节点唯一ID
label: node.actualWarehouseName, // 显示文本
children: node.children, // 子节点null=未加载,[]=无节点)
isDisabled: node.isDisabled, // 是否禁用
raw: node // 保留原始节点数据,方便后续查找
};
handleVisibleChange(visible) {
if (!visible) {
this.displayOptions = [...this.flatOptions];
}
},
/** 选中值变化时触发(仅单选) */
onInput(val) {
handleChange(val) {
this.$emit('input', val);
const node = this.flatOptions.find(option => option.actualWarehouseId === val);
this.$emit('select', node);
},
/** 外部刷新方法(如需重新加载数据可调用) */
refresh() {
this.initOptions();
this.fetchTree();
}
}
};
</script>
</script>
<style scoped>
.actual-warehouse-select {
display: inline-block;
}
.actual-warehouse-select.is-block {
width: 100%;
}
.actual-warehouse-select__inner {
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>