背景
同事在使用一个 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>