背景
同事在使用一个 vue 表格组件 vue-easytable
时遇到了点麻烦,现在项目中出现了一个需求就是要给表格增加树节点功能,让表格中的行可以展开与收缩,我们知道使用 element UI / ant-design-vue 中的表格是自带树节点功能的。但是老项目如果要更换组件的话已经是不大可能,于是需要帮忙加上树结构功能。
费话不多说,先上效果及源码
实现思路
其实比较简单,就是操作 Dom 来控制展示与隐藏相关的行,但需要后端数据的支持,数据展示的顺序必需严格按树的顺序展示出来。这里面有些地方要注意,就是表格(table
)中实现树跟传统的树可能有点不一样,控制起来要稍复杂一点。
比如,如果要点击一个树根,传统的树处理只对第一层儿子进行隐藏就可以了,因为儿子、孙子都在同一层的(如同一个 ul / li / div
)里面;但表格(table
)则不一样,所有儿子、孙子(row
)都在同一层,处理方式就有所不同,要通过 Dom 遍历查找,把所有的儿子、孙子(row
)都找出来,全部隐藏。
而显示儿子呢?传统的树直接就是把第一层儿子显示出来就可以了,孙子状态可以保持原来的状态,这体验是非常好的。但表格(table
)中的树呢?则不一样,把第一层的儿子(row
)显示出来就可以了,其他孙子旧的状态不能保持,如果要做保持也是可以的,处理逻辑则相对复杂。
关键代码
js 代码如下:
// 自定义列组件 Vue.component('table-operation', { template: `<span> <a href="" @click.stop.prevent="update(rowData,index)">编辑</a> <a href="" @click.stop.prevent="deleteRow(rowData,index)">删除</a> </span>`, props: { rowData: { type: Object }, field: { type: String }, index: { type: Number } }, methods: { update() { // 参数根据业务场景随意构造 let params = { type: 'edit', index: this.index, rowData: this.rowData }; this.$emit('on-custom-comp', params); }, deleteRow() { // 参数根据业务场景随意构造 let params = { type: 'delete', index: this.index }; this.$emit('on-custom-comp', params); } } }) new Vue({ el: '#app', data() { return { tableData: [ { "name": "赵伟-1", "tel": "151*****1987", "hobby": "钢琴、书法、唱歌", id: 1, pid: 0, isLeaf: 0, level: 0 }, { "name": "赵伟-1-1", "tel": "152*****1987", "hobby": "钢琴、书法、唱歌", id: 2, pid: 1, isLeaf: 0, level: 1 }, { "name": "赵伟-1-1-1", "tel": "154*****1987", "hobby": "钢琴、书法、唱歌", id: 4, pid: 2, isLeaf: 1, level: 2 }, { "name": "赵伟-1-1-2", "tel": "182*****1538", "hobby": "钢琴、书法、唱歌", id: 5, pid: 2, isLeaf: 1, level: 2 }, { "name": "赵伟-1-2", "tel": "153*****1987", "hobby": "钢琴、书法、唱歌", id: 3, pid: 1, isLeaf: 1, level: 1 }, { "name": "孙伟", "tel": "161*****0097", "hobby": "钢琴、书法、唱歌", id: 6, pid: 0, isLeaf: 1, level: 0 }, { "name": "周伟", "tel": "197*****1123", "hobby": "钢琴、书法、唱歌", id: 7, pid: 0, isLeaf: 1, level: 0 }, { "name": "吴伟", "tel": "183*****6678", "hobby": "钢琴、书法、唱歌", id: 8, pid: 0, isLeaf: 1, level: 0 } ], columns: [ { field: 'custome', title: '序号', width: 100, titleAlign: 'left', columnAlign: 'left', formatter: function (rowData, rowIndex, pagingIndex, field) { return `<b class="level level-${rowData.level}"></b>` + `<b class="no" data-pid="${rowData.pid}" data-id="${rowData.id}">${rowIndex + 1}</b>` + (rowData.isLeaf ? '' : `<i data-id="${rowData.id}" class="opt open"></i>`) } }, { field: 'name', title: '姓名', width: 100, titleAlign: 'center', columnAlign: 'center' }, { field: 'tel', title: '手机号码', width: 260, titleAlign: 'center', columnAlign: 'center' }, { field: 'hobby', title: '爱好', width: 330, titleAlign: 'center', columnAlign: 'center' }, { field: 'custome-adv', title: '操作', width: 200, titleAlign: 'center', columnAlign: 'center', componentName: 'table-operation', isResize: true } ] } }, mounted() { // dom 操作 const root = document.querySelector('#my-easy-table'); const optEls = [...root.querySelectorAll('.opt')]; optEls.forEach(el => { el.onclick = () => { if (el.classList.contains('open')) { el.classList.remove('open'); el.classList.add('close'); hideChildrens(el.getAttribute('data-id')); } else { el.classList.remove('close'); el.classList.add('open'); showChildrens(el.getAttribute('data-id')); } } }) function hideChildrens(pid) { // console.log(pid); const noEls = [...root.querySelectorAll(`.no[data-pid="${pid}"]`)]; if (noEls.length > 0) { noEls.forEach(el => { const rowEl = findParent(el, '.v-table-row'); if (rowEl) { rowEl.classList.add('hide'); const optEl = rowEl.querySelector('.opt'); if (optEl) { optEl.classList.remove('open'); optEl.classList.add('close'); } } hideChildrens(el.getAttribute('data-id')); }) } } function showChildrens(pid) { const noEls = [...root.querySelectorAll(`.no[data-pid="${pid}"]`)]; if (noEls.length > 0) { noEls.forEach(el => { const rowEl = findParent(el, '.v-table-row'); if (rowEl) { rowEl.classList.remove('hide'); } }) } } function findParent(el, selector, filter) { // console.log(el); const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; let pEl = el.parentElement; while (pEl && !matchesSelector.call(pEl, selector)) { if (!filter) { result.push(pEl); } else { if (matchesSelector.call(pEl, filter)) { result.push(pEl); } } pEl = pEl.parentElement; } if(pEl && matchesSelector.call(pEl, selector)) { return pEl; } return null; } }, methods: { customCompFunc(params) { console.log(params); if (params.type === 'delete') { // do delete operation this.$delete(this.tableData, params.index); } else if (params.type === 'edit') { // do edit operation alert(`行号:${params.index} 姓名:${params.rowData['name']}`); } } } })
css 代码如下:
.opt { font-style: normal; cursor: pointer; display: inline-block; } .close:after { content: "[+]"; margin-left: 10px; } .open:after { content: "[-]"; margin-left: 10px; } .level { display: inline-block; } .level-0 { padding-left: 0; } .level-1 { padding-left: 20px; } .level-2 { padding-left: 40px; } .level-3 { padding-left: 60px; } .hide { display: none !important; }
html 代码如下:
<html> <html> <head> <meta charset="UTF-8"> </head> <body> <div id="app"> <div id="my-easy-table"> <v-table is-horizontal-resize style="width:100%" :columns="columns" :table-data="tableData" row-hover-color="#eee" row-click-color="#edf7ff" @on-custom-comp="customCompFunc" ></v-table> </div> </div> </body> </html>