
在一些组件库中,状态管理并不是用vuex实现的,因为是组件,考虑到环境不可能指定存储库来进行存储,组件库基本是维护了自己的一个状态库,element-ui tree组件也是创建了自己状态管理组件,很简单就是一个store.js 文件,里面存放各种数据。本文来源于一次项目的功能开发,写文章旨在传递Tree组件的编写思想,因此选用了Vuex作为全局的状态管理。
上一篇 Vue递归组件+Vuex开发树形组件Tree--递归组件 已经完成了组件方面的创建,这一篇主要编写逻辑与数据更新存储。
撸代码:
store文件夹下新建index.js作为全局数据管理:
//index.js入口文件
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);
import data_store from './components/data_store.js';
import loading_store from './components/loading_store.js';
export default new vuex.Store({
modules: {
data_store: data_store,
loading_store: loading_store
}
})
项目中目前维护了两种状态一种是Tree数据,一种是公共的loading的状态,为了可拓展性,将index.js分离成modules的形式,每新增一个状态库只需要增加一条,而不需要频繁修改index.js的代码。
//store/modules/data_store.js
export default {
state:{
data:{}
},
mutations:{
set_data(state,data){
state.data=data
}
}
}
很简单,修改data与提交的操作。
仓库写好了,那么现在就开始交互的部分。
需求是,每点击一层,那么就去请求后台获取他下一层的数据,有数据则展开下拉。并且每一个node节点都有增删改的功能。好一样样来写:
开发前先写一个工具库,集中一些axios请求和工具函数。首先,想一下交互的思路,已知后台会返回每一节点的唯一id,点击这个节点的时候根据id向后台发送请求获取当前id下一层数据,当得到一个节点的数组的时候如何插入store中的data仓库?换句话说插入到data中的哪一层?删除也是一样,得到点击id了,那么删除data的哪一层节点?因为是数据驱动视图,所以我们只增删改data仓库,那么Dom就会触发相应的更新。因此需要一些递归函数来辅助操作。
递归添加与删除公共方法:
//新建utils.js
/*
* tree: tree 的数据,存放于vuex中
* data:需要插入的数据节点组成的对象。
*/
export function getData(tree, data) {
if (tree.id == data.id) {
tree.nodes = data.nodes;
} else {
for (let i in tree.nodes) {
if (tree.nodes[i].id == data.id) {
tree.nodes[i].nodes = data.nodes;
break;
} else {
getData(tree.nodes[i], data)
}
}
}
}
//删除数据 提供id删除对应的节点
export function deleteData(tree, id) {
if (tree.id == id) {
tree.nodes.splice(0, 1);
} else {
for (let i in tree.nodes) {
if (tree.nodes[i].id == id) {
console.log(typeof tree.nodes[i])
tree.nodes.splice(i, 1);
break;
} else {
deleteData(tree.nodes[i], id)
}
}
}
}
递归:传入id和对比的对象数组,首先对比根层级,如果id匹配执行相应的增删,如果不匹配则向下nodes[]中去查找,还不存在则递归查找。上面两个函数封装了后台获取数据增加与删除的函数,还需要封装自定义添加的函数,可以自定义前端添加数据,而不是从后台获取的数据,原理相同,只是增加一个数据模板。
//根据id添加新数据 template数据模板,可以根据模态框取值
export function addDataByID(arr, id, template) {
if (arr.id == id) {
arr.nodes.push(template)
} else {
for (let i in arr.nodes) {
if (arr.nodes[i].id == id) {
arr.nodes[i].nodes.push(template);
break;
} else {
addDataByID(arr.nodes[i], id, template)
}
}
}
}
函数比较简单就是一个递归函数,继续修改代码 :
//store/modules/data_store.js
import {getData, deleteData, addDataByID} from "common/utils.js";
export default {
state:{
data:{},
node:[]
},
mutations:{
set_data(state,data){
state.data=data
},
//点击节点展示下一级子节点
getNodesData(state, data) {
getData(state.data, data)
},
//删除子节点
delData(state, id) {
deleteData(state.data, id)
},
//添加子节点
addData(state, id, dataTemplate) {
addDataByID(state.data ,id, dataTemplate)
}
}
}
// TreeMenu.vue
<template>
//...
<template>
<script>
import { mapState, mapMutations } from "vuex";
//...
computed() {
...mapState({
treeData(state) { //vuex中的树的数据
return state.data_store.data;
}
})
},
methods: {
//...接上文
...mapMutations(['getNodesData','delData','addData']),
//添加loadTreeNode后台获取节点的函数
loadTreeNode(id) { //点击节点调用此函数,需要传递节点id
apiTreeAddress({ parentId: id })//封装的接口函数,返回节点列表
.then(res => {
const dataCache = { id: id, nodes: [] }; //根据id创建模拟的某一层级节点
for (let node of res.result.list) {
let data = {
id: node.id, //节点id
label: node.name, //节点lable
isLoad: false, //自定义flag,下文用到
nodes: []
};
dataCache.nodes.push(data); //处理数据后将节点装入nodes
}
/*递归对比dataCache.id与store内的各级别id,相同
*则插入对应id的nodes数组中成为下一层数据*/
this.getNodesData(dataCache);//提交到vuex
})
.catch(res => {
console.log("请求失败" + res);
});
}
}
</script>
在上篇文章预先写好的toggleChildren方法插入如下代码:
//节点点击事件
toggleChildren(event) {
this.showChildren = !this.showChildren;
let id = event.currentTarget.getAttribute("id");
this.loadTreeNode(id);
},
toggleChildren点击节点触发事件,获取到当前节点的id,然后调用loadTreeNode方法,此方法根据id向后台查询到当前id下的子节点,然后数据转化重新到新的数组,最后提交到vuex 调用其中的递归比对的函数,进行数据插入。
这样就为每一层node节点绑定了点击事件,点击获取数据显示。但是这只是点击事件,那么第一次加载页面的时候是没有根数据的啊,所以要在Tree.vue中写一个初始化的函数,初始加载根节点:
//Tree.vue
import { mapState, mapMutations } from "vuex";
...
methods: {
...mapMutations(['set_data']),
loadRootNode(id) {
apiTreeAddress({parentId:id}).then(res=>{
let list = res.result.list;
let data = {
id: list[0].id,
lable: list[0].name,
isLoad: false,
nodes:[]
},
this.set_data(data);
})
},
},
mounted() {
this.loadRootNode(0);
//后台约定,加载页面通过id = 0;取下一级节点,根据实际情况有所不同
}
这样一个树状菜单点击加载就做好了:

自定义添加与删除
//TreeMenu.vue
<template>
<div class="tree-menu">
<div :style="indent" @click="toggleChildren">{{label}}</div>
<div v-if="showChildren">
<tree-menu
v-for="(item, index) of nodes"
:key="index"
:nodes="node.nodes"
:label="node.label"
:depth="depth + 1"
></tree-menu>
</div>
<span class="edit-menu">
<i class="el-icon-plus" @click="add($event)" :id="id"></i> //添加按钮
<i class="el-icon-delete" @click="dele($event)" :id="id"></i> //删除按钮
</span>
</div>
</template>
<script>
data() {
return {
// ...
count: 1000
}
}
...
add(e) {
let id = e.currentTarget.getAttribute("id");
let dataTemplate = { id: this.count++, label: "xxx人民政府", nodes: [] };
this.addData(id, dataTemplate)//提交到vuex
},
dele(e) {
let id = e.currentTarget.getAttribute("id");
this.delData(id); //提交到vuex
},
</script>
dataTemplate是一个添加的数据模板,可自定义添加,this.count是定义的一个变量,保证每次id都不同。实际上添加和删除是要走后台的,上面写的这种是前端页面展示上的添加与删除,根据需求决定,但是思路和上面后台获取添加是一样的,点击传递id给后台,如果后台返回成功那么就执行自定义添加或者删除的代码就是了。现在是这样的效果:

一个基本的树状菜单就开发完成了,“改” 这个操作也很简单,说一下就不写了,点击修改将当前标题标签切换成input标签,输入完成再赋值给当前元素即可,也是要向后台传递的。还有一些可以优化的地方,现在每次点击都会发送请求,需要优化一下: 上文中后台返回数据后,有一层转化:
apiTreeAddress({parentId:id}).then(res=>{
let list = res.result.list;
let data = {
id: list[0].id,
lable: list[0].name,
isLoad: false, // 子节点是否加载标记
nodes:[]
},
this.set_data(data);
})
默认当前节点的isLoad字段为false,意义就是当前节点的子节点未加载:
utils添加函数:
//utils.js
//获取节点是否load
export function getLoadState(arr, id) {
if (arr.id == id) {
return arr.isLoad
} else {
for (let i in arr.nodes) {
if (arr.nodes[i].id == id) {
return arr.nodes[i].isLoad
break;
} else {
getLoadState(arr.nodes[i], id)
}
}
}
}
//设置节点Load
export function setLoadState(arr, id) {
if (arr.id == id) {
arr.isLoad = true;
} else {
for (let i in arr.nodes) {
if (arr.nodes[i].id == id) {
arr.nodes[i].isLoad = true;
break;
} else {
setLoadState(arr.nodes[i], id)
}
}
}
}
// store/modules/data_store.js
...
//添加设置节点状态的mutations
mutations:{
...
//设置节点状态
setDataLoad(state, id) {
addDataByID(state.data ,id)
}
}
然后在每次点击的时候判断当前id的isLoad是否为true,为真则就不取后台取子节点,简单的通过显示隐藏展示子节点即可(还记得showChildren字段么),如果为false,说明子节点没有获取过,也就是第一次点击当前节点,然后正常请求,请求回来后,设置当前节点isLoad true:
//TreeMenu.vue
//...接上文
...mapMutations(['getNodesData','delData','addData','setDataLoad']),//添加setDataLoad设置节点信息
//添加loadTreeNode后台获取节点的函数
loadTreeNode(id) {
if (getLoadState(treeData, id)) {
return;
}
apiTreeAddress({ parentId: id })
.then(res => {
var dataCache = { id: id, nodes: [] };
for (let node of res.result.list) {
let data = {
id: node.id,
label: node.name,
isLoad: false,
nodes: []
};
dataCache.nodes.push(data);
}
//数据返回成功,就设置已经Load
this.setDataLoad(id);
/*递归对比dataCache.id与store内的各级别id,相同
*则插入对应id的nodes数组中成为下一层数据*/
this.getNodesData(dataCache);//提交到vuex
})
.catch(res => {
console.log("请求失败" + res);
});
这样当第一次点击节点就会取数据,再次点击就不会进入到取数据。
现在树形插件已经开发完成了,需要根据上一篇来一起实现。
链接: Vue递归组件+Vuex开发树形组件Tree--递归组件
感谢观看!