1
0
Эх сурвалжийг харах

fix: auto check parent after node selected (#5794)

Netfan 6 өдөр өмнө
parent
commit
870cd86393

+ 164 - 117
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<number | string>;
-  /** 默认展开的级别(优先级高于defaultExpandedKeys) */
-  defaultExpandedLevel?: number;
-  /** 默认值 */
-  defaultValue?: Arrayable<number | string>;
-  /** 禁用 */
-  disabled?: boolean;
-  /** 自定义节点类名 */
-  getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
-  iconField?: string;
-  /** label字段 */
-  labelField?: string;
-  /** 当前值 */
-  modelValue?: Arrayable<number | string>;
-  /** 是否多选 */
-  multiple?: boolean;
-  /** 显示由iconField指定的图标 */
-  showIcon?: boolean;
-  /** 启用展开收缩动画 */
-  transition?: boolean;
-  /** 树数据 */
-  treeData: Recordable<any>[];
-  /** 值字段 */
-  valueField?: string;
-}
 const props = withDefaults(defineProps<TreeProps>(), {
   allowClear: false,
+  autoCheckParent: true,
   bordered: false,
   checkStrictly: false,
   defaultExpandedKeys: () => [],
+  defaultExpandedLevel: 0,
   disabled: false,
   expanded: () => [],
   iconField: 'icon',
@@ -61,7 +30,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
   modelValue: () => [],
   multiple: false,
   showIcon: true,
-  transition: false,
+  transition: true,
   valueField: 'value',
   childrenField: 'children',
 });
@@ -72,28 +41,36 @@ const emits = defineEmits<{
   'update:modelValue': [value: Arrayable<Recordable<any>>];
 }>();
 
-interface InnerFlattenItem<T = Recordable<any>> {
+interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
   hasChildren: boolean;
   level: number;
+  parents: P[];
   value: T;
 }
 
-function flatten<T = Recordable<any>>(
+function flatten<T = Recordable<any>, P = number | string>(
   items: T[],
   childrenField: string = 'children',
   level = 0,
-): InnerFlattenItem<T>[] {
-  const result: InnerFlattenItem<T>[] = [];
+  parents: P[] = [],
+): InnerFlattenItem<T, P>[] {
+  const result: InnerFlattenItem<T, P>[] = [];
   items.forEach((item) => {
     const children = get(item, childrenField) as Array<T>;
     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<Recordable<any>>) {
   modelValue.value = Array.isArray(val)
     ? val.map((v) => get(v, props.valueField))
@@ -186,7 +155,33 @@ function collapseAll() {
 function onToggle(item: FlattenedItem<Recordable<any>>) {
   emits('expand', item);
 }
-function onSelect(item: FlattenedItem<Recordable<any>>) {
+function onSelect(item: FlattenedItem<Recordable<any>>, 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({
     <div class="w-full" v-if="$slots.header">
       <slot name="header"> </slot>
     </div>
-    <TreeItem
-      v-for="item in flattenItems"
-      v-slot="{
-        isExpanded,
-        isSelected,
-        isIndeterminate,
-        handleSelect,
-        handleToggle,
-      }"
-      :key="item._id"
-      :style="{ 'padding-left': `${item.level - 0.5}rem` }"
-      :class="
-        cn('cursor-pointer', getNodeClass?.(item), {
-          'data-[selected]:bg-accent': !multiple,
-        })
-      "
-      v-bind="item.bind"
-      @select="
-        (event) => {
-          if (event.detail.originalEvent.type === 'click') {
-            // event.preventDefault();
-          }
-          onSelect(item);
-        }
-      "
-      @toggle="
-        (event) => {
-          if (event.detail.originalEvent.type === 'click') {
-            event.preventDefault();
-          }
-          onToggle(item);
-        }
-      "
-      class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
+    <TransitionGroup
+      :name="transition ? 'fade' : ''"
+      mode="out-in"
+      class="container"
     >
-      <ChevronRight
-        v-if="item.hasChildren"
-        class="size-4 cursor-pointer transition"
-        :class="{ 'rotate-90': isExpanded }"
-        @click.stop="handleToggle"
-      />
-      <div v-else class="h-4 w-4">
-        <!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
-      </div>
-      <Checkbox
-        v-if="multiple"
-        :checked="isSelected"
-        :indeterminate="isIndeterminate"
-        @click.stop="handleSelect"
-      />
-      <div
-        class="flex items-center gap-1 pl-2"
-        @click="
-          ($event) => {
-            $event.stopPropagation();
-            $event.preventDefault();
-            handleSelect();
+      <TreeItem
+        v-for="item in flattenItems"
+        v-slot="{
+          isExpanded,
+          isSelected,
+          isIndeterminate,
+          handleSelect,
+          handleToggle,
+        }"
+        :key="item._id"
+        :style="{ 'padding-left': `${item.level - 0.5}rem` }"
+        :class="
+          cn('cursor-pointer', getNodeClass?.(item), {
+            'data-[selected]:bg-accent': !multiple,
+          })
+        "
+        v-bind="item.bind"
+        @select="
+          (event) => {
+            if (event.detail.originalEvent.type === 'click') {
+              // event.preventDefault();
+            }
+            onSelect(item, event.detail.isSelected);
           }
         "
+        @toggle="
+          (event) => {
+            if (event.detail.originalEvent.type === 'click') {
+              event.preventDefault();
+            }
+            onToggle(item);
+          }
+        "
+        class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
       >
-        <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>
-    </TreeItem>
+        <ChevronRight
+          v-if="item.hasChildren"
+          class="size-4 cursor-pointer transition"
+          :class="{ 'rotate-90': isExpanded }"
+          @click.stop="
+            () => {
+              handleToggle();
+              onToggle(item);
+            }
+          "
+        />
+        <div v-else class="h-4 w-4">
+          <!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
+        </div>
+        <Checkbox
+          v-if="multiple"
+          :checked="isSelected"
+          :indeterminate="isIndeterminate"
+          @click="
+            () => {
+              handleSelect();
+              // onSelect(item, !isSelected);
+            }
+          "
+        />
+        <div
+          class="flex items-center gap-1 pl-2"
+          @click="
+            (_event) => {
+              // $event.stopPropagation();
+              // $event.preventDefault();
+              handleSelect();
+              // onSelect(item, !isSelected);
+            }
+          "
+        >
+          <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>
+      </TreeItem>
+    </TransitionGroup>
     <div class="w-full" v-if="$slots.footer">
       <slot name="footer"> </slot>
     </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;
+}
+
+/* 1. 声明过渡效果 */
+.fade-move,
+.fade-enter-active,
+.fade-leave-active {
+  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
+}
+
+/* 2. 声明进入和离开的状态 */
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+  transform: scaleY(0.01) translate(30px, 0);
+}
+
+/* 3. 确保离开的项目被移除出了布局流
+      以便正确地计算移动时的动画效果。 */
+.fade-leave-active {
+  position: absolute;
+}
+</style>

+ 42 - 0
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<number | string>;
+  /** 默认展开的级别(优先级高于defaultExpandedKeys) */
+  defaultExpandedLevel?: number;
+  /** 默认值 */
+  defaultValue?: Arrayable<number | string>;
+  /** 禁用 */
+  disabled?: boolean;
+  /** 自定义节点类名 */
+  getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
+  iconField?: string;
+  /** label字段 */
+  labelField?: string;
+  /** 当前值 */
+  modelValue?: Arrayable<number | string>;
+  /** 是否多选 */
+  multiple?: boolean;
+  /** 显示由iconField指定的图标 */
+  showIcon?: boolean;
+  /** 启用展开收缩动画 */
+  transition?: boolean;
+  /** 树数据 */
+  treeData: Recordable<any>[];
+  /** 值字段 */
+  valueField?: string;
+}

+ 1 - 1
playground/src/views/system/role/modules/form.vue

@@ -98,7 +98,7 @@ function getNodeClass(node: Recordable<any>) {
   <Drawer :title="getDrawerTitle">
     <Form>
       <template #permissions="slotProps">
-        <Spin :spinning="loadingPermissions">
+        <Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
           <VbenTree
             :tree-data="permissions"
             multiple