Vue 树形组件
小于 1 分钟
Vue 树形组件
1. 实现原理
本示例使用 @iconify/vue
作为图标库,如果你没有安装,可以使用下面的命令或 HTML 代码安装。
yarn
yarn add --dev @iconify/vue
npm
npm install --save-dev @iconify/vue
pnpm
pnpm add --save-dev @iconify/vue
在 Vue 的 <template>
中使用自身的名称即可实现树形组件。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
interface Node {
children?: Node[]
name: string
}
const props = defineProps<{
items: Node[]
}>()
const isShow = ref<boolean[]>([])
</script>
<template>
<ul>
<li v-for="item, i in props.items" :key="item.name">
<div class="wrapper" @click="isShow[i] = !isShow[i]">
<Icon
v-if="item.children && item.children.length"
:icon="isShow[i] ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-right-24-regular'"
/>
<Icon v-else icon="fluent:document-24-regular" />
<span class="name">{{ item.name }}</span>
</div>
<div v-if="item.children && item.children.length" class="sub-tree">
<tree-test v-show="isShow[i]" :items="item.children" />
</div>
</li>
</ul>
</template>
<style lang="scss" scoped>
li {
list-style: none;
user-select: none;
.wrapper {
padding: .2rem;
cursor: pointer;
border-radius: .2rem;
display: flex;
align-items: center;
}
.wrapper:hover {
background-color: #f5f5f5;
}
svg {
margin-right: 5px;
width: 1.2rem;
}
.sub-tree {
padding-left: 1rem;
}
}
</style>
直接使用 TreeTest
组件即可:
<TreeTest :items="data" />
2. 示例
- A
- B
- C
查看数据
[
{
"name": "A",
"children": [
{
"name": "A1",
"children": [
{
"name": "A1.1"
},
{
"name": "A1.2"
}
]
},
{
"name": "A2"
}
]
},
{
"name": "B",
"children": [
{
"name": "B1"
},
{
"name": "B2"
}
]
},
{
"name": "C"
}
]
3. 递归修改名称
我们从后端获得的数据往往和树形组件名称不一致,这时候我们需要递归修改名称。
例如后端得到的数据的名称是 label
和 subNodes
,我们需要将其修改为 name
和 children
。
interface Data {
label: string
subNodes?: Data[]
}
interface TreeNode {
children?: TreeNode[]
name: string
}
const data = [
{
label: 'A',
subNodes: [
{
label: 'A1',
subNodes: [
{
label: 'A1.1',
},
{
label: 'A1.2',
},
],
},
{
label: 'A2',
},
],
},
{
label: 'B',
subNodes: [
{
label: 'B1',
},
{
label: 'B2',
},
],
},
{
label: 'C',
},
]
function changeNameRecursive(data: Data[]): TreeNode[] {
return data.map(({ label, subNodes }) => ({
children: subNodes && changeNameRecursive(subNodes),
name: label,
}))
}
const result = changeNameRecursive(data)
console.log(JSON.stringify(result, null, 2))
4. 实现默认展开或固定
细节不予以讨论,本项目实现了一份默认展开的版本,可以参考。
查看代码
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
interface Node {
// 子节点
children?: Node[]
// 是否固定
fixed?: boolean
// 标签
label?: string
// 名称
name: string
// 注释
note?: string
// 是否默认显示
show?: boolean
}
const props = defineProps<{
items: Node[]
}>()
const isShow = ref<boolean[]>([])
/**
* 切换显示状态
* @param i 第几个节点
*/
function toggle(i: number) {
if (props.items[i].fixed)
return
if (isShow.value[i] === undefined)
isShow.value[i] = true
else
isShow.value[i] = !isShow.value[i]
}
</script>
<template>
<ul>
<li v-for="item, i in props.items" :key="item.name">
<div
:class="{ tooltip: item.note }" :data-tooltip="`${item.name}: ${item.note}`" class="wrapper"
@click="toggle(i)"
>
<Icon
v-if="item.children && item.children.length"
:icon="(!!item.show !== !!isShow[i]) ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-right-24-regular'"
/>
<Icon v-else icon="fluent:document-24-regular" />
<span class="name">{{ item.name }}</span>
<span v-if="item.label" class="label">
{{ item.label }}
<Icon v-if="item.note" icon="fluent:question-circle-24-regular" />
</span>
</div>
<div v-if="item.children && item.children.length" class="sub-tree">
<TreeNode v-show="!!item.show !== !!isShow[i]" :items="item.children" />
</div>
</li>
</ul>
</template>
<style scoped lang="scss">
$mono-font: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
ul {
padding-left: 0;
}
li {
list-style: none;
user-select: none;
.wrapper {
padding: .2rem;
cursor: pointer;
border-radius: .2rem;
display: flex;
align-items: center;
&:hover {
cursor: pointer;
box-shadow: 0 0 15px 0 var(--box-shadow);
border-radius: 10px;
}
svg {
margin-right: 5px;
padding-left: 10px;
width: 1.2rem;
}
span {
font-family: $mono-font;
&.name {
color: var(--c-text);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&.label {
margin-left: 2rem;
color: var(--c-text-quote);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
.sub-tree {
margin-left: 1.8rem;
}
// Ref: https://picturepan2.github.io/spectre/components/tooltips.html
// Different from the ../styles/tooltip.scss
// If we can split this style and use common style, it will be better.
.tooltip {
position: relative;
&::after {
background: rgba(48, 55, 66, .95);
border-radius: .2rem;
bottom: 100%;
color: #fff;
content: attr(data-tooltip);
display: block;
left: 50%;
max-width: 60vw;
opacity: 0;
overflow: hidden;
padding: .2rem .4rem;
pointer-events: none;
position: absolute;
transform: translate(-50%, .4rem);
transition: opacity .2s, transform .2s;
white-space: pre-wrap;
z-index: 300;
bottom: auto;
top: 100%;
font-family: $mono-font;
}
&:hover::after {
opacity: 0.9;
transform: translate(-50%, .2rem);
}
}
</style>