给 vue-easytable 表格增加树节点功能

背景

同事在使用一个 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>&nbsp;
    <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>

作者: 博主

Talk is cheap, show me the code!

发表评论

邮箱地址不会被公开。

Captcha Code