Vue手搓轮子系列——表格封装(二)

概述

本文在Vue手搓轮子系列——表格封装(一) 的基础上增加了排序、展开功能。主要通过中间件实现(如果一层中间件不够,就再加一层)。

代码

LiteTabel.vue

<template>
    <div class="table">
        <table cellspacing="0" class="lite-table" :class="border ? 'is-border' : ''">
            <thead>
                <tr>
                    <lite-table-column-warp column-type="header" :sort-status="sortstatus" @sort="sort">
                        <slot />
                    </lite-table-column-warp>
                </tr>
            </thead>
            <tbody>
                <lite-table-row ref="table" :row-class-name="rowClassName" :data="tableData" :default-expand-all="defaultExpandAll"  :row-key="rowKey" :expand-row-keys="expandRowKeys">
                    <slot />
                </lite-table-row>
                <!-- <tr v-for="(row, index) in data" :key="row">
                    <lite-table-column-warp column-type="body" :row="row" :index="index">
                        <slot />
                    </lite-table-column-warp>
                </tr> -->
            </tbody>
        </table>
        <div v-if="!tableData.length" :class="border ? 'lite-empty is-border' : ''">
            {{emptyText}}
        </div>
    </div>
</template>

<script>

import LiteTableColumnWarp  from './LiteTableColumnWarp.vue';
import LiteTableRow from './LiteTableRow.vue';
import { getDeepObjectValue } from '../utils';

export default {
    name: 'LiteTable',
    components: {
        LiteTableColumnWarp,
        LiteTableRow
    },
    data() {
        return {
            sortstatus:{ // 排序状态
                prop: '', // 排序key
                order: '' // 正序or倒序
            }
        }
    },
    methods: {
        sort(prop, order){
            this.sortstatus = {
                prop,
                order
            }
        },
        toggleRowExpansion(row, expanded) {
            this.$refs.table.toggleRowExpansion(row, expanded);
        }
    },
    props: {
        data: {
            type: Array,
            required: true
        },
        rowKey: {
            type: String,
            default: ''
        },
        expandRowKeys: {
            type: Array,
            default: () => []
        },
        defaultExpandAll: {
            type: Boolean,
            default: false
        },
        rowClassName: {
            type: Function,
            default: () => ''
        },
        border: {
            type: Boolean,
            default: false
        },
        emptyText: {
            type: String,
            default: '暂无数据'
        }
    },
    computed: {
        tableData() {
            try{
                if(this.sortstatus.order == 'ascending'){
                    let data = this.data.map(item => item);
                    data.sort((a, b) => {
                        let aVal = getDeepObjectValue(a, this.sortstatus.prop);
                        let bVal = getDeepObjectValue(b, this.sortstatus.prop);
                        if(aVal > bVal){
                            return 1;
                        }else if(aVal < bVal){
                            return -1;
                        }else{
                            return 0;
                        }
                    });
                    return data;
                }else if(this.sortstatus.order == 'descending'){
                    let data = this.data.map(item => item);
                    data.sort((a, b) => {
                        let aVal = getDeepObjectValue(a, this.sortstatus.prop);
                        let bVal = getDeepObjectValue(b, this.sortstatus.prop);
                        if(aVal > bVal){
                            return -1;
                        }else if(aVal < bVal){
                            return 1;
                        }else{
                            return 0;
                        }
                    });
                    return data;
                }else{
                    return this.data;
                }
            }catch(e) {
                return this.data;
            }
        }
    }
}

</script>

<style>

.lite-table{
    border-spacing: 0;
    border-collapse: collapse;
    width: 100%;
    table-layout: fixed;
}

.lite-table th, .lite-table td{
    padding-top: 12px;
    padding-bottom: 12px;
}

.is-border th, .is-border td{
    border: 1px solid #ebeef5;
}

.lite-empty{
    padding-top:12px; 
    padding-bottom:12px; 
    text-align:center;
    color: var(--el-color-text-secondary); /*element-plus 颜色,请按需修改*/
}

.lite-empty.is-border{
    border-left: 1px solid #ebeef5;
    border-right: 1px solid #ebeef5;
    border-bottom: 1px solid #ebeef5;   
}

</style>

在这里重新增加了一个vue组件中间层lite-table-row。主要原因是,将行展开以后,是一种递归的结构,而直接将lite-table进行递归会出现table的表头重复的问题,因此这里重新增加一个lite-table-row中间件。

LiteTableRow.vue

<template>
    <template v-for="(row, index) in data" :key="row">
        <tr :class="rowClassName(row)">
            <lite-table-column-warp :space="space" :is-expand="isExpanded(row, index)" column-type="body" :row="row" :index="index" @expand="expand_index[index] = !expand_index[index]">
                <slot/>
            </lite-table-column-warp>
        </tr>
        <LiteTableRow :space="space+1" :row-class-name="rowClassName" ref="table" v-if="hasChildren(row) && isExpanded(row, index)" :default-expand-all="defaultExpandAll" :data="row.children" :row-key="rowKey" :expand-row-keys="expandRowKeys" >
            <slot/>
        </LiteTableRow>
    </template>
</template>

<script>
    
    import LiteTableColumnWarp from './LiteTableColumnWarp.vue';

    export default {
        components: {
            LiteTableColumnWarp
        },
        name: 'LiteTableRow',
        data() {
            return {
                expand_index:[],
            }
        },
        props: {
            data: {
                type: Array,
                required: true
            },
            rowKey: {
                type: String,
                default: ''
            },
            expandRowKeys: {
                type: Array,
                default: () => []
            },
            defaultExpandAll: {
                type: Boolean,
                default: false
            },
            rowClassName:{
                type: Function,
                default: () => ''
            },
            space: {
                type: Number,
                default: 0
            }
        },
        methods: {
            hasChildren(row) {
                return row.children && row.children.length > 0;
            },
            isExpanded(row, index) {
                // if (this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0) {
                //     let ret = this.expand_index[index] && 
                //     (this.expandRowKeys.some(item => item==row[this.rowKey]) ||
                //     row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey])));
                //     return ret;
                // }
                return this.expand_index[index];
            },
            toggleRowExpansion(row, expanded) {
                let index = this.data.indexOf(row);
                if(index != -1){
                    if (expanded !== undefined) {
                        this.expand_index[index] = expanded;
                    } else {
                        this.expand_index[index] = !this.expand_index[index];
                    }
                    return;
                }
                this.data.forEach((item, i)=>{
                    if(item.children && item.children.length > 0){
                        if(item.children.indexOf(row) != -1){
                            this.expand_index[i] = true;
                        }
                    }
                })
                this.$nextTick(()=>{
                    if(this.$refs.table){
                        if(Array.isArray(this.$refs.table)){
                            this.$refs.table.forEach(item => {
                                item.toggleRowExpansion(row, expanded);
                            });
                        }else{
                            this.$refs.table.toggleRowExpansion(row, expanded);
                        }
                    }
                })
            }
        },
        watch: {
            defaultExpandAll(val){
                if(val){
                    this.expand_index = this.data.map(() => true);
                }else{
                    this.expand_index = this.data.map((row)=>{
                        if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
                            return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
                            row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
                        }
                        return false;
                    });
                }
            },
            data:{
                handler(val){
                    if(this.defaultExpandAll){
                        this.expand_index = val.map(() => true);
                    }else{
                        this.expand_index = val.map((row)=>{
                            if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
                                return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
                                row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
                            }
                            return false;
                        });
                    }
                },
                deep: true
            }
        },
        mounted() {
            if(this.defaultExpandAll){
                this.expand_index = this.data.map(() => true);
            }
            else{
                this.expand_index = this.data.map((row)=>{
                    if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
                        return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
                        row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
                    }
                    return false;
                });
            }
        }
    }

</script>

这里通过expand_index数组与是否存在子节点数据(children)保证是否将行展开。
此外,通过递归来实现子节点。由于树/森林本身就是递归的,因此,这里的子节点也可以用树/森林来进行抽象,从而将该组件进行递归即可获得带有展开行的表格。

LiteTableColumnWarp.vue

<template>
    <slot/>
</template>

<script>

    export default {
        name: 'LiteTableColumnWarp',
        props: {
            columnType:{
                type: String,
                default: '',
            },
            row:{
                type: Object,
                default: () => {},
            },
            index:{
                type: Number,
                default: 0,
            },
            isExpand:{
                type: Boolean,
                default: false,
            },
            sortStatus:{
                type: Object,
                default: () => {},
            },
            space:{
                type: Number,
                default: 0,
            }
        },
        methods: {
            expand() {
                this.$emit('expand');
            },
            sort(prop, order){
                this.$emit('sort', prop, order);
            }
        }
    }

</script>

该组件主要承担着LiteTableColumn与上层组件LiteTableRow和LiteTable之间的通讯职能。由于使用了slot插槽将LiteTableColumn插入LiteTableRow与LiteTable,而slot不能进行事件监听,因此使用该组件进行通讯。

LiteTableColumn.vue

<template>
    <template  v-if="columnType == 'header'">
        <th :width="width" :style="Style" v-if="sortable" @click="setOrder" class="lite-sort-th">
            <div v-if="$slots.header" class="lite-td" :class="thClass">
                <slot name="header" />
                <span  class="lite-caret-wrapper">
                    <i @click="SetAscending" class="el-icon-caret-top lite-ascending" :style="asc" @click.stop></i>
                    <i @click="SetDescending" class="el-icon-caret-bottom lite-descending" :style="des" @click.stop></i>
                </span>
            </div>
            <div v-else class="lite-td" :class="thClass">
                {{label}}
                <span  class="lite-caret-wrapper">
                    <i @click="SetAscending" class="el-icon-caret-top ascending" :style="asc" @click.stop></i>
                    <i @click="SetDescending" class="el-icon-caret-bottom descending" :style="asc" @click.stop></i>
                </span>
            </div>
        </th>
        <th :width="width" :style="Style" v-else>
            <div v-if="$slots.header" class="lite-td" :class="thClass">
                <slot name="header" />
            </div>
            <div v-else>
                {{label}}
            </div>
        </th>
    </template>
    <template v-if="columnType == 'body'">
        <td :width="width" :style="Style">
            <div class="lite-td" :class="tdClass">
                <span v-if="space && expandRowArraw" class="lite-ident" :style="`padding-left:${space + 20}px;`"></span>
                <div ref="icon" class="el-table__expand-icon lite-expand-icon" v-if="hasExpandedRow" @click="expand">
                    <i class="el-icon-arrow-right"></i>
                </div>
                <div v-if="$slots.default">
                    <slot :row="row" :index="index" />
                </div>
                <div v-else>
                    {{data}}
                </div>
            </div>
        </td>
    </template>
</template>

<script>

import {getDeepObjectValue} from '../utils';

export default {
    name: 'LiteTableColumn',
    props: {
        label:{
            type: String,
            default: '',
        },
        prop:{
            type: String,
            default: '',
        },
        width:{
            type: Number || String,
            default: null,
        },
        minWidth:{
            type: Number || String,
            default: null,
        },
        align:{
            type: String,
            default: 'center',
        },
        formatter:{
            type: Function,
            default: null,
        },
        indent:{
            type: Number,
            default: 16,
        },
        expandRowArraw:{
            type: Boolean,
            default: false,
        },
        sortable:{
            type: Boolean,
            default: false,
        },
        headerAlign:{
            type: String,
            default: '',
        }
    },
    data(){
        return{
            columnType: '',
            index: 0,
            row: {},
        }
    },
    mounted() {
        this.columnType = this.$parent.columnType;
        this.row = this.$parent.row;
        this.index = this.$parent.index;
        this.$nextTick(()=>{
            if(this.$parent.isExpand && this.$refs.icon){
                this.$refs.icon.style.transform = 'rotate(90deg)';
            }
            else if(this.$refs.icon){
                this.$refs.icon.style.transform = 'rotate(0deg)';
            }
        })
    },
    methods: {
        expand(){
            this.$parent.expand();
        },
        SetAscending(){
            this.$parent.sort(this.prop, 'ascending');
            //this.order = this.order == "ascending" ? null : "ascending";
        },
        SetDescending(){
            this.$parent.sort(this.prop, 'descending');
            //this.order = this.order == "descending" ? null : "descending";
        },
        setOrder(){
            if(this.$parent.sortStatus.prop == this.prop){
                if(this.$parent.sortStatus.order == 'ascending'){
                    this.$parent.sort(this.prop, 'descending');
                }else if(this.$parent.sortStatus.order == 'descending'){
                    this.$parent.sort(this.prop, null);
                }else{
                    this.$parent.sort(this.prop, 'ascending');
                }
            }else{
                this.$parent.sort(this.prop, 'ascending');
            }
        },
    },
    computed: {
        data(){
            if(this.formatter){
                let cellValue = getDeepObjectValue(this.row, this.prop);
                return this.formatter(this.row, cellValue, this.index);
            }
            return getDeepObjectValue(this.row, this.prop);
        },
        Style(){
            let ret = {};
            if(this.minWidth){
                ret.minWidth = typeof this.minWidth == "number" ? (this.minWidth + 'px') : this.minWidth;
            }
            // if(!this.minWidth && this.width){
            //     ret.width = typeof this.width == "number" ? (this.width + 'px') : this.width;
            // }
            ret.textAlign = this.align;
            return ret;
        },
        thClass(){
            if(this.headerAlign){
                switch(this.headerAlign){
                    case 'left':
                        return 'lite-td--left';
                    case 'right':
                        return 'lite-td--right';
                    default:
                        return 'lite-td--center';
                }
            }else{
                return this.tdClass;
            }
        },
        tdClass(){
            switch(this.align){
                case 'left':
                    return 'lite-td--left';
                case 'right':
                    return 'lite-td--right';
                default:
                    return 'lite-td--center';
            }
        },
        hasExpandedRow(){
            return this.expandRowArraw && this.row.children && this.row.children.length > 0;
        },
        space(){
            let totalSpace = this.$parent.space * this.indent;
            return totalSpace;
        },
        asc(){
            if(this.$parent.sortStatus.prop == this.prop && this.$parent.sortStatus.order == "ascending"){
                return "color: #409EFF;";
            }
            return "";
        },
        des(){
            if(this.$parent.sortStatus.prop == this.prop && this.$parent.sortStatus.order == "descending"){
                return "color: #409EFF;";
            }
            return "";
        },
    },
    watch: {
        '$parent.isExpand': function(val){
            if(val && this.$refs.icon){
                this.$refs.icon.style.transform = 'rotate(90deg)';
            }else if(this.$refs.icon){
                this.$refs.icon.style.transform = 'rotate(0deg)';
            }
            
        }
    }
}
</script>

<style>
.lite-td{
    display: flex;
    align-items: center;
    flex-direction: row;
}
.lite-td--left{
    justify-content: flex-start;
}
.lite-td--center{
    justify-content: center;
}
.lite-td--right{
    justify-content: flex-end;
}
.lite-expand-icon{
    transition: 'all 0.3s',
}
.lite-caret-wrapper{
    margin-left: 5px;
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
.lite-ascending{
    top: 3px;
    position: relative;
}
.lite-descending{
    top: -3px;
    position: relative;
}
.lite-sort-th{
    cursor: pointer;
}

th{
    color: var(--el-table-header-font-color);
}

.lite-ident{
    display: inline-block;
    width: 0;
}

</style>

该组件功能不再进行具体赘述,主要是增加了排序的表头样式,排序的功能函数,以及展开所需的样式。

总结

本次主要新增了展开行与表格搜索功能。简单增加了一些样式信息。以及其他一些简单的样式信息。(主要原因是完成目标功能只需要这些功能,element-plus的表格功能实在太卡了。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值