效果
要素
-
组件调用自身
组件中调用自身,直接使用组件名标签,要保证循环有结束的出口,避免陷入无限递归 -
事件传递
使用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;
}