Files
workspace/code/fms/fms-vue2/src/components/FmsTree.vue
T
2026-08-09 22:34:28 +08:00

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>