前言
公司需求,需要实现如下样式的树形列表 (基于vue3 + element-plus)
当节点展开时,显示展开的文件夹图标,当节点收起时显示收起的文件夹,最后一级显示文件样式
废话没有了, 代码如下
<!-- 树形列表组件 -->
<template>
<div class="tree-input" v-if="filter">
<el-input v-model="filterText" clearable placeholder="输入关键字" />
</div>
<el-tree :data="treeDataList" highlight-current :node-key="nodeKey"
:current-node-key="currentNodekey" :default-expanded-keys="defaultExpandedList"
ref="treeRef" icon="none" :filter-node-method="fliterNode" :expand-on-click-node="false" >
<template #default="{ data }">
<div class="com-tree">
<el-icon size="13" class="com-tree-icon" @click="toggleChild(data)" >
<Document v-if="data.isLast" />
<FolderOpened v-else-if="data.opened" />
<Folder v-else />
</el-icon>
<div class="com-tree-text" @click="handleNodeClick(data)" >{{ data[prop.label] }}</div>
</div>
</template>
</el-tree>
</template>
<script setup>
import { Folder, FolderOpened, Document } from "@element-plus/icons-vue"
import { compileTreeData } from '@/utils/compileTreeData.js'
const props = defineProps({
// 树内容
treeData: {
type: Array,
default: []
},
// 过滤器
filter: {
type: Boolean,
default: false
},
// 树形结构属性绑定值
prop: {
type: Object,
default: {
label: 'label'
}
},
// 树节点唯一绑定值 注意是唯一值 不是value值
nodeKey: {
type: String,
default: 'id'
},
// 当前选中的树节点 即nodeKey
currentNodekey: {
type: [String, Number],
default: ''
},
// 默认展开的树节点列表 即nodeKey组成的列表
defaultExpandedList: {
type: Array,
default: []
},
})
// 树dom
const treeRef = ref()
const treeDataList = ref([])
// 当有默认展开时,切换展开图标样式
const expandIcon = () => {
treeDataList.value.map(item => {
if (props.defaultExpandedList.includes(item[props.nodeKey])) {
item.opened = true
}
})
}
// 监听传入的树形结构, 并且进行转化
watch(() => props.treeData, newValue => {
if (newValue.length > 0) {
const treeDataTemp = [...newValue]
// 获得新的树形结构
treeDataList.value = compileTreeData(treeDataTemp)
// 如果传入了默认展开
if (props.defaultExpandedList.length > 0) {
expandIcon() // 切换展开图标样式
}
}
}, {
immediate: true
})
const emits = defineEmits(['node-click'])
// 点击图标
const toggleChild = (data) => {
let nodes = treeRef.value.store.nodesMap
// console.log('nodes', nodes)
for (let i in nodes) {
const item = nodes[i]
if (item.data[props.nodeKey] === data[props.nodeKey] && !item.data.isLast) {
data.opened = !data.opened // 图标切换
item.expanded = data.opened // 展开或收起子节点
}
}
}
const handleNodeClick = (data) => {
emits('node-click', data)
}
// 树过滤文本
const filterText = ref('')
watch(filterText, (val) => {
treeRef.value.filter(val)
})
// 树过滤
const fliterNode = (value, data) => {
if (!value) return true
return data.label.includes(value)
}
// 折叠全部节点
const setAllFold = () => {
let nodes = treeRef.value.store.nodesMap;
for (const node in nodes) {
nodes[node].expanded = false;
}
}
// 清除输入框 折叠所有节点
const clearInput = () => {
filterText.value = ''
setAllFold() // 折叠所有节点
}
defineExpose({
clearInput
})
</script>
<style lang="scss" scoped>
.tree-input {
padding: 0 24px 10px;
}
.com-tree {
display: flex;
align-items: center;
&-icon {
margin-right: 10px;
flex: 0 0 auto;
}
&-text {
flex: 1;
}
}
</style>
用到的JS
compileTreeData.js
let topId = '' // 最高级ID 考虑到点击很底层需要用到最高层id 而后端没有返回
const deepLoop = (treeData, isChild) => { // 一个递归
if (treeData && Array.isArray(treeData)) {
treeData.map(item => {
if (!isChild) { // 进入if 表示当前item为顶级
topId = item.id
}
item.opened = false // 默认全部都是收起状态
item.topId = topId // 不是顶级的 赋值顶级ID
if (item.children && item.children.length > 0) { // 有子元素
item.isLast = false // 不是最后一级
deepLoop(item.children, true) // 进行递归
}else { // 是最后一级
item.isLast = true
}
})
return treeData
}
}
export const compileTreeData = (treeData)=> {
return deepLoop(treeData)
}
还有一件事
有的时候文字很短,不好点击,需要点击文字右侧空白也能触发数据回传事件
那么你需要想办法 覆盖这个类 ---- .el-tree-node__label
覆盖后即可
/* 我的写法 */
.my-app .el-tree-node__label {
flex: 1;
}