vue3 递归写个Tree组件

150 阅读2分钟

效果

WX20231127-205116@2x.png

要素

  • 组件调用自身
    组件中调用自身,直接使用组件名标签,要保证循环有结束的出口,避免陷入无限递归

  • 事件传递
    使用prop属性传递事件不会乱套,直接最终都是调用的顶级组件中的方法, 如果采用emit,在顶级组件中有自定义方法,但是在TreeItem组件内部绑定的方法不好定义

  • 数据传递
    同事件传递一样,不要使用v-model,而是采用prop属性传递修改数据的函数

  • css样式
    subtree中的tree-item要设置横线和竖线元素,其中竖线设置为100%高度且都向上偏移(高度100%可根据margin自由调整,我这里是calc(100%+10px)),另外对于最后一个子元素要设置固定的高度32,也就是一行tree-item的高度

编码

<template>
  <div class="page-content">
    <ul v-for="(tree, index) in treeData" :key="index" class="vue-tree">
      <TreeItem
        v-for="(subTree, subIndex) in tree.children"
        :key="subIndex"
        :data-item="subTree"
        :click-event="handleClickEvent"
        :change-data="changeData"
      ></TreeItem>
    </ul>
    <el-button type="primary" @click="submit">确认</el-button>
  </div>
</template>

<script lang="ts" setup>
import { reactive } from 'vue';
import TreeItem from './components/TreeItem.vue';

const treeData = reactive<IDataItem[]>([
  {
    paramName: 'tree',
    label: 'tree',
    required: false,
    description: '根组件',
    children: [
      {
        paramName: 'value1',
        label: '1',
        required: true,
        description: '1',
        children: [
          {
            paramName: 'value11',
            label: '1-1',
            required: true,
            description: '1-1',
            children: [
              {
                paramName: 'value111',
                label: '1-1-1',
                required: true,
                description: '1-1-1',
                children: [
                  {
                    paramName: 'value1111',
                    label: '1-1-1-1',
                    required: true,
                    description: '1-1-1-1',
                    children: [
                      {
                        paramName: 'value11111',
                        label: '1-1-1-1-1',
                        required: true,
                        description: '1-1-1-1-1',
                        children: [],
                      },
                    ],
                  },
                ],
              },
              {
                paramName: 'value112',
                label: '1-1-2',
                required: true,
                description: '1-1-2',
                children: [
                  {
                    paramName: 'value1121',
                    label: '1-1-2-1',
                    required: true,
                    description: '1-1-2-1',
                    children: [
                      {
                        paramName: 'value11211',
                        label: '1-1-2-1-1',
                        required: true,
                        description: '1-1-2-1-1',
                        children: [],
                      },
                    ],
                  },
                ],
              },
            ],
          },
          {
            paramName: 'value12',
            label: '1-2',
            required: true,
            description: '1-2',
            children: [],
          },
        ],
      },
      {
        paramName: '2',
        label: '2',
        required: false,
        description: '2',
        children: [],
      },
    ],
  },
]);

function handleClickEvent(item: IDataItem) {
  item.folded = !item.folded;
}

function changeData(item: IDataItem, key: keyof IDataItem, value: IDataItem[keyof IDataItem]) {
  item[key] = value;
}

function submit() {
  console.log('treeData=>>>', treeData);
}
</script>

<style lang="scss" scoped>
.page-content {
  padding: 20px;
}

.vue-tree {
  list-style: none;
  padding: 0;
  margin-bottom: 10px;
}
</style>

<template>
  <li class="tree-item">
    <div class="vertical-line"></div>
    <div class="horizontal-line"></div>
    <div class="line-wrapper">
      <template v-if="props.dataItem.children && props.dataItem.children.length">
        <mtd-icon v-if="props.dataItem.folded" name="add-square-fill" @click="expand"></mtd-icon>
        <mtd-icon v-else name="checkbox-indetermina" @click="collapse"></mtd-icon>
      </template>

      <mtd-input :model-value="props.dataItem.paramName" @input="changeValue('paramName', $event)"></mtd-input>

      <mtd-input :model-value="props.dataItem.label" @change="changeValue('label', $event)"></mtd-input>

      <mtd-radio-group :model-value="props.dataItem.required" @change="changeValue('required', $event)">
        <mtd-radio :value="true">必填</mtd-radio>
        <mtd-radio :value="false">非必填</mtd-radio>
      </mtd-radio-group>
    </div>

    <ul v-if="props.dataItem.children && props.dataItem.children.length && !props.dataItem.folded" class="sub-tree">
      <TreeItem
        v-for="(item, index) in props.dataItem.children"
        :key="index"
        :data-item="item"
        :click-event="props.clickEvent"
        :change-data="props.changeData"
        style="margin-left: 50px"
      ></TreeItem>
    </ul>
  </li>
</template>

<script lang="ts" setup>
import { PropType } from 'vue';

const emit = defineEmits(['click', 'update:dataItem']);

const props = defineProps({
  dataItem: {
    type: Object as PropType<IDataItem>,
    required: true,
  },
  clickEvent: {
    type: Function as PropType<(data: IDataItem) => void>,
    required: true,
  },
  changeData: {
    type: Function as PropType<(data: IDataItem, key: keyof IDataItem, value: IDataItem[keyof IDataItem]) => void>,
    required: true,
  },
});

function expand() {
  props.clickEvent(props.dataItem);
}

function collapse() {
  props.clickEvent(props.dataItem);
}

function changeValue(key: keyof IDataItem, value: IDataItem[keyof IDataItem]) {
  console.log('changeValue=>>>>', key, value);
  props.changeData(props.dataItem, key, value);
}

// function changeValue(key: string, value: unknown) {
//   console.log('changeValue=>>>>', key, value);
//   props.changeData(props.dataItem, key, value);
// }
</script>

<style lang="scss" scoped>
.tree-item {
  background: #f9fafc;
  border-radius: 6px;
  position: relative;

  .line-wrapper {
    margin-bottom: 10px;
  }

  .sub-tree {
    .vertical-line {
      position: absolute;
      height: calc(100% + 10px);
      border: 1px dashed #dcdddf;
      top: -24px;
      left: -44px;
    }
    .horizontal-line {
      position: absolute;
      width: 44px;
      border-bottom: 1px dashed #dcdddf;
      left: -44px;
      top: 18px;
    }
  }
}

.tree-item:last-child {
  .vertical-line {
    position: absolute;
    height: 36px;
    border: 1px dashed #dcdddf;
    top: -21px;
    left: -44px;
  }
}
</style>

interface IDataItem {
  paramName: string;
  label: string;
  required: boolean;
  description: string;
  children: IDataItem[];
  folded?: boolean;
}