fix: auto check parent after node selected (#5794)
							parent
							
								
									7863652be1
								
							
						
					
					
						commit
						ffd7f1a53f
					
				| 
						 | 
				
			
			@ -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,6 +219,11 @@ defineExpose({
 | 
			
		|||
    <div class="w-full" v-if="$slots.header">
 | 
			
		||||
      <slot name="header"> </slot>
 | 
			
		||||
    </div>
 | 
			
		||||
    <TransitionGroup
 | 
			
		||||
      :name="transition ? 'fade' : ''"
 | 
			
		||||
      mode="out-in"
 | 
			
		||||
      class="container"
 | 
			
		||||
    >
 | 
			
		||||
      <TreeItem
 | 
			
		||||
        v-for="item in flattenItems"
 | 
			
		||||
        v-slot="{
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +246,7 @@ defineExpose({
 | 
			
		|||
            if (event.detail.originalEvent.type === 'click') {
 | 
			
		||||
              // event.preventDefault();
 | 
			
		||||
            }
 | 
			
		||||
          onSelect(item);
 | 
			
		||||
            onSelect(item, event.detail.isSelected);
 | 
			
		||||
          }
 | 
			
		||||
        "
 | 
			
		||||
        @toggle="
 | 
			
		||||
| 
						 | 
				
			
			@ -263,7 +263,12 @@ defineExpose({
 | 
			
		|||
          v-if="item.hasChildren"
 | 
			
		||||
          class="size-4 cursor-pointer transition"
 | 
			
		||||
          :class="{ 'rotate-90': isExpanded }"
 | 
			
		||||
        @click.stop="handleToggle"
 | 
			
		||||
          @click.stop="
 | 
			
		||||
            () => {
 | 
			
		||||
              handleToggle();
 | 
			
		||||
              onToggle(item);
 | 
			
		||||
            }
 | 
			
		||||
          "
 | 
			
		||||
        />
 | 
			
		||||
        <div v-else class="h-4 w-4">
 | 
			
		||||
          <!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
 | 
			
		||||
| 
						 | 
				
			
			@ -272,15 +277,21 @@ defineExpose({
 | 
			
		|||
          v-if="multiple"
 | 
			
		||||
          :checked="isSelected"
 | 
			
		||||
          :indeterminate="isIndeterminate"
 | 
			
		||||
        @click.stop="handleSelect"
 | 
			
		||||
          @click="
 | 
			
		||||
            () => {
 | 
			
		||||
              handleSelect();
 | 
			
		||||
              // onSelect(item, !isSelected);
 | 
			
		||||
            }
 | 
			
		||||
          "
 | 
			
		||||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          class="flex items-center gap-1 pl-2"
 | 
			
		||||
          @click="
 | 
			
		||||
          ($event) => {
 | 
			
		||||
            $event.stopPropagation();
 | 
			
		||||
            $event.preventDefault();
 | 
			
		||||
            (_event) => {
 | 
			
		||||
              // $event.stopPropagation();
 | 
			
		||||
              // $event.preventDefault();
 | 
			
		||||
              handleSelect();
 | 
			
		||||
              // onSelect(item, !isSelected);
 | 
			
		||||
            }
 | 
			
		||||
          "
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			@ -294,8 +305,44 @@ defineExpose({
 | 
			
		|||
          </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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue