提要
最近项目中需要用到树形表格来描述部门、区域之间的父子展开关系。但是已经在项目中使用的Vue的成熟组件ElementUI以及iViewUI组件都没有提供相应的树形表格组件,无奈找了其他替代方案也都被pass掉了,只能从改造现有组件放面着手。
在网上也找到了一些实践案例:http://blog.csdn.net/s8460049/article/details/61414751
第一种方案
第一种方案就是原作者介绍的,即将具有层级关系的数据进行提前处理。比如: 数据结构为:
[ { id: 1, parentId: 0, name: '测试1', age: 18, sex: '男', children: [ { id: 2, parentId: 1, name: '测试2', age: 22, sex: '男' } ] }, { id: 3, parentId: 0, name: '测试3', age: 23, sex: '女', children: [ { id: 4, parentId: 3, name: '测试4', age: 22, sex: '男' }, { id: 5, parentId: 3, name: '测试5', age: 25, sex: '男' }, { id: 6, parentId: 3, name: '测试6', age: 26, sex: '女', children: [ { id: 7, parentId: 6, name: '测试7', age: 27, sex: '男' } ] } ] }, { id: 18, parentId: 0, name: '测试8', age: 18, sex: '男' } ]
这样可以通过数据转换方法,把每一条数据从它的父级中取出来,把树形结构数据转换成数组数据。
dataTranslate.js内容:
import Vue from 'vue' function DataTransfer (data) { if (!(this instanceof DataTransfer)) { return new DataTransfer(data, null, null) } } DataTransfer.treeToArray = function (data, parent, level, expandedAll) { let tmp = [] Array.from(data).forEach(function (record) { if (record._expanded === undefined) { Vue.set(record, '_expanded', expandedAll) } if (parent) { Vue.set(record, '_parent', parent) } let _level = 0 if (level !== undefined && level !== null) { _level = level + 1 } Vue.set(record, '_level', _level) tmp.push(record) if (record.children && record.children.length > 0) { let children = DataTransfer.treeToArray(record.children, record, _level, expandedAll) tmp = tmp.concat(children) } }) return tmp } export default DataTransfer
有了进行数据转换的方法之后,开始正式些数据TreeGrid.vue组件:
<template> <el-table :data="data" border style=" 100%" :row-style="showTr"> <el-table-column v-for="(column, index) in columns" :key="column.dataIndex" :label="column.text"> <template scope="scope"> <span v-if="spaceIconShow(index)" v-for="(space, levelIndex) in scope.row._level" class="ms-tree-space"></span> <button class="button is-outlined is-primary is-small" v-if="toggleIconShow(index,scope.row)" @click="toggle(scope.$index)"> <i v-if="!scope.row._expanded" class="el-icon-caret-right" aria-hidden="true"></i> <i v-if="scope.row._expanded" class="el-icon-caret-bottom" aria-hidden="true"></i> </button> <span v-else-if="index===0" class="ms-tree-space"></span> {{scope.row[column.dataIndex]}} </template> </el-table-column> <el-table-column label="操作" v-if="treeType === 'normal'" width="260"> <template scope="scope"> <button type="button" class="el-button el-button--default el-button--small"> <router-link :to="{ path: requestUrl + 'edit', query: {id: scope.row.Oid} }" tag="span"> 编辑 </router-link> </button> <el-button size="small" type="danger" @click="handleDelete()"> 删除 </el-button> <button type="button" class="el-button el-button--success el-button--small"> <router-link :to="{ path: requestUrl, query: {parentId: scope.row.parentOId} }" tag="span"> 添加下级树结构 </router-link> </button> </template> </el-table-column> </el-table> </template> <script> import DataTransfer from '../utils/dataTranslate.js' import Vue from 'vue' export default { name: 'tree-grid', props: { // 该属性是确认父组件传过来的数据是否已经是树形结构了,如果是,则不需要进行树形格式化 treeStructure: { type: Boolean, default: function () { return false } }, // 这是相应的字段展示 columns: { type: Array, default: function () { return [] } }, // 这是数据源 dataSource: { type: Array, default: function () { return [] } }, // 这个作用是根据自己需求来的,比如在操作中涉及相关按钮编辑,删除等,需要向服务端发送请求,则可以把url传过来 requestUrl: { type: String, default: function () { return '' } }, // 这个是是否展示操作列 treeType: { type: String, default: function () { return 'normal' } }, // 是否默认展开所有树 defaultExpandAll: { type: Boolean, default: function () { return false } } }, data () { return {} }, computed: { // 格式化数据源 data: function () { let me = this if (me.treeStructure) { let data = DataTransfer.treeToArray(me.dataSource, null, null, me.defaultExpandAll) console.log(data) return data } return me.dataSource } }, methods: { // 显示行 showTr: function (row, index) { let show = (row._parent ? (row._parent._expanded && row._parent._show) : true) row._show = show return show ? '' : 'display:none;' }, // 展开下级 toggle: function (trIndex) { let me = this let record = me.data[trIndex] record._expanded = !record._expanded }, // 显示层级关系的空格和图标 spaceIconShow (index) { let me = this if (me.treeStructure && index === 0) { return true } return false }, // 点击展开和关闭的时候,图标的切换 toggleIconShow (index, record) { let me = this if (me.treeStructure && index === 0 && record.children && record.children.length > 0) { return true } return false }, handleDelete () { this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'error' }).then(() => { this.$message({ type: 'success', message: '删除成功!' }) }).catch(() => { this.$message({ type: 'info', message: '已取消删除' }) }) } } } </script> <style scoped> .ms-tree-space{position: relative; top: 1px; display: inline-block; font-family: 'Glyphicons Halflings'; font-style: normal; font-weight: 400; line-height: 1; 18px; height: 14px;} .ms-tree-space::before{content: ""} table td{ line-height: 26px; } </style>
写好了树形表格组件,使用方式和普通的Vue组件使用方法相同:
<template> <div class="hello"> <tree-grid :columns="columns" :tree-structure="true" :data-source="dataSource"></tree-grid> </div> </template> <script> import {TreeGrid} from './TreeGrid' export default { name: 'hello', data () { return { columns: [ { text: '姓名', dataIndex: 'name' }, { text: '年龄', dataIndex: 'age' }, { text: '性别', dataIndex: 'sex' } ], dataSource: [ { id: 1, parentId: 0, name: '测试1', age: 18, sex: '男', children: [ { id: 2, parentId: 1, name: '测试2', age: 22, sex: '男' } ] }, { id: 3, parentId: 0, name: '测试3', age: 23, sex: '女', children: [ { id: 4, parentId: 3, name: '测试4', age: 22, sex: '男' }, { id: 5, parentId: 3, name: '测试5', age: 25, sex: '男' }, { id: 6, parentId: 3, name: '测试6', age: 26, sex: '女', children: [ { id: 7, parentId: 6, name: '测试7', age: 27, sex: '男' } ] } ] }, { id: 18, parentId: 0, name: '测试8', age: 18, sex: '男' } ] } }, components: { TreeGrid } } </script>
以上就是实现树形表格的方法,提前把树形表格数据处理成数组数据,然后针对父级和子集分别增加不同的表示,以实现不同的表格首行显示效果。
但是,该组件将说有数据都加在到Table中,然后采用操作css样式"display:none"的方式进行隐藏和显示。数量比较时候倒是可以完全满足使用,但是如果数据量超过100条或者更多就会出现页面卡顿的现象。
第二种方式
第二种方式在原方法的基础上进行操作,原理就是基于MVVM框架vue的数据驱动原理。采用操作数据的方式执行子级数据的显示隐藏。
根据ElementUI 的 Table组件会根据数据变化进行加载的原理,通过只传入需要展示的数据的方式,来提高浏览器渲染的速度。
直接上代码TreeGrid.vue:
<template> <div class="table-content"> <el-table :data="TableDate" border style=" 18.82rem"> <el-table-column label="部门名称" min-width="400"> <template scope="scope"> <span v-for="(space, levelIndex) in scope.row._level" :key="levelIndex" class="ms-tree-space"></span> <span class="button is-outlined is-primary is-small" v-if="toggleIconShow(scope.row)" @click="toggle(scope.row)"> <i v-if="!scope.row._expanded" class="el-icon-arrow-right" aria-hidden="true"></i> <i v-if="scope.row._expanded" class="el-icon-arrow-down" aria-hidden="true"></i> </span> <span v-else class="ms-tree-space"></span> <span :title="scope.row.dpmName"> {{ scope.row.dpmName }} </span> </template> </el-table-column> <el-table-column label="组织机构代码" min-width="300"> <template scope="scope"> <span :title="scope.row.dpmAdc"> {{ scope.row.dpmAdc }} </span> </template> </el-table-column> <el-table-column label="所属地区" min-width="300"> <template scope="scope"> <span :title="scope.row.areaName"> {{ scope.row.areaName }} </span> </template> </el-table-column> <el-table-column label="上级部门" min-width="315"> <template scope="scope"> <span :title="scope.row.parentName"> {{ scope.row.parentName }} </span> </template> </el-table-column> </el-table> </div> </template> <script> import {deepCopy} from "../utils/util.js" Array.prototype.removeByValue = function(val) { //对数组原型添加删除指定项的方法 for(var i=0; i<this.length; i++) { if(this[i] == val) { this.splice(i, 1); break; } } }; export default { name: 'TreeGrid', components: { }, data(){ return { TableDate:[] } }, computed:{ allData(){ let me = this; let newData = deepCopy(me.$store.getters.Data); return newData; } }, watch: { allData(val){ this.TableDate = deepCopy(val); } }, methods: { toggleIconShow (record) { /** * 点击展开和关闭的时候,图标的切换 */ let me = this; if (record.children && record.children.length > 0) { return true } return false }, toggle(rowData) { let me = this; /** * 展开下级 */ let childLen = rowData.children.length; if(rowData._expanded){ let dataArr=[]; dataArr.push(rowData); let arr = me.getChildFlowId(dataArr,[]); for(let i=0; i < childLen; i++){ me.TableDate.map((value)=>{ if(arr.indexOf(value.parentId) > -1){ me.TableDate.removeByValue(value); } }); } } else { rowData.children = me.setSpaceIcon(rowData.children,rowData._level); let index = me.TableDate.indexOf(rowData); let pre = me.TableDate.slice(0,index+1); let last = me.TableDate.slice(index+1); let concatChildren = pre.concat(rowData.children); me.TableDate = concatChildren.concat(last); } rowData._expanded = !rowData._expanded; }, getChildFlowId(data,emptyArr){ // 获取子级的flowId let me = this; Array.from(data).forEach((record)=>{ emptyArr.push(record.flowId); if(record.children&&record.children.length > 0){ let childFlowIdArr = me.getChildFlowId(record.children,emptyArr); emptyArr.concat(childFlowIdArr); } }); return emptyArr; }, setSpaceIcon(data,level){ // 设置第一列的空格和方向按钮 let me = this; let _level = 0; data.forEach((value)=>{ value._expanded = false; if(level !== undefined && level !== null){ _level = level + 1; } else { _level = 1; } value._level = _level; if(value.children&&value.children.length > 0){ me.setSpaceIcon(value.children, _level); } }); return data; } } } </script>
虽然上了大段的代码,不过也有很多不细致的地方。重点还是理解实现的方式,尽量减少需要写个方法或者组件的时候,在网上Google一下就拿过来用,更多的应该是理解其中的原理,并且自己进行实践才能进步。
不明白的地方,欢迎提问交流!