tree组件的经验总结
最近项目中有了树形结构的功能需求,要求能够将分类信息使用树形结构展示、新增节点、删除节点、选择节点等功能,项目中使用了iview框架作为视图层框架,iview本身也提供了树形结构示例,但是其ui风格并不符合设计稿需求,没办法一开始的想法是请求外援,网上找到了jquery的著名插件ztree插件,其功能非常强大完全符合项目的功能性需求,但是由于项目是基于vue技术构建,考虑除非非常必要否则都不用jquery插件的原则,又找到了开源框架vue-ztree,它是基于ztree使用vue实现的树形结构,同样非常强大,但是ui风格依旧不符合设计稿,也没有看到可以自定义ui的地方,本着不能谈论别人的精彩,让自己永远是一个看客的技术原则,所以尝试开发了这个tree组件,由于技术浅薄所以该组件仅仅为了满足个人项目需要和自己的技术积累习惯以及给像我一样想实现自己tree组件的人一个参考写下了这篇文章。
该组件的开发环境为:vue-cli2.x脚手架工具创建的项目,iview3.x作为组件视图层框架,vue全家桶技术,stylus作为css解析器。其实该组件也就使用了iview3.x的icon图标而已,完全可以使用自己的icon来代替。先来谈下实现思路,一开始看到项目设计稿需求决定自己写时,就想到了它的html骨架结构,它应该是一个ul>li无序列表嵌套组合,并且是一个递归循环的结构,而vue本身就支持递归组件,戳我https://cn.vuejs.org/v2/guide/components-edge-cases.html,里面有递归组件的定义要求,至此html结构构思完成,下一步是侦听tree组件事件,如何展开收起,如何侦听选择节点的点击事件,如何新增、删除等等,在这个过程里一开始想到的vue里面的父子组件的事件传递机制,但是踩了一个坑,好像对于递归组件,外层父组件不能够像平常的方式去侦听,如下的方式:
#父组件
<div class="parent-tree">
<BasicTree @add="addNode"></BasicTree>
</div>
...addNode(target) {
console.log('监听tree组件',target);
}
#tree组件
<BasicTree>
<template>
.....this.$emit('add',event.target);
</template>
通过上述方式完全监听不到tree组件往外抛出的事件,我能想到的是因为是递归组件,本身就是自身引用,既是子组件又是自己的父组件,如果像这种方式去监听往外抛出的事件需要一层层往外抛出,非常麻烦,所以路过的大神对于这种情况还望能够详细描述这个事件的传递过程。
这种方式在我这里行不通,只能另想别的办法,还好vue提供了vuex作为全局状态管理机制,所以决定所有的事件和传值都通过vuex来进行全局注入,废话不多说直接贴源码
#父组件KnowledgeQuery组件
<template>
<div class="knowledge-query">
<!--左侧布局-->
<div class="left-wrapper">
<!--搜索框-->
<div class="search-wrapper">
<div class="input-wrapper">
<input type="text" v-model="keywordSearch"
@keyup.enter="searchKnowledge"
placeholder="请输入关键词搜索">
<span class="icon" @click="delKeywordSearch"><Icon type="ios-close"/></span>
<span class="search-btn" @click="searchKnowledge(1)">搜索</span>
</div>
<div class="result-wrapper" v-if="IsResultListShow">
<ul>
<li v-for="(item,index) in searchListData" class="item-list"
@click="choiceValue(item.result)" :key="index">
{{item.result}}
</li>
</ul>
</div>
</div>
<!--内容区-->
<div class="content-wrapper" @click="hideResultList">
<h1 class="title-hide" v-show="!keywordSearch.length">暂无搜索条件以下为默认搜索结果</h1>
<h1 class="title" v-show="keywordSearch.length">大约有<span
class="num">236</span>项与 “<span class="key">{{keywordSearch}}</span>” 相关的搜索结果</h1>
<ul>
<li v-for="(result,index) in resultListData" class="result-list"
@click="navictionUrl(1)"
:key="index">
<h1 class="title" v-html="result.title"></h1>
<div class="content">
<div class="img-wrapper" v-show="result.img.length">
<img :src="result.img" width="124" height="84">
</div>
<div class="text-wrapper">
<h1 class="top-wrapper">{{result.desc}}</h1>
<div class="bottom-wrapper">
<span class="time">{{result.time}}</span>
<span class="classify">{{result.classify}}</span>
<span class="user">{{result.reportUser}}</span>
</div>
</div>
</div>
</li>
</ul>
<div class="page">
<Page @on-change="searchKnowledge" :total="100" show-total/>
</div>
</div>
</div>
<!--右侧布局-->
<div class="right-wrapper" @click="hideResultList">
<!--添加知识库-->
<div class="add-wrapper">
<div class="btn-wrapper" @click="addKnowledge">
<span class="icon-knowledge"><Icon type="ios-cloud-upload-outline"/></span>
<span class="btn">添加知识库</span>
</div>
<div class="text">已有<span class="total">3166</span>份知识库资料</div>
<h1 class="nums">363</h1>
<p class="new-text">今日新增知识库</p>
</div>
<!--树形结构-->
<div class="tree-wrapper">
<div class="title-wrapper">
<span class="border"></span>
<span class="tree-title">知识库分类</span>
</div>
<div class="tree" ref="treeWrapper">
<BasicTree :treeList="treeList" :funcFlag="funcFlag"></BasicTree>
</div>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import axios from 'axios';
import { mapState, mapMutations } from 'vuex';
import BasicTree from 'base/tree/BasicTree.vue';
export default {
name: 'KnowledgeQuery',
data () {
return {
IsResultListShow: false, // 搜索列表显示隐藏的开关
searchListData: [
{result: '打印机卡纸在什么位置'},
{result: '打印机卡纸的解决办法'},
{result: '打印机卡纸了怎么把它拿出来'},
{result: '电脑没法联网'},
{result: '怎么清理电脑c盘'},
{result: '笔记本电脑屏保损坏'}
], // 存放搜索列表的数组
resultListData: [
{
title: '<font color="#f14f4f">打印机卡纸</font>',
img: 'https://gd4.alicdn.com/imgextra/i4/3275704363/TB2E2okfOFTMKJjSZFAXXckJpXa_!!3275704363.jpg',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
},
{
title: '针式<font color="#f14f4f">打印机卡纸</font>',
img: 'https://gd3.alicdn.com/imgextra/i3/246349854/TB2jqJVcXmWBuNjSspdXXbugXXa_!!246349854.jpg',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
},
{
title: '<font color="#f14f4f">打印机卡纸</font>进纸器损坏',
img: 'https://gd3.alicdn.com/imgextra/i3/246349854/TB2H9D4dkfb_uJkSmLyXXcxoXXa_!!246349854.jpg',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
},
{
title: '喷墨<font color="#f14f4f">打印机卡纸</font>',
img: 'https://gd1.alicdn.com/imgextra/i1/246349854/TB2xl4ZlDnI8KJjy0FfXXcdoVXa_!!246349854.png',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
},
{
title: '激光<font color="#f14f4f">打印机卡纸</font>',
img: 'https://gd1.alicdn.com/imgextra/i1/246349854/TB2dAnTgZnI8KJjSsziXXb8QpXa_!!246349854.jpg',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
},
{
title: '<font color="#f14f4f">打印机卡纸</font>如何修理',
img: 'https://gd2.alicdn.com/imgextra/i3/0/O1CN01uMwFND1W6xhSOpi3t_!!0-item_pic.jpg',
desc: '打印机不能打印大都是因为进出纸部件损坏,就是滚纸那个轴;纸路传感器故障,驱动部件失灵,还有可能是同时进了多张纸甚至纸张的质量差也会导致卡纸等等',
time: '2018年12月16号',
classify: '打印机卡纸',
reportUser: '张云飞'
}
], // 存放搜索结果列表的数组展示内容
funcFlag: true // 是否开启tree组件的节点增加删除功能
};
},
computed: {
...mapState([
'treeList' // treeList tree组件的数据源
]),
// 搜索关键词的计算属性的get和set方法
keywordSearch: {
get: function () {
return this.$store.state.treeNodeVal;
},
set: function (newValue) {
this.setTreeValue(newValue);
}
}
},
methods: {
...mapMutations([
'setTreeList', // 请求接口设置tree组件的数据源
'setTreeValue', // 设置store中搜索关键词treeNodeVal的值
'delTreeValue' // 删除同上
]),
/**
* 将中文转为unicode编码
* @param s 中文字符串参数
*/
toUnicode (s) {
return s.replace(/([\u4E00-\u9FA5]|[\uFE30-\uFFA0])/g, function (newStr) {
return '\\u' + newStr.charCodeAt(0).toString(16);
});
},
showResultList () {
this.IsResultListShow = true;
},
hideResultList () {
this.IsResultListShow = false;
},
delKeywordSearch () {
this.delTreeValue();
},
/**
* 调用获取搜索关键词列表接口
*/
haveSearchList () {
console.log('调用获取搜索关键词列表接口得到searchListData的值');
},
/**
* 调用搜索结果列表接口
* @param num 页码
*/
searchKnowledge (num) {
if (this.keywordSearch) {
num = !num ? 1 : num;
console.log('调用搜索结果列表接口', num);
}
},
choiceValue (result) {
this.setTreeValue(result);
},
addKnowledge () {
console.log('跳转到增加知识库界面');
},
/**
* 打开新页面窗口
* @param num 传递的参数id
*/
navictionUrl (num) {
let routeUrl = this.$router.resolve({
path: '/test',
query: {id: num}
});
window.open(routeUrl.href, '_blank');
}
},
watch: {
'keywordSearch' () {
if (this.keywordSearch) {
this.haveSearchList();
// 模拟搜索列表的出现效果,关键点中文正则匹配
let unicodeVal = this.toUnicode(this.keywordSearch);
let searchArrays = [];
this.searchListData.forEach((val) => {
if (new RegExp(`(${unicodeVal})`).test(val.result)) {
searchArrays.push(val);
}
});
console.log(searchArrays);
if (searchArrays.length > 1) {
this.showResultList();
} else {
this.hideResultList();
}
this.searchKnowledge();
}
}
},
created () {
let vm = this;
axios.get('/api/treelist').then(res => {
if (res.data.errno === 1) {
vm.setTreeList(res.data.data);
}
});
},
components: {
BasicTree
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus" scoped>
@import "~common/stylus/variable.styl";
.knowledge-query
display: flex
margin: 16px 24px
.left-wrapper
flex: 1
margin-right: 16px
.search-wrapper
position: relative
margin-bottom: 16px
.input-wrapper
display: flex
position: relative
font-size: $font-size-medium-x
color: $color-text-input
input
flex: 1
height: 56px
line-height: 56px
padding-left: 24px
border-top: 1.5px solid $color-blue
border-left: 1.5px solid $color-blue
border-bottom: 1.5px solid $color-blue
.icon
position: absolute
top: 12px
right: 22% // 设置百分比能够保证图标始终与右侧保持同等距离不会随着分辨率变化而出现错位
font-size: 30px
.search-btn
display: inline-block
flex: 0 0 20%
width: 20%
height: 57px
line-height: 57px
text-align: center
color: $color-text-white
background-color: $color-blue
.result-wrapper
position: absolute
top: 58px
left: 0
width: 80%
z-index: 100
font-size: $font-size-medium
background-color: $color-text-white
box-shadow: 2px 2px 2px 0 rgba(100, 100, 100, 0.1)
.item-list
height: 36px
line-height: 36px
padding-left: 24px
&:hover
background-color: #f0f0f0
.content-wrapper
position: relative
padding: 32px 24px 16px
border-radius: 8px
background: $color-text-white
.title, .title-hide
margin-bottom: 18px
line-height: 14px
font-size: $font-size-small
color: $color-text-input
.num
color: $color-blue
.key
color: $color-red
.result-list
padding: 16px 0
border-bottom: 1px dashed $color-background-t
&:first-child
padding-top: 0
&:last-child
padding-bottom: 30px
border: none
.title
display: inline-block
margin-bottom: 12px
padding-bottom: 2px
line-height: 18px
font-size: $font-size-medium-x
color: $color-text-title
border-bottom: 1px solid $color-text-title
.content
display: flex
font-size: 0
.img-wrapper
flex: 0 0 124px
width: 124px
height: 84px
margin-right: 16px
.text-wrapper
flex: 1
font-size: $font-size-small-x
.top-wrapper
margin: 2px 0 10px
line-height: 20px
color: $color-text-content
.bottom-wrapper
color: $color-text-input
.classify
display: inline-block
margin: 0 18px
.page
font-size: $font-size-medium
.right-wrapper
flex: 0 0 360px
.add-wrapper
padding: 24px 26px
text-align: center
font-size: $font-size-medium
border-radius: 8px
background-color: $color-text-white
.btn-wrapper
margin: 0 10px 16px
height: 56px
line-height: 56px
font-size: 0
color: $color-text-white
background: $color-orange
.icon-knowledge
display: inline-block
width: 20px
height: 20px
margin-right: 6px
font-size: $font-size-large
.btn
line-height: 18px
font-size: $font-size-medium-x
.text
padding-bottom: 16px
line-height: 18px
color: $color-text-input
border-bottom: 1px solid $color-background-t
.total
font-size: $font-size-large
color: $color-orange
.nums
margin: 18px 0 12px
font-size: 36px
color: $color-purple
.new-text
color: $color-text-input
.tree-wrapper
margin-top: 16px
border-radius: 8px
background: $color-text-white
.title-wrapper
height: 58px
line-height: 58px
font-size: 0
border-bottom: 1px solid $color-background-t
.border
display: inline-block
position: relative
top: 6px
width: 4px
height: 22px
background: $color-blue
.tree-title
display: inline-block
padding-left: 24px
font-size: $font-size-medium
color: $color-text-title
.tree
width: 360px
height: 772px
overflow: auto
padding-left: 10px
padding-bottom: 10px
box-sizing: border-box
&::-webkit-scrollbar
width: 2px
height: 2px
&::-webkit-scrollbar-thumb
border-radius: 5px
-webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2)
background: $color-background-t
&::-webkit-scrollbar-track
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2)
border-radius: 0
background: $color-text-white
</style>
#tree组件BasicTree
<template>
<div class="basic-tree">
<ul style="padding-left: 16px;">
<template v-for="item in treeList">
<li v-if="item.childs==null &&item.treeNode" :key="item.treeNode.id">
<div @click="handleValue(item.treeNode.name,item.treeNode.id)"
:class="{'current': currentIndex === item.treeNode.id}">
<!--<Checkbox v-model="item.treeNode.name"></Checkbox>-->
<span class="parent-icon"><Icon :type="item.treeNode.icon"/></span>
<span>{{item.treeNode.name}}</span>
<div class="funcs" v-show="funcFlag">
<span @click.stop.prevent="addTreeNode(item.treeNode.id)">
<Icon type="ios-add"/>
</span>
<span @click.stop.prevent="substructTreeNode(item.treeNode.id,$event)">
<Icon type="ios-close"/>
</span>
</div>
</div>
</li>
<li v-if="item.childs&&item.treeNode" :key="item.id" :id='"tree"+item.treeNode.id'>
<div class="parentli" @click="toggleTreeNodeList(item.treeNode.id)">
<!--<Checkbox v-model="item.treeNode.name"></Checkbox>-->
<span class="parent-icon"><Icon :type="item.treeNode.icon"/></span>
<span>{{item.treeNode.name}}</span>
<div class="funcs" v-show="funcFlag">
<span @click.stop.prevent="addTreeNode(item.treeNode.id)">
<Icon type="ios-add"/>
</span>
</div>
</div>
<BasicTree :treeList="item.childs" :funcFlag="funcFlag"></BasicTree>
</li>
</template>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
import { mapState, mapMutations } from 'vuex';
export default {
name: 'BasicTree',
props: {
/**
* 接收tree组件数据源
*/
treeList: {
type: Array,
default () {
return [];
}
},
/**
* 是否开启节点新增删除功能
*/
funcFlag: {
type: Boolean,
default: false
}
},
computed: {
...mapState([
'currentIndex', // 当前选中的属性节点索引
'treeFlag', // 控制tree中的节点展开和收起
'treeNodeValue' // tree新增的节点值
])
},
methods: {
...mapMutations([
'setTreeValue', // 设置store中搜索关键词treeNodeVal的值
'setCurrentIndex', // 设置选中的属性节点索引值
'setTreeFlag', // 设置tree中的节点开关标识值
'addStoreTreeNode', // 增加新节点
'delStoreTreeNode', // 删除节点
'setTreeNodeValue' // 设置新增节点的值
]),
/**
* 设置搜索关键词的值和当前选中的索引
* @param val 关键词的值
* @param id 索引
*/
handleValue (val, id) {
this.setTreeValue(val);
this.setCurrentIndex(id);
},
/**
* 展开/收起节点
* @param num 节点id值
*/
toggleTreeNodeList (num) {
let dom = document.getElementById('tree' + num);
let subDom = dom.childNodes;
if (this.treeFlag) {
subDom[2].style.display = 'none';
this.setTreeFlag();
} else {
subDom[2].style.display = '';
this.setTreeFlag();
}
},
/**
* 新增节点
* @param num 节点id值
*/
addTreeNode (num) {
let vm = this;
this.$Modal.confirm({
render: (h) => {
return h('Input', {
props: {
value: this.treeNodeValue,
autofocus: true,
placeholder: '请输入节点名称'
},
on: {
input: (val) => {
vm.setTreeNodeValue(val);
}
}
});
},
onOk () {
if (vm.treeNodeValue === '' || vm.treeNodeValue === null) {
vm.$Message.error('节点值不能为空');
} else {
let obj = [];
obj.push({id: num, name: vm.treeNodeValue});
vm.addStoreTreeNode(obj);
}
}
});
},
/**
* 删除节点
* @param num 节点id值
* @param event 当前点击事件对象
*/
substructTreeNode (num, event) {
let currentParentNode = event.target.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
let currentParentId = currentParentNode.getAttribute('id');
if (new RegExp(`(tree)`).test(currentParentId)) {
currentParentId = currentParentId.replace(RegExp.$1, '');
}
let finalParentId = parseInt(currentParentId);
let numObj = {};
numObj.num = num;
numObj.finalParentId = finalParentId;
this.delStoreTreeNode(numObj);
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus" scoped>
@import "~common/stylus/variable.styl";
.basic-tree
width: 100%
position: relative
font-size: $font-size-small
.parent-icon
display: inline-block
margin-left: 4px
font-size: $font-size-large
color: $color-blue
.parentli
height: 30px
line-height: 30px
font-size: $font-size-small-x
background: $color-background-s
.funcs
display: inline-block
margin-left: 10px
font-size: 0
span
display: inline-block
width: 10px
height: 10px
z-index: 100
font-size: 20px
&:nth-child(1)
color: $color-blue
&:nth-child(2)
color: $color-red
ul
& > li
padding-top: 6px
position: relative
left: 0
cursor: pointer
li
&::before
border-left: 1px dashed $color-text-input
&::after
border-top: 1px dashed $color-text-input
li
div
.funcs
display: inline-block
margin-left: 10px
font-size: 0
span
display: inline-block
width: 10px
height: 10px
margin-right: 10px
z-index: 100
font-size: 20px
&:nth-child(1)
color: $color-blue
&:nth-child(2)
color: $color-red
&.current
color: $color-text-white
background: $color-blue
.parent-icon
color: $color-text-white
.funcs
span
color: $color-text-white
&::before, &::after
position: absolute
left: -14px
right: auto
content: ''
&::before
top: 0
bottom: 50px
width: 1px
height: 100%
&::after
width: 14px
top: 25px
&:last-child
&::before
height: 25px
ul > li
padding-top: 10px
position: relative
left: 10px
</style>
// mutations.js
.....
[type.SET_TREELIST] (state, val) {
state.treeList = val;
},
[type.SET_TREEVALUE] (state, val) {
state.treeNodeVal = val;
},
[type.DEL_TREEVALUE] (state) {
state.treeNodeVal = '';
},
[type.SET_CURRENTINDEX] (state, val) {
state.currentIndex = val;
},
[type.SET_TREEFlAG] (state) {
state.treeFlag = !state.treeFlag;
},
[type.ADDSTORE_TREENODE] (state, obj) {
let num = obj[0].id;
let val = obj[0].name;
let rel = util.getTreeListObjById(state.treeList, num);
let treeTime = util.getTimeRandom();
let finalTime = parseInt(treeTime);
let treeObj = [];
treeObj.push({
treeNode: {
id: finalTime,
icon: 'ios-document',
name: val
},
childs: null
});
if (rel.childs === null) {
rel.treeNode.icon = 'ios-book';
rel.childs = [];
}
rel.childs.push(treeObj[0]);
},
[type.DELSTORE_TREENODE] (state, obj) {
let parentObj = util.getTreeListObjById(state.treeList, obj.finalParentId);
let parentChildArrays = parentObj.childs;
for (let i in parentChildArrays) {
if (parentChildArrays[i].treeNode.id === obj.num) {
parentChildArrays.splice(i, 1);
}
}
if (!parentChildArrays.length) {
parentObj.treeNode.icon = 'ios-document';
parentObj.childs = null;
}
},
[type.SET_TREENODEVALUE] (state, val) {
state.treeNodeValue = val;
}
// util.js
....
/**
* 递归遍历树形结构
* @param trees 数据源
* @param id 节点id值
* @returns {*}
*/
util.getTreeListObjById = function (trees, id) {
if (!id || !trees || !trees.length) {
return null;
}
let treeObj = null;
for (let item of trees) {
if (item.treeNode.id === id) {
return item;
}
treeObj = util.getTreeListObjById(item.childs, id);
if (treeObj) {
return treeObj;
}
}
return null;
};
/**
* 获取当前时间跟上随机数由此生成永不重复的数据
* @returns {string}
*/
util.getTimeRandom = function () {
const now = new Date();
let month = now.getMonth() + 1;
let day = now.getDate();
let hour = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
let time = now.getFullYear().toString() + month.toString() + day + hour + minutes + seconds + (Math.round(Math.random() * 89 + 100)).toString();
return time;
};
// type.js
// 设置tree结构数据
export const SET_TREELIST = 'setTreeList';
// 设置知识库搜索框的值
export const SET_TREEVALUE = 'setTreeValue';
// 删除知识库搜索框的值
export const DEL_TREEVALUE = 'delTreeValue';
// 设置tree索引
export const SET_CURRENTINDEX = 'setCurrentIndex';
// 改变tree节点开关
export const SET_TREEFlAG = 'setTreeFlag';
// 增加tree节点
export const ADDSTORE_TREENODE = 'addStoreTreeNode';
// 删除tree节点
export const DELSTORE_TREENODE = 'delStoreTreeNode';
// 设置增加节点的值
export const SET_TREENODEVALUE = 'setTreeNodeValue';
// 模拟后台树形结构数据
{
"tree": [
{
"treeNode": {
"id": 1,
"icon": "ios-book",
"name": "业务知识"
},
"childs": [
{
"treeNode": {
"id": 10,
"icon": "ios-book",
"name": "业务知识节点1"
},
"childs": [
{
"treeNode": {
"id": 101,
"icon": "ios-book",
"name": "层级三101"
},
"childs": [
{
"treeNode": {
"id": 1011,
"icon": "ios-document",
"name": "层级四1011"
},
"childs": null
},
{
"treeNode": {
"id": 1012,
"icon": "ios-document",
"name": "层级四1012"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 102,
"icon": "ios-document",
"name": "层级三102"
},
"childs": null
},
{
"treeNode": {
"id": 103,
"icon": "ios-document",
"name": "层级三103"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 11,
"icon": "ios-book",
"name": "业务知识节点2"
},
"childs": [
{
"treeNode": {
"id": 111,
"icon": "ios-book",
"name": "层级三111"
},
"childs": [
{
"treeNode": {
"id": 1111,
"icon": "ios-document",
"name": "层级四1111"
},
"childs": null
},
{
"treeNode": {
"id": 1112,
"icon": "ios-document",
"name": "层级四1112"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 112,
"icon": "ios-document",
"name": "层级三112"
},
"childs": null
},
{
"treeNode": {
"id": 113,
"icon": "ios-document",
"name": "层级三113"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 12,
"icon": "ios-book",
"name": "业务知识节点3"
},
"childs": [
{
"treeNode": {
"id": 121,
"icon": "ios-document",
"name": "层级三121"
},
"childs": null
},
{
"treeNode": {
"id": 122,
"icon": "ios-document",
"name": "层级三122"
},
"childs": null
},
{
"treeNode": {
"id": 123,
"icon": "ios-document",
"name": "层级三123"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 13,
"icon": "ios-book",
"name": "业务知识节点4"
},
"childs": [
{
"treeNode": {
"id": 131,
"icon": "ios-document",
"name": "层级三131"
},
"childs": null
},
{
"treeNode": {
"id": 132,
"icon": "ios-document",
"name": "层级三132"
},
"childs": null
},
{
"treeNode": {
"id": 133,
"icon": "ios-document",
"name": "层级三133"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 14,
"icon": "ios-book",
"name": "业务知识节点5"
},
"childs": [
{
"treeNode": {
"id": 141,
"icon": "ios-document",
"name": "层级三141"
},
"childs": null
},
{
"treeNode": {
"id": 142,
"icon": "ios-document",
"name": "层级三142"
},
"childs": null
},
{
"treeNode": {
"id": 143,
"icon": "ios-document",
"name": "层级三143"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 15,
"icon": "ios-book",
"name": "业务知识节点6"
},
"childs": [
{
"treeNode": {
"id": 151,
"icon": "ios-document",
"name": "层级三111"
},
"childs": null
},
{
"treeNode": {
"id": 152,
"icon": "ios-document",
"name": "层级三112"
},
"childs": null
},
{
"treeNode": {
"id": 153,
"icon": "ios-document",
"name": "层级三113"
},
"childs": null
}
]
}
]
},
{
"treeNode": {
"id": 2,
"icon": "ios-book",
"name": "技术知识"
},
"childs": [
{
"treeNode": {
"id": 20,
"icon": "ios-book",
"name": "技术知识节点1"
},
"childs": [
{
"treeNode": {
"id": 201,
"icon": "ios-document",
"name": "层级三201"
},
"childs": null
},
{
"treeNode": {
"id": 202,
"icon": "ios-document",
"name": "层级三202"
},
"childs": null
},
{
"treeNode": {
"id": 203,
"icon": "ios-document",
"name": "层级三203"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 21,
"icon": "ios-book",
"name": "技术知识节点2"
},
"childs": [
{
"treeNode": {
"id": 211,
"icon": "ios-document",
"name": "层级三211"
},
"childs": null
},
{
"treeNode": {
"id": 212,
"icon": "ios-document",
"name": "层级三112"
},
"childs": null
},
{
"treeNode": {
"id": 213,
"icon": "ios-document",
"name": "层级三113"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 22,
"icon": "ios-book",
"name": "技术知识节点3"
},
"childs": [
{
"treeNode": {
"id": 221,
"icon": "ios-document",
"name": "层级三221"
},
"childs": null
},
{
"treeNode": {
"id": 222,
"icon": "ios-document",
"name": "层级三222"
},
"childs": null
},
{
"treeNode": {
"id": 223,
"icon": "ios-document",
"name": "层级三223"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 23,
"icon": "ios-book",
"name": "技术知识节点4"
},
"childs": [
{
"treeNode": {
"id": 231,
"icon": "ios-document",
"name": "层级三231"
},
"childs": null
},
{
"treeNode": {
"id": 232,
"icon": "ios-document",
"name": "层级三232"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 24,
"icon": "ios-book",
"name": "技术知识节点5"
},
"childs": [
{
"treeNode": {
"id": 241,
"icon": "ios-document",
"name": "层级三241"
},
"childs": null
}
]
},
{
"treeNode": {
"id": 25,
"icon": "ios-book",
"name": "技术知识节点6"
},
"childs": [
{
"treeNode": {
"id": 251,
"icon": "ios-document",
"name": "层级三251"
},
"childs": null
},
{
"treeNode": {
"id": 252,
"icon": "ios-document",
"name": "层级三252"
},
"childs": null
}
]
}
]
}
],
"success": 1
}
新增节点会在当前选择的父节点下新增子节点,并且原来没有下一级的节点成具有下层级的节点,可以无限层级添加删除节点
如果想在自己的项目中使用,请自己仔细阅读源码,注释已经写的很清楚了,注意后台传过来的树形结构中必须有id为数值类型,并且永不重复,新增节点已经保证了永不重复。
该组件还有很多功能有待完善和优化,比如多层级节点复选框选择,节点上移下移,后面有时间再来慢慢完善,期望能写出一个最终可以开源的tree框架,目前源码可以自由复制修改,但必须写好引用博客地址,多谢!