这绝对是我最后一次写树状结构,我非常的确定一定以及肯定!上一篇文章总结了vue树状目录组件,忘记的可以点击传送门回顾一下:vue目录树组件
还是这个目录树,改版成表格树状结构。
我这里采用的UI库是Element,库自带了一种实现方式:
但是这种自带的实现方式,可扩展性很差,就很low,而且这样偷懒会让自己的技术止步不前,心里不安呀。
首先明确实现逻辑,表格树状结构中所谓的树状,并不是数据结构上的树状,只是布局上的效果而已,这里要求的数据结构是平级的数据列表。
还是上篇文章中说的,后端直接返回树状结构数据。
所以这里需要对数据处理一下,将树状结构的数据转换成平级的。getTreeList方法中调用接口先获取到后端数据。
getTreeList() {
this.treeList = [];
Catalog.getListByParent(this.catalogId).then((res) => {
if (res.status === 0) {
const treeList = res.data || [];
const tableList: ITree[] = [];
this.setCatalogList(tableList, treeList);
this.treeList = treeList;
this.tableList = tableList;
}
});
},
这里有个需要注意的点,接口返回的数据并没有直接赋给 this.treeList ,因为直接赋值之后 vue 不会一直监听数据的变化,而树状结构是需要在原有数据上修改的,例如 setCatalogList 方法中将树状结构数据转换成平级列表数据,所以定义个常量中间过渡一下,防止数据不实时更新。
setCatalogList(arr: ITree[], treeList: ITree[]) {
treeList.forEach((item) => {
arr.push(item);
if (item.catalogTreeVoList && item.catalogTreeVoList.length > 0) {
this.setCatalogList(arr, item.catalogTreeVoList);
}
});
},
因为树状列表中一些交互操作,如收起展开子节点,需要前端自定义一些字段属性用来控制,所以还需要将拿到的数据再构建一个map对象,更方面后面对自定义字段属性的操作。
将this.treeList 完善一下。
getTreeList() {
this.treeList = [];
Catalog.getListByParent(this.catalogId).then((res) => {
if (res.status === 0) {
const treeList = res.data || [];
const tableList: ITree[] = [];
const treeMap: ITreeMap = {};
this.setCatalogList(tableList, treeList);
this.setCatalogMap(treeMap, treeList, 0);
this.treeList = treeList;
this.tableList = tableList;
this.treeMap = treeMap;
}
});
},
setCatalogMap(catalogMap: ITreeMap , list: ITree[], level: number) {
list.forEach((item, index) => {
catalogMap[item.catalogId] = item;
item.level = level;
item.index = index;
if (item.catalogTreeVoList && item.catalogTreeVoList.length > 0) {
this.setCatalogMap(catalogMap, item.catalogTreeVoList, level + 1);
}
});
},
这时候打印一下treeList,tableList或者treeMap,会发现数据中已经添加了level和index字段。但这里暂时不用,后面其他操作的时候需要用的。
那对于树状列表最基础的收起展开操作,需要的字段是isOpen和isShow,我们这里规定isOpen控制展开收起图标的切换,isShow控制子节点的显示和隐藏。
这两个字段的添加还是在this.treeList 中,完整的this.treeList 代码如下:
getTreeList() {
this.treeList = [];
Catalog.getListByParent(this.catalogId).then((res) => {
if (res.status === 0) {
const treeList = res.data || [];
const tableList: ITree[] = [];
const treeMap: ITreeMap = {};
this.setCatalogList(tableList, treeList);
this.setCatalogMap(treeMap, treeList, 0);
Object.keys(treeMap).forEach((key) => {
const item = treeMap[key];
if (this.treeMap[key]) {
item.isOpen = this.treeMap[key].isOpen;
item.isShow = this.treeMap[key].isShow;
} else {
item.isOpen = true;
item.isShow = true;
}
});
this.treeList = treeList;
this.tableList = tableList;
this.treeMap = treeMap;
}
});
},
其实,应该先给出页面结构的代码:
<el-table
ref="table"
:data="tableList"
:row-class-name="cellClass"
>
<el-table-column label="名称">
<template slot-scope="scope">
<div :style="{'paddingLeft': 20 * scope.row.level + 'px'}">
<template v-if="scope.row.catalogTreeVoList.length>0">
<i class="iconfont icon-shouqi" v-if="scope.row.isOpen" @click="tabNode(scope.row)"></i>
<i class="iconfont icon-zhankai" v-if="!scope.row.isOpen" @click="tabNode(scope.row)"></i>
</template>
<template v-else>
<i class="lastTab"></i>
</template>
<div>{{scope.row.catalogName}}</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="500" align="center">
<template slot-scope="scope">
<span class="operate" @click="doNode(scope.row, 'up', scope.row.index)">上移</span>
<span class="operate" @click="doNode(scope.row, 'down', scope.row.index)">下移</span>
<span class="operate" @click="doNode(scope.row, 'edit', scope.row.index)">编辑</span>
<span class="operate" @click="doNode(scope.row, 'delete', scope.row.index)">删除</span>
</template>
</el-table-column>
</el-table>
展开收起操作的实现逻辑,就是修改isOpen的值,但是相应的判断当前节点的子节点的显示和隐藏。
当子节点存在的时候,如果是要收起当前节点,就很简单的不用管子节点的状态,全部隐藏就好。如果是要展开当前节点,isOpen为真所有子节点显示,否则隐藏。
tabNode(node: ITree) {
this.treeMap[node.catalogId].isOpen = !this.treeMap[node.catalogId].isOpen;
this.statusChange(node, node.isOpen);
},
statusChange(data: ITree, status: boolean) {
if (data.catalogTreeVoList) {
data.catalogTreeVoList.forEach((childEl: ITree) => {
if (status) {
if (data.isOpen) {
childEl.isShow = true;
} else {
childEl.isShow = false;
}
} else {
childEl.isShow = status;
}
if (childEl.catalogTreeVoList) {
this.statusChange(childEl, status);
}
});
}
},
isShow控制子节点显示或隐藏,是基于Element表格自带的一个方法,这里的类名rowNone就是display: none。
cellClass({ row, rowIndex }: ITableRow) {
if (!row.isShow) {
return "rowNone";
}
},
上篇文章结尾处提到的上下移排序的问题,这里需要做相应的改变,就是用到了前文写的index字段。
上移排序的逻辑,修改当前节点和上一个节点的下标值,就是经典的基础题:a=1,b=2,交换a,b的值,这就需要定义个中间变量过渡一下。最后会发现点击上移之后,打印出来的数据修改了,但是页面并没有更新。
这是因为index字段是在构建map对象时定义的,修改index的值只会让treeList更新,而此时排序需要修改的数据是tableList,所以最后还需要重新调用 setCatalogList ,将最新的treeList转换成tableList。
doUp(node: ICatalogModel, index: number) {
if (index === 0) {
return;
}
const parentId: string = node.catalogParent as string;
const parentItem: ICatalogModel = this.treeMap[parentId];
let dataList: ICatalogModel[] = [];
// 如果为空则是顶级
if (parentItem) {
if (parentItem.catalogTreeVoList) {
dataList = parentItem.catalogTreeVoList;
}
} else {
dataList = this.treeList;
}
const beforeItem = dataList[index - 1]; // a
const item = dataList[index]; // b
// 中间变量
let itemIndex: number = 0; // c
itemIndex = item.index; // c = b
item.index = beforeItem.index; // b = a
beforeItem.index = itemIndex; // a = c
dataList.splice(index, 1);
dataList.splice(index - 1, 0, item);
this.doSaveSort(dataList);
const tableList: ITree[] = [];
this.setCatalogList(tableList, this.treeList);
this.tableList = tableList;
},
相应的下移操作,修改的是当前节点和下一个节点的下标值,就不赘述了。
最后效果如下(样式没咋优化):