依赖的库 Element
创建组件
创建文件 cus-tree-node.vue , 这是一个节点组件
<template>
<div class="cus-tree-node">
<div class="cus-tree-entry">
<div class="cus-tree-main" @click="nodeHandle(node, $event)">
<div>
<i
v-if="(node.child && node.child.length > 0) || node.pid == 0"
class="cus-tree-icon"
:class="{ 'el-icon-folder': !nodeOpenTag, 'el-icon-folder-opened': nodeOpenTag }"
></i>
<i v-if="(!node.child || node.child.length <= 0) && node.pid != 0" class="cus-tree-icon el-icon-document"></i>
</div>
<div>
<div v-show="nodeEditTag" @click.stop>
<el-input size="mini" v-model="node.title"></el-input>
</div>
<div v-show="!nodeEditTag" class="cus-tree-txt">
<div v-if="node.child && node.child.length > 0" style="font-weight: bolder">
{{ node.title }}
</div>
<div v-else-if="!node.child || node.child.length <= 0">
{{ node.title }}
</div>
</div>
</div>
</div>
<!-- 编辑栏 -->
<div class="tree-handle-tool">
<i v-show="nodeEditTag && node.title.trim().length > 0" class="el-icon-check" @click="submitHandle"></i>
<i v-show="nodeEditTag" class="el-icon-close" @click="closeHandle"></i>
<i v-show="!nodeEditTag" class="el-icon-plus" @click="nodeCreate"></i>
<i v-show="!nodeEditTag" class="el-icon-edit-outline" @click="nodeEdit"></i>
<i v-show="!nodeEditTag" class="el-icon-delete" @click="nodeDelete"></i>
</div>
</div>
<!-- 递归轮询出下一级的树节点 -->
<el-collapse-transition>
<div v-show="nodeOpenTag" class="cus-child-container" v-if="node.child && node.child.length > 0">
<el-collapse-transition v-for="child in node.child" :key="child.id">
<div class="cus-child-node" v-show="nodeOpenTag">
<cusTreeNode :node="child" :treeData="treeData" :addHanlde="addHanlde" :delHandle="delHandle" :editHandle="editHandle"></cusTreeNode>
</div>
</el-collapse-transition>
</div>
</el-collapse-transition>
</div>
</template>
<script>
/**
* @description 树形节点
* @param {Object} node 节点的数据
* @param {Boolean} allOpen 子节点全部打开
* @param {Array} treeData 树型组件的数据
* @param {Boolean} noLine 开启树形的线条
*
* @event {Function()} addHanlde 添加的操作
* @event {Function()} delHandle 删除的操作
* @event {Function()} editHandle 修改的操作
*/
export default {
name: 'cusTreeNode',
props: {
node: {
type: Object,
default: {},
},
allOpen: {
type: Boolean,
default: true,
},
treeData: {
type: Array,
default: [],
},
noLine: {
type: Boolean,
default: false,
},
addHanlde: {
type: Function,
default: () => {},
},
delHandle: {
type: Function,
default: () => {},
},
editHandle: {
type: Function,
default: () => {},
},
},
data: () => {
return {
nodeOpenTag: false, // 节点打开标志
nodeEditTag: false, // 节点编辑标志
oldNodeTitle: '',
}
},
methods: {
inputHandle() {},
// 子级节点的展开操作
nodeHandle(node, e) {
this.nodeOpenTag = !this.nodeOpenTag
},
// 节点创建
nodeCreate() {
let treeArr = this.treeToArray(this.treeData)
let addNode = treeArr.filter((t) => t.id == '')
if (addNode && addNode[0]) {
return this.$message('已有未确定的添加')
}
this.recQueryHandle(this.treeData, 'create')
},
// 节点删除
nodeDelete() {
this.delHandle(this.node)
},
// 节点编辑
nodeEdit() {
this.nodeEditTag = true
this.oldNodeTitle = this.node.title
},
// 提交操作
submitHandle() {
this.recQueryHandle(this.treeData, 'submit')
},
// 关闭操作
closeHandle() {
this.nodeEditTag = false
this.recQueryHandle(this.treeData, 'close')
},
// 对自己节点进行递归操作
recQueryHandle(treeData, tag) {
// 当前操作的节点
let curNode = this.node
treeData.forEach((item) => {
if (item.id === curNode.id) {
// 添加元素
if (tag === 'create') {
// 先查询是否已有追加的元素
if (!curNode.child) {
this.$set(item, 'child', [])
}
// 查询当前同层新增且未提交的节点
let addTag = item.child.filter((t) => t.id == '')
if (addTag.length > 0) {
this.$message('已有未确定的添加')
return
}
// 添加节点
item.child.push({ title: '新建子节点', id: '', edit: true, pid: item.id })
}
// 删除元素
else if (tag === 'del') {
let index = treeData.findIndex((t) => t.id == curNode.id)
treeData.splice(index, 1)
}
// 编辑元素
else if (tag === 'edit') {
this.oldNodeTitle = this.node.title
}
// 节点的提交事件
else if (tag == 'submit') {
// id为空为添加数据
if (curNode.id == '') {
var existIndex = item.title.indexOf('/')
if (existIndex != -1) return this.$message('不允许存在 / 字符存在')
this.addHanlde(curNode)
} else {
// id 不为空为编辑数据
if (item.title === this.oldNodeTitle) return (this.nodeEditTag = false)
// 特殊字符查询
var existIndex = item.title.indexOf('/')
if (existIndex != -1) return this.$message('不允许存在 / 字符存在')
this.editHandle(curNode)
}
}
// 节点的修改或添加状态的关闭状态
else if (tag == 'close') {
// id 若为空则删除
if (curNode.id == '') {
treeData.pop()
} else {
item.title = this.oldNodeTitle
}
}
return
}
if (item.child) {
this.recQueryHandle(item.child, tag)
}
})
},
// 获取扁平化的数据
treeToArray(tree) {
var res = []
for (const item of tree) {
const { child, ...i } = item
if (child && child.length) {
res = res.concat(this.treeToArray(child))
}
res.push(i)
}
return res
},
layerTips(title) {},
},
watch: {
node() {
this.nodeEditTag = this.node.edit
},
},
created() {
this.nodeOpenTag = this.allOpen
this.nodeEditTag = this.node.edit
},
mounted() {
this.oldNodeTitle = this.node.title
},
}
</script>
<style lang="scss" scoped>
/* 树节点 */
.cus-tree-node {
line-height: 30px;
padding-left: 23px;
position: relative;
}
.cus-tree-node:not(:last-child) > .cus-child-container::after {
content: '';
border-left: 1px dashed #ccc;
position: absolute;
left: 30.5px;
top: 29.5px;
height: calc(100% - 30px);
}
/* 子节点的连线 */
.cus-child-node:not(:last-child) > ::after {
content: '';
border-left: 1px dashed #ccc;
position: absolute;
left: 7.5px;
top: 0;
height: 100%;
}
.cus-child-node:last-child > ::after {
content: '';
border-left: 1px dashed #ccc;
position: absolute;
left: 7.5px;
top: -1px;
height: 16px;
}
.cus-child-node .cus-tree-icon::after {
content: '';
border-top: 1px dashed #ccc;
width: 60%;
position: absolute;
top: 7.2px;
left: -14px;
}
/* 单个书节点的显示主体 */
.cus-tree-entry {
height: 30px;
display: flex;
line-height: 30px;
cursor: pointer;
}
.cus-tree-entry:hover .tree-handle-tool {
line-height: 30px;
height: 30px;
display: block;
}
.cus-tree-main {
display: flex;
font-size: 15px;
div:first-child {
padding-right: 8px;
}
}
/* 树的操作栏工具 */
.tree-handle-tool {
display: none;
padding-left: 10px;
i {
padding: 0 5px;
font-weight: 9;
}
i:hover {
opacity: 0.5;
}
}
.cus-tree-icon {
background-color: #ffffff;
color: #ffb800;
z-index: 2;
position: relative;
}
/* 树节点的标题 */
.cus-tree-txt:hover {
text-decoration: underline;
color: #a8a8a8;
}
</style>
创建一个节点的容器组件 cus-tree.vue
<template>
<div class="cus-tree">
<template v-for="(node, index) in tree_data">
<cus-tree-node
:node="node"
:noLine="false"
:treeData="tree_data"
:allOpen="allOpen"
:addHanlde="addHanlde"
:delHandle="delHandle"
:editHandle="editHandle"
:key="index"
>
</cus-tree-node>
</template>
</div>
</template>
<script>
/**
* @description 自定义树形组件
* @param {Array} treeData 树型组件的数据
* @param {Boolean} allOpen 子节点全部打开
*
* @event {Function()} addHanlde 添加的操作
* @event {Function()} delHandle 删除的操作
* @event {Function()} editHandle 修改的操作
*/
import cusTreeNode from './cus-tree-node.vue'
export default {
name: 'cusTree',
props: {
treeData: {
type: Array,
default: [],
},
allOpen: {
type: Boolean,
default: true,
},
},
components: {
cusTreeNode,
},
data: () => {
return {
tree_data: [],
}
},
methods: {
addHanlde(node) {
this.$emit('addHanlde', node)
},
delHandle(node) {
this.$emit('delHandle', node)
},
editHandle(node) {
this.$emit('editHandle', node)
},
// 强制刷新(解决数据添加的时候无法更新的问题)
//(这里我也很纳闷,监听不起作用,只能出此下策)
forceFlush() {
this.tree_data = JSON.parse(JSON.stringify(this.treeData))
},
},
watch: {
treeData: {
handler() {
this.tree_data = JSON.parse(JSON.stringify(this.treeData))
console.log('节点修改监听')
},
deep: true,
},
},
created() {
console.log(this.treeData)
this.tree_data = JSON.parse(JSON.stringify(this.treeData))
},
}
</script>
<style lang="scss" scoped>
.cus-tree {
box-sizing: border-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
组件的引用案例
<template>
<div class="index">
<cusTree ref="cusTree" :treeData="treeData"
@addHanlde="addHanlde"
@editHandle="editHandle"
@delHandle="delHandle">
</cusTree>
</div>
</template>
<script>
import cusTree from '@/components/cus-tree/cus-tree.vue'
export default {
data() {
return {
isShowTag: 'cusTree',
treeData:[{
"id": 1,
"title": "测试库(系统测试专用库)",
"pid": 0,
"child": [
{
"id": 11,
"title": "新建子节热帖点111",
"pid": 1,
}
]
},
{
"id": 2,
"title": "测试库(系统测试专用库)",
"pid": 0,
"child": [{
"id": 21,
"title": "新建子节热帖点111",
"pid": 2,
}]
}]
}
},
components: {
cusTree,
},
methods:{
// 添加确认的回调
addHanlde(node){
// 这里是后端传递过来的id
node.id = new Date().getSeconds()
console.log(node);
this.recQueryHandle(this.treeData,node,"add")
this.$refs.cusTree.forceFlush();
},
// 编辑的回调
editHandle(node){
// .. 发送后端请求
this.recQueryHandle(this.treeData,node,"edit")
this.$refs.cusTree.forceFlush();
},
// 删除
delHandle(node){
this.recQueryHandle(this.treeData,node,"del")
this.$refs.cusTree.forceFlush();
},
recQueryHandle (treeData, curNode, tag) {
// 当前操作的节点
treeData.forEach((item) => {
if (tag == 'add' && item.id == curNode.pid) {
console.log('item.child', item)
if (!item.child) {
item.child = []
//this.$set(item, 'child', [])
}
curNode.edit = false
item.child.push(curNode)
}
if (item.id === curNode.id) {
if (tag == 'edit') {
item.edit = false;
item.title = curNode.title
} else if (tag === 'del') {
let index = treeData.findIndex((t) => t.id == curNode.id)
treeData.splice(index, 1)
}
}
if (item.child && item.child.length > 0) {
this.recQueryHandle(item.child, curNode, tag)
}
})
}
}
}
</script>
<style>
.cus-component {
margin-top: 50px;
margin-bottom: 50px;
}
.cus-component span {
margin: 0 5px;
cursor: pointer;
font-weight: 600;
}
.check-item {
color: blueviolet;
}
</style>
最终效果图
本人非专业前端人员,样式可能有点陋,有需求的兄弟,还是自己修改 cus-tree-node 文件中的样式