From 870cd863932ae93dcf64a0875cc164bfd115b6f3 Mon Sep 17 00:00:00 2001 From: Netfan Date: Thu, 27 Mar 2025 14:22:05 +0800 Subject: [PATCH 1/3] fix: auto check parent after node selected (#5794) --- .../ui-kit/shadcn-ui/src/ui/tree/tree.vue | 281 ++++++++++-------- .../ui-kit/shadcn-ui/src/ui/tree/types.ts | 42 +++ .../src/views/system/role/modules/form.vue | 2 +- 3 files changed, 207 insertions(+), 118 deletions(-) create mode 100644 packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue index 46866979..b75088bb 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue @@ -4,7 +4,9 @@ import type { FlattenedItem } from 'radix-vue'; import type { ClassType, Recordable } from '@vben-core/typings'; -import { onMounted, ref, watch, watchEffect } from 'vue'; +import type { TreeProps } from './types'; + +import { onMounted, ref, watchEffect } from 'vue'; import { ChevronRight, IconifyIcon } from '@vben-core/icons'; import { cn, get } from '@vben-core/shared/utils'; @@ -14,46 +16,13 @@ import { TreeItem, TreeRoot } from 'radix-vue'; import { Checkbox } from '../checkbox'; -interface TreeProps { - /** 单选时允许取消已有选项 */ - allowClear?: boolean; - /** 显示边框 */ - bordered?: boolean; - /** 取消父子关联选择 */ - checkStrictly?: boolean; - /** 子级字段名 */ - childrenField?: string; - /** 默认展开的键 */ - defaultExpandedKeys?: Array; - /** 默认展开的级别(优先级高于defaultExpandedKeys) */ - defaultExpandedLevel?: number; - /** 默认值 */ - defaultValue?: Arrayable; - /** 禁用 */ - disabled?: boolean; - /** 自定义节点类名 */ - getNodeClass?: (item: FlattenedItem>) => string; - iconField?: string; - /** label字段 */ - labelField?: string; - /** 当前值 */ - modelValue?: Arrayable; - /** 是否多选 */ - multiple?: boolean; - /** 显示由iconField指定的图标 */ - showIcon?: boolean; - /** 启用展开收缩动画 */ - transition?: boolean; - /** 树数据 */ - treeData: Recordable[]; - /** 值字段 */ - valueField?: string; -} const props = withDefaults(defineProps(), { allowClear: false, + autoCheckParent: true, bordered: false, checkStrictly: false, defaultExpandedKeys: () => [], + defaultExpandedLevel: 0, disabled: false, expanded: () => [], iconField: 'icon', @@ -61,7 +30,7 @@ const props = withDefaults(defineProps(), { modelValue: () => [], multiple: false, showIcon: true, - transition: false, + transition: true, valueField: 'value', childrenField: 'children', }); @@ -72,28 +41,36 @@ const emits = defineEmits<{ 'update:modelValue': [value: Arrayable>]; }>(); -interface InnerFlattenItem> { +interface InnerFlattenItem, P = number | string> { hasChildren: boolean; level: number; + parents: P[]; value: T; } -function flatten>( +function flatten, P = number | string>( items: T[], childrenField: string = 'children', level = 0, -): InnerFlattenItem[] { - const result: InnerFlattenItem[] = []; + parents: P[] = [], +): InnerFlattenItem[] { + const result: InnerFlattenItem[] = []; items.forEach((item) => { const children = get(item, childrenField) as Array; const val = { hasChildren: Array.isArray(children) && children.length > 0, level, + parents: [...parents], value: item, }; result.push(val); if (val.hasChildren) - result.push(...flatten(children, childrenField, level + 1)); + result.push( + ...flatten(children, childrenField, level + 1, [ + ...parents, + get(item, props.valueField), + ]), + ); }); return result; } @@ -133,14 +110,6 @@ function updateTreeValue() { : getItemByValue(val); } -watch( - modelValue, - () => { - updateTreeValue(); - }, - { deep: true, immediate: true }, -); - function updateModelValue(val: Arrayable>) { modelValue.value = Array.isArray(val) ? val.map((v) => get(v, props.valueField)) @@ -186,7 +155,33 @@ function collapseAll() { function onToggle(item: FlattenedItem>) { emits('expand', item); } -function onSelect(item: FlattenedItem>) { +function onSelect(item: FlattenedItem>, isSelected: boolean) { + if ( + !props.checkStrictly && + props.multiple && + props.autoCheckParent && + isSelected + ) { + flattenData.value + .find((i) => { + return ( + get(i.value, props.valueField) === get(item.value, props.valueField) + ); + }) + ?.parents?.forEach((p) => { + if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) { + modelValue.value.push(p); + } + }); + } else { + if (Array.isArray(modelValue.value)) { + const index = modelValue.value.indexOf(get(item.value, props.valueField)); + if (index !== -1) { + modelValue.value.splice(index, 1); + } + } + } + updateTreeValue(); emits('select', item); } @@ -224,78 +219,130 @@ defineExpose({
- - -
- -
- -
- - - {{ get(item.value, labelField) }} - -
-
+ +
+ +
+ +
+ + + {{ get(item.value, labelField) }} + +
+ +
+ diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts new file mode 100644 index 00000000..cb4e82da --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts @@ -0,0 +1,42 @@ +import type { Arrayable } from '@vueuse/core'; +import type { FlattenedItem } from 'radix-vue'; + +import type { Recordable } from '@vben-core/typings'; + +export interface TreeProps { + /** 单选时允许取消已有选项 */ + allowClear?: boolean; + /** 非关联选择时,自动选中上级节点 */ + autoCheckParent?: boolean; + /** 显示边框 */ + bordered?: boolean; + /** 取消父子关联选择 */ + checkStrictly?: boolean; + /** 子级字段名 */ + childrenField?: string; + /** 默认展开的键 */ + defaultExpandedKeys?: Array; + /** 默认展开的级别(优先级高于defaultExpandedKeys) */ + defaultExpandedLevel?: number; + /** 默认值 */ + defaultValue?: Arrayable; + /** 禁用 */ + disabled?: boolean; + /** 自定义节点类名 */ + getNodeClass?: (item: FlattenedItem>) => string; + iconField?: string; + /** label字段 */ + labelField?: string; + /** 当前值 */ + modelValue?: Arrayable; + /** 是否多选 */ + multiple?: boolean; + /** 显示由iconField指定的图标 */ + showIcon?: boolean; + /** 启用展开收缩动画 */ + transition?: boolean; + /** 树数据 */ + treeData: Recordable[]; + /** 值字段 */ + valueField?: string; +} diff --git a/playground/src/views/system/role/modules/form.vue b/playground/src/views/system/role/modules/form.vue index 1b6d00e8..cae54a1c 100644 --- a/playground/src/views/system/role/modules/form.vue +++ b/playground/src/views/system/role/modules/form.vue @@ -98,7 +98,7 @@ function getNodeClass(node: Recordable) {