215 lines
5.5 KiB
Vue
215 lines
5.5 KiB
Vue
<script setup>
|
|
import { computed, ref, useAttrs, watch } from "vue";
|
|
import { useAppStore } from "@/stores/app";
|
|
|
|
defineOptions({ inheritAttrs: false });
|
|
|
|
const attrs = useAttrs();
|
|
const appStore = useAppStore();
|
|
const handledEventAttrs = new Set([
|
|
"onDrop",
|
|
"onDragStart",
|
|
"onDragstart",
|
|
"onDragEnter",
|
|
"onDragenter",
|
|
"onDragLeave",
|
|
"onDragleave",
|
|
"onDragOver",
|
|
"onDragover",
|
|
"onDragEnd",
|
|
"onDragend",
|
|
"onRightClick",
|
|
"onSelect",
|
|
"onUpdate:expandedKeys",
|
|
]);
|
|
|
|
const treeAttrs = computed(() => {
|
|
const result = {};
|
|
for (const key in attrs) {
|
|
if (!handledEventAttrs.has(key)) result[key] = attrs[key];
|
|
}
|
|
return result;
|
|
});
|
|
|
|
const props = defineProps({
|
|
treeData: { type: Array, default: () => [] },
|
|
expandedKeys: { type: Array, default: undefined },
|
|
blockNode: { type: Boolean, default: true },
|
|
virtual: { type: Boolean, default: true },
|
|
showLine: { type: Boolean, default: false },
|
|
draggable: { type: [Boolean, Function, Object], default: false },
|
|
allowDrop: { type: Function, default: null },
|
|
rootKey: { type: [String, Number], default: "__fms_tree_root__" },
|
|
// 右键菜单生成函数:(node) => { items: [...] }
|
|
contextMenu: { type: Function, default: null },
|
|
contextMenuDisabled: { type: Boolean, default: false },
|
|
});
|
|
|
|
// 虚拟滚动与拖拽冲突:draggable 开启时强制关闭 virtual
|
|
const effectiveVirtual = computed(() => {
|
|
if (props.draggable) return false;
|
|
return props.virtual;
|
|
});
|
|
|
|
function getNodeTitle(node) {
|
|
const fallback = node?.title || node?.b_name || node?.b_code || "";
|
|
return appStore.t(node?.b_i18n, fallback);
|
|
}
|
|
|
|
function localizeNodes(nodes) {
|
|
const result = [];
|
|
for (const node of nodes || []) {
|
|
result.push({
|
|
...node,
|
|
title: getNodeTitle(node),
|
|
children: localizeNodes(node.children),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const effectiveTreeData = computed(() => {
|
|
return [
|
|
{
|
|
key: props.rootKey,
|
|
title: appStore.t("common.allCategories", "全部分类"),
|
|
isFmsRoot: true,
|
|
children: localizeNodes(props.treeData),
|
|
},
|
|
];
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
"menuClick",
|
|
"drop",
|
|
"dragStart",
|
|
"dragEnter",
|
|
"dragLeave",
|
|
"dragOver",
|
|
"dragEnd",
|
|
"rightClick",
|
|
"select",
|
|
"update:expandedKeys",
|
|
]);
|
|
|
|
let rootExpansionInitialized = false;
|
|
watch(
|
|
() => props.expandedKeys,
|
|
(keys) => {
|
|
if (rootExpansionInitialized || !Array.isArray(keys)) return;
|
|
rootExpansionInitialized = true;
|
|
const hasRoot = keys.some((key) => String(key) === String(props.rootKey));
|
|
if (!hasRoot) emit("update:expandedKeys", [props.rootKey, ...keys]);
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const contextNode = ref(null);
|
|
|
|
function onTreeRightClick(info) {
|
|
contextNode.value = info.node;
|
|
emit("rightClick", info);
|
|
}
|
|
|
|
const contextMenuItems = computed(() => {
|
|
if (!props.contextMenu || !contextNode.value) return undefined;
|
|
const result = props.contextMenu(contextNode.value);
|
|
if (!result) return undefined;
|
|
return { ...result, onClick: onMenuClick };
|
|
});
|
|
|
|
function onMenuClick({ key }) {
|
|
const node = contextNode.value;
|
|
contextNode.value = null;
|
|
if (node) emit("menuClick", { key, node });
|
|
}
|
|
|
|
const selectSuppress = ref(false);
|
|
|
|
function onSelect(keys, info) {
|
|
// 点击有子节点的父节点时,阻止 a-tree 内部自动触发展开
|
|
if (info?.node?.children?.length) {
|
|
selectSuppress.value = true;
|
|
}
|
|
emit("select", keys, info);
|
|
}
|
|
|
|
function onExpandedKeysChange(keys) {
|
|
if (selectSuppress.value) {
|
|
selectSuppress.value = false;
|
|
return;
|
|
}
|
|
emit("update:expandedKeys", keys);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<a-dropdown
|
|
v-if="contextMenu"
|
|
:trigger="['contextmenu']"
|
|
:menu="contextMenuItems"
|
|
:disabled="contextMenuDisabled"
|
|
>
|
|
<div class="fms-tree-wrapper">
|
|
<a-tree
|
|
v-bind="treeAttrs"
|
|
:tree-data="effectiveTreeData"
|
|
:expanded-keys="expandedKeys"
|
|
:default-expanded-keys="[rootKey]"
|
|
:block-node="blockNode"
|
|
:virtual="effectiveVirtual"
|
|
:show-line="showLine"
|
|
:draggable="draggable"
|
|
:allow-drop="allowDrop"
|
|
@right-click="onTreeRightClick"
|
|
@select="onSelect"
|
|
@drop="$emit('drop', $event)"
|
|
@dragstart="$emit('dragStart', $event)"
|
|
@dragenter="$emit('dragEnter', $event)"
|
|
@dragleave="$emit('dragLeave', $event)"
|
|
@dragover="$emit('dragOver', $event)"
|
|
@dragend="$emit('dragEnd', $event)"
|
|
@update:expanded-keys="onExpandedKeysChange"
|
|
>
|
|
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
<slot :name="name" v-bind="slotProps || {}" />
|
|
</template>
|
|
</a-tree>
|
|
</div>
|
|
</a-dropdown>
|
|
<a-tree
|
|
v-else
|
|
v-bind="treeAttrs"
|
|
:tree-data="effectiveTreeData"
|
|
:expanded-keys="expandedKeys"
|
|
:default-expanded-keys="[rootKey]"
|
|
:block-node="blockNode"
|
|
:virtual="effectiveVirtual"
|
|
:show-line="showLine"
|
|
:draggable="draggable"
|
|
:allow-drop="allowDrop"
|
|
@select="onSelect"
|
|
@drop="$emit('drop', $event)"
|
|
@dragstart="$emit('dragStart', $event)"
|
|
@dragenter="$emit('dragEnter', $event)"
|
|
@dragleave="$emit('dragLeave', $event)"
|
|
@dragover="$emit('dragOver', $event)"
|
|
@dragend="$emit('dragEnd', $event)"
|
|
@update:expanded-keys="onExpandedKeysChange"
|
|
>
|
|
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
<slot :name="name" v-bind="slotProps || {}" />
|
|
</template>
|
|
</a-tree>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.fms-tree-wrapper {
|
|
display: flex;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|