mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2026-10-06 12:33:49 +08:00
feat(tree): 树组件能力优化,增加includeIndeterminate控制参数,支持modelValue仅返回全选节点 & 全选节点与半选节点同时返回; (#8290)
* Add includeIndeterminate option to tree types
* Refactor tree component for improved state management
* fix(tree): 修复 1、声明后移动初始化块。
当 有初始值时,调用 之前的调用被初始化。这会抛弃并阻止组件渲染。modelValueupdateTreeValue()getItemByValue()const valueMapReferenceError: Cannot access 'valueMap' before initialization
2、不要用 来对选择分支进行门控。
现在警卫要求整个街区都有。该分支执行 。如果消费者设置 ,祖先不再被添加在选择中。 在 中仅影响 uncheck 的级联移除。!props.keepParentOnUncheckisSelectedautoCheckParentkeepParentOnUncheck: truekeepParentOnUnchecktypes.ts
3、避免将不确定的祖先ID排除在Reka UI内部选择之外
启用时,将每个不确定祖先添加到 ,并按选中方式传递给 Reka UI 2.10.3。复选框依然会渲染,因为优先级。然而,点击该祖先后,它被视为被选中,因此移除了祖先及其所有后代,而不是选择后代。将不确定的身份从中移除,只将它们添加到发射的 。includeIndeterminatemergeIndeterminatemodelValueupdateTreeValueMinusindeterminateTreeRootpropagateSelecttreeValuemodelValue
---------
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import type { ClassType, Recordable } from '@vben-core/typings';
|
||||
|
||||
import type { TreeProps } from './types';
|
||||
|
||||
import { computed, onMounted, ref, watchEffect } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { ChevronRight, IconifyIcon } from '@vben-core/icons';
|
||||
import { cn, get } from '@vben-core/shared/utils';
|
||||
@@ -62,36 +62,21 @@ function flatten<T = Recordable<any>, P = number | string>(
|
||||
|
||||
const flattenData = ref<Array<InnerFlattenItem>>([]);
|
||||
const modelValue = defineModel<Arrayable<number | string>>();
|
||||
const expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []);
|
||||
const expanded = ref<Array<number | string>>([]);
|
||||
|
||||
const treeValue = ref();
|
||||
let lastTreeData: any = null;
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
flattenData.value = flatten(props.treeData, props.childrenField);
|
||||
if (flattenData.value.length > 0) {
|
||||
updateTreeValue();
|
||||
}
|
||||
|
||||
// 只在 treeData 变化时执行展开
|
||||
const currentTreeData = JSON.stringify(props.treeData);
|
||||
if (lastTreeData !== currentTreeData) {
|
||||
lastTreeData = currentTreeData;
|
||||
if (
|
||||
props.defaultExpandedLevel !== undefined &&
|
||||
props.defaultExpandedLevel > 0
|
||||
) {
|
||||
expandToLevel(props.defaultExpandedLevel);
|
||||
}
|
||||
}
|
||||
});
|
||||
/** ID → 原始数据映射,O(1) 查找替代 O(n) find */
|
||||
const valueMap = computed(() => {
|
||||
const map = new Map<number | string, Recordable<any>>();
|
||||
for (const item of flattenData.value) {
|
||||
map.set(get(item.value, props.valueField) as number | string, item.value);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
function getItemByValue(value: number | string) {
|
||||
return flattenData.value.find(
|
||||
(item) => get(item.value, props.valueField) === value,
|
||||
)?.value;
|
||||
return valueMap.value.get(value);
|
||||
}
|
||||
|
||||
function updateTreeValue() {
|
||||
@@ -102,14 +87,30 @@ function updateTreeValue() {
|
||||
if (val.length === 0) {
|
||||
treeValue.value = [];
|
||||
} else {
|
||||
const filteredValues = val.filter((v) => {
|
||||
// 单次遍历完成过滤 + 映射,避免重复 getItemByValue
|
||||
const enabledItems: Recordable<any>[] = [];
|
||||
const enabledIds: (number | string)[] = [];
|
||||
for (const v of val) {
|
||||
const item = getItemByValue(v);
|
||||
return item && !get(item, props.disabledField);
|
||||
});
|
||||
treeValue.value = filteredValues.map((v) => getItemByValue(v));
|
||||
if (item && !get(item, props.disabledField)) {
|
||||
enabledItems.push(item);
|
||||
enabledIds.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredValues.length !== val.length) {
|
||||
modelValue.value = filteredValues;
|
||||
// 半选节点不传给 Reka UI 内部选择:Reka UI 会把 model-value 中的节点当作已选中,
|
||||
// 点击半选祖先时会被视为取消选择,误移除祖先及其所有后代。半选节点仅保留在 modelValue 返回值中。
|
||||
if (props.includeIndeterminate && props.multiple) {
|
||||
const excludedKeys = computeIndeterminateKeysInSelection(enabledIds);
|
||||
treeValue.value = enabledItems.filter(
|
||||
(item) => !excludedKeys.has(get(item.value, props.valueField)),
|
||||
);
|
||||
} else {
|
||||
treeValue.value = enabledItems;
|
||||
}
|
||||
|
||||
if (enabledIds.length !== val.length) {
|
||||
modelValue.value = mergeIndeterminate(enabledIds);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -123,22 +124,82 @@ function updateTreeValue() {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:构建 flattenData 并同步 treeValue
|
||||
flattenData.value = flatten(props.treeData, props.childrenField);
|
||||
if (flattenData.value.length > 0) {
|
||||
updateTreeValue();
|
||||
}
|
||||
|
||||
// 初始化:展开 — defaultExpandedLevel 优先,否则使用 defaultExpandedKeys
|
||||
if (
|
||||
props.defaultExpandedLevel !== undefined &&
|
||||
props.defaultExpandedLevel > 0
|
||||
) {
|
||||
expandToLevel(props.defaultExpandedLevel);
|
||||
} else {
|
||||
expanded.value = props.defaultExpandedKeys ?? [];
|
||||
}
|
||||
|
||||
// 仅监听 treeData 及相关字段变更,重建 flattenData
|
||||
let lastTreeData: any = JSON.stringify(props.treeData);
|
||||
watch(
|
||||
() => ({
|
||||
treeData: props.treeData,
|
||||
childrenField: props.childrenField,
|
||||
valueField: props.valueField,
|
||||
}),
|
||||
() => {
|
||||
flattenData.value = flatten(props.treeData, props.childrenField);
|
||||
if (flattenData.value.length > 0) {
|
||||
updateTreeValue();
|
||||
}
|
||||
|
||||
// 仅在 treeData 内容实质变化时才重新展开(避免过度展开)
|
||||
const currentTreeData = JSON.stringify(props.treeData);
|
||||
if (lastTreeData !== currentTreeData) {
|
||||
lastTreeData = currentTreeData;
|
||||
if (
|
||||
props.defaultExpandedLevel !== undefined &&
|
||||
props.defaultExpandedLevel > 0
|
||||
) {
|
||||
expandToLevel(props.defaultExpandedLevel);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 仅监听 modelValue 变更,同步 treeValue(不重建 flattenData,避免响应式回环)
|
||||
// flush: 'sync' 确保赋值后立即同步,配合 checkAll/unCheckAll 移除手动 updateTreeValue 调用
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
() => {
|
||||
if (flattenData.value.length > 0) {
|
||||
updateTreeValue();
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
|
||||
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
||||
if (Array.isArray(val)) {
|
||||
const filteredVal = val.filter((v) => !get(v, props.disabledField));
|
||||
modelValue.value = filteredVal.map((v) => get(v, props.valueField));
|
||||
const selectedIds = filteredVal.map((v) => get(v, props.valueField));
|
||||
modelValue.value = mergeIndeterminate(selectedIds);
|
||||
} else {
|
||||
if (val && !get(val, props.disabledField)) {
|
||||
modelValue.value = get(val, props.valueField);
|
||||
}
|
||||
// 单选模式下取消选择时 val 为 null/undefined,需要同步清空
|
||||
modelValue.value =
|
||||
val && !get(val, props.disabledField)
|
||||
? get(val, props.valueField)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function expandToLevel(level: number) {
|
||||
const keys: string[] = [];
|
||||
const keys: (number | string)[] = [];
|
||||
flattenData.value.forEach((item) => {
|
||||
if (item.level <= level - 1) {
|
||||
keys.push(get(item.value, props.valueField));
|
||||
keys.push(get(item.value, props.valueField) as number | string);
|
||||
}
|
||||
});
|
||||
expanded.value = keys;
|
||||
@@ -172,20 +233,19 @@ function collapseAll() {
|
||||
|
||||
function checkAll() {
|
||||
if (!props.multiple) return;
|
||||
modelValue.value = [
|
||||
const selectedIds = [
|
||||
...new Set(
|
||||
flattenData.value
|
||||
.filter((item) => !get(item.value, props.disabledField))
|
||||
.map((item) => get(item.value, props.valueField)),
|
||||
),
|
||||
];
|
||||
updateTreeValue();
|
||||
modelValue.value = mergeIndeterminate(selectedIds);
|
||||
}
|
||||
|
||||
function unCheckAll() {
|
||||
if (!props.multiple) return;
|
||||
modelValue.value = [];
|
||||
updateTreeValue();
|
||||
}
|
||||
|
||||
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
|
||||
@@ -201,9 +261,8 @@ const selectAllStatus = computed<'indeterminate' | boolean>(() => {
|
||||
.filter((item) => !get(item.value, props.disabledField))
|
||||
.map((item) => get(item.value, props.valueField));
|
||||
|
||||
const selectedCount = allValues.filter((v) =>
|
||||
(modelValue.value as (number | string)[]).includes(v),
|
||||
).length;
|
||||
const selectedSet = new Set(modelValue.value as (number | string)[]);
|
||||
const selectedCount = allValues.filter((v) => selectedSet.has(v)).length;
|
||||
|
||||
if (selectedCount === 0) return false;
|
||||
if (selectedCount === allValues.length) return true;
|
||||
@@ -226,68 +285,177 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
props.checkStrictly &&
|
||||
props.multiple &&
|
||||
props.autoCheckParent &&
|
||||
isSelected
|
||||
) {
|
||||
flattenData.value
|
||||
.find((i) => {
|
||||
return (
|
||||
get(i.value, props.valueField) === get(item.value, props.valueField)
|
||||
);
|
||||
})
|
||||
?.parents?.filter((item) => !get(item, props.disabledField))
|
||||
?.forEach((p) => {
|
||||
if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {
|
||||
modelValue.value.push(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (
|
||||
props.checkStrictly &&
|
||||
props.multiple &&
|
||||
props.autoCheckParent &&
|
||||
!props.keepParentOnUncheck &&
|
||||
!isSelected
|
||||
) {
|
||||
flattenData.value
|
||||
.find((i) => {
|
||||
return (
|
||||
get(i.value, props.valueField) === get(item.value, props.valueField)
|
||||
);
|
||||
})
|
||||
?.parents?.filter((item) => !get(item, props.disabledField))
|
||||
?.toReversed()
|
||||
.forEach((p) => {
|
||||
const children = flattenData.value.filter((i) => {
|
||||
return (
|
||||
i.parents.length > 0 &&
|
||||
i.parents.includes(p) &&
|
||||
i.id !== item._id &&
|
||||
i.parentId === p
|
||||
);
|
||||
if (props.checkStrictly && props.multiple && props.autoCheckParent) {
|
||||
// 获取当前节点在 flattenData 中的记录以读取 parents
|
||||
const flatItem = flattenData.value.find(
|
||||
(i) =>
|
||||
get(i.value, props.valueField) === get(item.value, props.valueField),
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
// 选中时:将所有已启用的祖先追加到 modelValue(keepParentOnUncheck 不影响选中时的祖先追加)
|
||||
flatItem?.parents
|
||||
?.filter((p) => {
|
||||
const parentItem = getItemByValue(p);
|
||||
return parentItem && !get(parentItem, props.disabledField);
|
||||
})
|
||||
?.forEach((p) => {
|
||||
if (
|
||||
Array.isArray(modelValue.value) &&
|
||||
!modelValue.value.includes(p)
|
||||
) {
|
||||
modelValue.value.push(p);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(modelValue.value)) {
|
||||
const hasSelectedChild = children.some((child) =>
|
||||
(modelValue.value as unknown[]).includes(
|
||||
get(child.value, props.valueField),
|
||||
),
|
||||
} else if (!props.keepParentOnUncheck) {
|
||||
// 取消选中时:移除不再有已选子节点的祖先;keepParentOnUncheck 为 true 时保留父节点选中状态
|
||||
flatItem?.parents
|
||||
?.filter((p) => {
|
||||
const parentItem = getItemByValue(p);
|
||||
return parentItem && !get(parentItem, props.disabledField);
|
||||
})
|
||||
?.toReversed()
|
||||
?.forEach((p) => {
|
||||
const hasSelectedChild = flattenData.value.some(
|
||||
(i) =>
|
||||
i.parentId === p &&
|
||||
i.id !== item._id &&
|
||||
Array.isArray(modelValue.value) &&
|
||||
(modelValue.value as (number | string)[]).includes(
|
||||
get(i.value, props.valueField) as number | string,
|
||||
),
|
||||
);
|
||||
if (!hasSelectedChild) {
|
||||
const index = modelValue.value.indexOf(p);
|
||||
const index = (modelValue.value as (number | string)[]).indexOf(p);
|
||||
if (index !== -1) {
|
||||
modelValue.value.splice(index, 1);
|
||||
(modelValue.value as (number | string)[]).splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// 确保 watcher 同步 treeValue:checkStrictly 模式需要新引用(祖先 push/pop 已原位修改数组),
|
||||
// 或 includeIndeterminate 需合并半选节点时创建新引用
|
||||
if (props.multiple && Array.isArray(modelValue.value)) {
|
||||
if (props.includeIndeterminate) {
|
||||
modelValue.value = mergeIndeterminate(
|
||||
modelValue.value as (number | string)[],
|
||||
);
|
||||
} else if (props.checkStrictly) {
|
||||
modelValue.value = [...modelValue.value];
|
||||
}
|
||||
}
|
||||
updateTreeValue();
|
||||
emits('select', item);
|
||||
}
|
||||
|
||||
/** 计算给定选中列表中,处于半选(indeterminate)状态的节点 key */
|
||||
function computeIndeterminateKeys(
|
||||
selectedIds: (number | string)[],
|
||||
): (number | string)[] {
|
||||
if (
|
||||
!props.multiple ||
|
||||
selectedIds.length === 0 ||
|
||||
flattenData.value.length === 0
|
||||
)
|
||||
return [];
|
||||
|
||||
const selectedSet = new Set(selectedIds);
|
||||
// 统计每个祖先节点下有多少个选中后代
|
||||
const ancestorCounts = new Map<number | string, number>();
|
||||
|
||||
for (const node of flattenData.value) {
|
||||
const nodeId = get(node.value, props.valueField) as number | string;
|
||||
if (!selectedSet.has(nodeId)) continue;
|
||||
|
||||
// 为当前选中节点的所有祖先累加计数
|
||||
for (const parentId of node.parents) {
|
||||
ancestorCounts.set(parentId, (ancestorCounts.get(parentId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤出自身未选中的祖先即为半选节点
|
||||
const result: (number | string)[] = [];
|
||||
for (const [nodeId] of ancestorCounts) {
|
||||
if (!selectedSet.has(nodeId)) {
|
||||
result.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算选中列表中"半选祖先节点"的 key:节点自身已在选中列表,但后代仅部分被选中。
|
||||
* 与 computeIndeterminateKeys 不同,它基于后代选中数量统计,适用于 modelValue 已合并半选节点的场景,
|
||||
* 用于从传给 Reka UI 的内部选择中剔除半选节点,并用于补充复选框的半选状态展示。
|
||||
*/
|
||||
function computeIndeterminateKeysInSelection(
|
||||
selectedIds: (number | string)[],
|
||||
): Set<number | string> {
|
||||
const result = new Set<number | string>();
|
||||
if (
|
||||
!props.multiple ||
|
||||
selectedIds.length === 0 ||
|
||||
flattenData.value.length === 0
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const selectedSet = new Set(selectedIds);
|
||||
const totalDescendants = new Map<number | string, number>();
|
||||
const selectedDescendants = new Map<number | string, number>();
|
||||
|
||||
for (const node of flattenData.value) {
|
||||
const nodeId = get(node.value, props.valueField) as number | string;
|
||||
for (const parentId of node.parents) {
|
||||
totalDescendants.set(parentId, (totalDescendants.get(parentId) ?? 0) + 1);
|
||||
if (selectedSet.has(nodeId)) {
|
||||
selectedDescendants.set(
|
||||
parentId,
|
||||
(selectedDescendants.get(parentId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of selectedIds) {
|
||||
const total = totalDescendants.get(id) ?? 0;
|
||||
const selected = selectedDescendants.get(id) ?? 0;
|
||||
if (total > 0 && selected > 0 && selected < total) {
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 半选节点 key 集合,用于 Reka UI 不计算半选状态的场景(如 checkStrictly)下补充复选框展示 */
|
||||
const indeterminateKeys = computed(() => {
|
||||
if (!props.multiple || !props.includeIndeterminate) {
|
||||
return new Set<number | string>();
|
||||
}
|
||||
const ids = Array.isArray(modelValue.value)
|
||||
? (modelValue.value as (number | string)[])
|
||||
: [];
|
||||
return computeIndeterminateKeysInSelection(ids);
|
||||
});
|
||||
|
||||
/** 根据 includeIndeterminate 配置,将半选节点合并到选中列表 */
|
||||
function mergeIndeterminate(
|
||||
selectedIds: (number | string)[],
|
||||
): (number | string)[] {
|
||||
if (
|
||||
!props.includeIndeterminate ||
|
||||
!props.multiple ||
|
||||
selectedIds.length === 0
|
||||
) {
|
||||
return selectedIds;
|
||||
}
|
||||
const indeterminate = computeIndeterminateKeys(selectedIds);
|
||||
if (indeterminate.length === 0) return selectedIds;
|
||||
return [...new Set([...selectedIds, ...indeterminate])];
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
collapseAll,
|
||||
collapseNodes,
|
||||
@@ -301,8 +469,8 @@ defineExpose({
|
||||
</script>
|
||||
<template>
|
||||
<TreeRoot
|
||||
:get-key="(item) => get(item, valueField)"
|
||||
:get-children="(item) => get(item, childrenField)"
|
||||
:get-key="(item: any) => get(item, valueField)"
|
||||
:get-children="(item: any) => get(item, childrenField)"
|
||||
:items="treeData"
|
||||
:model-value="treeValue"
|
||||
v-model:expanded="expanded as string[]"
|
||||
@@ -367,19 +535,17 @@ defineExpose({
|
||||
handleToggle,
|
||||
}"
|
||||
:key="item._id"
|
||||
:style="{ 'margin-left': `${item.level - 1}rem` }"
|
||||
:class="
|
||||
cn('cursor-pointer', getNodeClass?.(item), {
|
||||
'data-[selected]:bg-accent': !multiple,
|
||||
'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
|
||||
})
|
||||
"
|
||||
v-bind="
|
||||
Object.assign(item.bind, {
|
||||
onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,
|
||||
disabled: isNodeDisabled(item),
|
||||
})
|
||||
"
|
||||
v-bind="{
|
||||
...item.bind,
|
||||
onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,
|
||||
disabled: isNodeDisabled(item),
|
||||
}"
|
||||
@select="
|
||||
(event: any) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
@@ -401,67 +567,78 @@ defineExpose({
|
||||
!isNodeDisabled(item) && onToggle(item);
|
||||
}
|
||||
"
|
||||
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded p-1 outline-hidden"
|
||||
class="tree-node focus:ring-grass8 my-0.5 rounded outline-hidden"
|
||||
>
|
||||
<!-- class="hover:ring-2" 鼠标移动上去时2px的圆环边框 -->
|
||||
<ChevronRight
|
||||
v-if="
|
||||
item.hasChildren &&
|
||||
Array.isArray(item.value[childrenField]) &&
|
||||
item.value[childrenField].length > 0
|
||||
"
|
||||
class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
@click.stop="
|
||||
() => {
|
||||
handleToggle();
|
||||
onToggle(item);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div v-else class="h-4 w-4"></div>
|
||||
<div class="flex items-center gap-1 item-checkbox">
|
||||
<Checkbox
|
||||
v-if="multiple"
|
||||
:model-value="isSelected && !isNodeDisabled(item)"
|
||||
:disabled="isNodeDisabled(item)"
|
||||
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
<!-- 内容行通过 paddingLeft 控制缩进,不受 TreeItem 自身的渲染行为影响 -->
|
||||
<div
|
||||
class="flex items-center w-full p-1"
|
||||
:style="{ paddingLeft: `${item.level - 1 + 0.25}rem` }"
|
||||
>
|
||||
<!-- 固定宽度展开区,有子节点时内部渲染箭头,无子节点时为空但宽度不变 -->
|
||||
<div class="flex shrink-0 items-center justify-center w-5">
|
||||
<ChevronRight
|
||||
v-if="
|
||||
item.hasChildren &&
|
||||
Array.isArray(item.value[childrenField]) &&
|
||||
item.value[childrenField].length > 0
|
||||
"
|
||||
class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
@click.stop="
|
||||
() => {
|
||||
handleToggle();
|
||||
onToggle(item);
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-1 item-checkbox"
|
||||
:title="get(item.value, labelField)"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
>
|
||||
<slot name="node" v-bind="item">
|
||||
<IconifyIcon
|
||||
class="size-4"
|
||||
v-if="showIcon && get(item.value, iconField)"
|
||||
:icon="get(item.value, iconField)"
|
||||
/>
|
||||
{{ get(item.value, labelField) }}
|
||||
</slot>
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 item-checkbox">
|
||||
<Checkbox
|
||||
v-if="multiple"
|
||||
:model-value="isSelected && !isNodeDisabled(item)"
|
||||
:disabled="isNodeDisabled(item)"
|
||||
:indeterminate="
|
||||
(isIndeterminate ||
|
||||
indeterminateKeys.has(get(item.value, valueField))) &&
|
||||
!isNodeDisabled(item)
|
||||
"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-1 item-checkbox"
|
||||
:title="get(item.value, labelField)"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
>
|
||||
<slot name="node" v-bind="item">
|
||||
<IconifyIcon
|
||||
class="size-4"
|
||||
v-if="showIcon && get(item.value, iconField)"
|
||||
:icon="get(item.value, iconField)"
|
||||
/>
|
||||
{{ get(item.value, labelField) }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-4 w-4 shrink-0"></div>
|
||||
</div>
|
||||
<div class="h-4 w-4"></div>
|
||||
</TreeItem>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
@@ -474,21 +651,8 @@ defineExpose({
|
||||
</div>
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background-color: #f3f3f3;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
.item-checkbox {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface TreeProps {
|
||||
/** 自定义节点类名 */
|
||||
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
|
||||
iconField?: string;
|
||||
/** 返回值是否包含半选节点(默认 false,仅返回全选节点;true 则返回全选 ∪ 半选) */
|
||||
includeIndeterminate?: boolean;
|
||||
/** 取消勾选子节点时保持父节点的选中状态,不级联取消父节点(默认 false:级联取消) */
|
||||
keepParentOnUncheck?: boolean;
|
||||
/** label字段 */
|
||||
@@ -60,6 +62,7 @@ export function treePropsDefaults() {
|
||||
multiple: false,
|
||||
showIcon: true,
|
||||
transition: true,
|
||||
includeIndeterminate: false,
|
||||
keepParentOnUncheck: false,
|
||||
valueField: 'value',
|
||||
childrenField: 'children',
|
||||
|
||||
Reference in New Issue
Block a user