前言
为了满足项目的需要,我开发了一个文件夹树结构。使用了基于 Element Plus 的 el-tree 来设计这个文件夹树,使其支持节点的增删改操作。
效果图
目录结构
Project
|-- src
|- |-- components
|- |- |-- FolderTree
|- |- |- |-- src
|- |- |- |- |-- FolderTree.vue
|- |- |- |-- index.tx
上代码
index.ts
import FolderTree from './src/FolderTree.vue'
interface Tree {
id?: string
label: string
name: string
isAddNode?: boolean
children?: Tree[]
parentId: string
}
export { FolderTree, Tree }
FolderTree.vue
<script setup lang="ts">
import { defineComponent, nextTick, PropType, ref, useAttrs, watch } from 'vue'
import {
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElInput,
ElMessage,
ElMessageBox,
ElTree
} from 'element-plus'
import { Icon } from '@/components/Icon'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { set } from 'lodash-es'
import { Tree } from '@/components/FolderTree'
import { useI18n } from '@/hooks/web/useI18n'
import { useIcon } from '@/hooks/web/useIcon'
import { isEmptyVal } from '@/utils/is'
const { t } = useI18n()
const attrs = useAttrs()
defineComponent({
name: 'FolderTree',
inheritAttrs: false
})
const props = defineProps({
dataList: {
type: Array as PropType<any[]>,
default: () => []
}
})
const nodeAdd = ref()
const dropdown = ref()
const treeEl = ref<ComponentRef<typeof ElTree>>()
const emit = defineEmits(['submit', 'remove'])
const treeData = ref<any[]>([
{
id: '0',
name: '全部',
children: props.dataList || [],
isAddNode: false,
parentId: ''
}
])
// 添加子节点
const addChildNode = (data: Tree) => {
const newNode: Tree = {
label: '',
children: [],
name: '',
isAddNode: true,
parentId: data.id?.toString() as string
}
if (!data.children) {
data.children = []
}
data.children.push(newNode)
nextTick(() => {
nodeAdd.value.focus()
})
set(treeData.value, 'value', treeData.value)
}
const addNode = (node: Node, data: Tree) => {
if (data.isAddNode) return // 如果已经是添加状态,则不再执行
addChildNode(data) // 添加子节点
// 自动展开当前节点
if (node) {
node.expanded = true
}
}
const editNode = (data: Tree) => {
if (data.id === '0') return
data.isAddNode = true
nextTick(() => {
nodeAdd.value.focus()
})
set(treeData.value, 'value', treeData.value)
}
const handleInputEnter = (node: Node, data: Tree) => {
data.isAddNode = false // 取消添加节点状态
if (data.label.trim() !== '') {
// 确保节点名称不为空
data.label = data.label.trim() // 将节点标签替换为输入的内容
emit('submit', data)
} else {
cleanNode(node, data)
}
set(treeData.value, 'value', treeData.value)
}
const handleInputEsc = (node: Node, data: Tree) => {
data.isAddNode = false
if (isEmptyVal(node.label)) {
cleanNode(node, data)
}
}
const cleanNode = (node: Node, data: Tree) => {
const parent = node.parent
if (parent) {
const children = parent.data.children || parent.data
const index = children.findIndex((d: Tree) => d.id === data.id)
children.splice(index, 1)
}
}
const delNode = (node: Node, data: Tree) => {
if (data.id === '0') return
ElMessageBox.confirm('确定删除当前节点吗?', '提示', {
confirmButtonText: t('common.delOk'),
cancelButtonText: t('common.delCancel'),
type: 'warning'
}).then(() => {
if (node.childNodes && node.childNodes.length > 0) {
ElMessage.warning(`【${node.label}】下存在子节点,无法删除`)
return
}
cleanNode(node, data)
set(treeData.value, 'value', treeData.value)
emit('remove', data)
})
}
watch(
() => props.dataList,
(newValue) => {
treeData.value[0].children = newValue
},
{
deep: true,
immediate: true
}
)
defineExpose({
treeEl
})
</script>
<template>
<div>
<ElTree
v-bind="attrs"
ref="treeEl"
:data="treeData"
:highlight-current="true"
:default-expand-all="true"
:expand-on-click-node="false"
node-key="id"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>
<span>
<Icon icon="twemoji:file-folder" class="mr-5px" :size="15" />
</span>
<span v-if="!data.isAddNode">{{ node.label }}</span>
<span>
<ElInput
ref="nodeAdd"
@blur="
() => {
handleInputEsc(node, data)
}
"
v-model="data.label"
v-if="data.isAddNode"
class="add-new-child-node"
clearable
@keyup.esc="
() => {
handleInputEsc(node, data)
}
"
@keyup.enter="() => handleInputEnter(node, data)"
/>
</span>
</span>
<span class="button-group">
<ElDropdown ref="dropdown" trigger="contextmenu">
<template #default>
<Icon :size="16" class="cursor-pointer" icon="basil:other-1-outline" />
</template>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
@click="addNode(node, data)"
:icon="useIcon({ icon: 'subway:add', size: 14, color: '#67C23A' })"
>
添加
</ElDropdownItem>
<ElDropdownItem
v-if="data.id !== '0'"
@click="editNode(data)"
:icon="useIcon({ icon: 'system-uicons:write', size: 14 })"
>
编辑
</ElDropdownItem>
<ElDropdownItem
v-if="data.id !== '0'"
@click="delNode(node, data)"
:icon="useIcon({ icon: 'subway:subtraction', size: 14, color: 'red' })"
>
删除
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</span>
</span>
</template>
</ElTree>
</div>
</template>
<style scoped lang="less">
.add-new-child-node {
z-index: 20;
padding-left: 0;
width: calc(100% - 30px);
:deep(.el-input__wrapper) {
background: #fff;
height: 18px;
}
:deep(.el-input__inner) {
background: #fff;
height: 18px;
}
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 1px;
}
.button-group {
display: flex;
align-items: center;
gap: 5px; /* 间距大小 */
}
</style>
使用方式
<script setup lang="ts">
import { FolderTree, Tree } from '@/components/FolderTree'
import { ref } from 'vue'
const treeData = ref([{ id: '1', name: 'Test', children: [], isAddNode: false }])
const currentNodeKey = ref('')
/**
* 删除节点操作
* @param data
*/
const removeNode = async (data: Tree) => {
if (!data.id) return
console.log(data)
}
/**
* 删除新增修改操作
* @param data
*/
const submitNode = async (data: Tree) => {
console.log(data)
}
/**
* 节点点击
* @param data
*/
const currentChange = (data: Tree) => {
console.log(data)
}
</script>
<template>
<div class="w-300px">
<ElCard>
<FolderTree
ref="folderRef"
:props="{
label: 'name'
}"
:dataList="treeData"
:current-node-key="currentNodeKey"
@remove="removeNode"
@submit="submitNode"
@current-change="currentChange"
/>
</ElCard>
</div>
</template>
<style lang="less"></style>
注意
如果需要用到原生的方法,调用方式
// 例如
folderRef.value?.treeEl?.setCurrentKey()
folderRef.value?.treeEl?.filter
总结
在这里做一个记录,我对 el-tree 进行了二次封装,完全暴露了原生 el-tree 的所有属性和方法。我自定义了两个方法:submit 和 remove,用于处理节点的增加、删除和修改操作。如果大家有其他的建议或想法,也欢迎大家提出并指点一下!