vue组件:实现一个简单的分组表格

6,223 阅读3分钟

背景

根据业务需求,需要开发一个将数据进行分组的表格组件,经过网上查询,好像没有类似的分组表格vue组件。后参照airtable (airtable.com/shrt81T9SjH…),自己实现了一个简单的分组表格vue组件。以下是对自己实现思路的一个简要分享,如有不足,还请多多指教!

效果

<template lang="pug">
    group-table(
        :items="dataList", 
        :groupKeys="groupKeys",
        :calculateInfo="groupCalcs",
        :isCheckbox="isCheckbox",
        :columns="columns")
        template(slot="headTr")
            td 姓名
            td 年纪
            td 地址
            td 编辑
        template(v-slot:itemTr="props")
            td {{props.model.name}}
            td {{props.model.age}}
            td {{props.model.adress}}
            td 
                button 编辑
</template>
<script>
export default {
    data() {
        return {
            groupKeys: [
                { code: "age", name: "年纪" },
                { code: "adress", name: "地址" }
            ],
            groupCalcs: [{ code: "age", type: "max" }],
            columns: ["name", "age", "adress", "btn-editor"],
            isCheckbox: true,
            dataList: [
                {
                    name: "小李",
                    adress: "湖南",
                    age: 22
                },
                {
                    name: "小明",
                    adress: "广州",
                    age: 23
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 20
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 23
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 23
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 20
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 20
                },
                {
                    name: "小dd",
                    adress: "长沙",
                    age: 23
                },
                {
                    name: "小cc",
                    adress: "长沙",
                    age: 21
                }
            ]
        };
    }
};
</script>

正文

1、组件框架设计

group-table 组件:显示整个表格

group-table-item组件:为group-table组件的子组件,负责渲染分组后的数据,如果分组完,则点击展开后显示渲染分组的详细信息,如果还没分组完,则点击展开显示分组的概要信息

2、分组表格组件功能

  1. 根据传入的原始数据和需要分组的字段进行分组
  2. 统计每一层对某字段进行分组后,相同该字段值的数量
  3. 统计每一层对某字段进行分组后,能对分组后的数据某个字段进行求和,最大值,最小值计算
  4. 进行勾选

3、组件参数设计

group-table 组件

参数 说明 类型 可选值 默认值
items 要进行分组的原始数据 Array []
groupKeys 分组使用的字段 Array []
columns 列标题信息 Array
calculateInfo 是否显示thead Array []
isHead 是否显示thead Boolean true
isCheckbox 是否可选中 Boolean false

group-table-item 组件

参数 说明 类型 可选值 默认值
model 分组后的概要信息 Object
level 第几次进行分组 Number
groupList 分组的字段 Object
singleRenderNum 懒加载时一次渲染的数量 Number 20

4、功能点实现

1、根据传入的原始数据和需要分组的字段进行分组

1.1 group-table组件中的相关实现

props: {
        items: {type: Array, default: ()=> ([])}, // 传入的原始数据
        groupKeys: {type: Array, default: ()=> ([])} // 分组使用的字段如: [{code:'name',name:'姓名'},{code:'age',name:'年纪'}]
    },
computed:{
    currentGroupKey() {
            return this.groupKeys[0];
        }
},
provide() {
        return {
            groupTableInstance: this
        };
    },
data() {
        return {
            groupData: [], // 分组后的数据信息
            level: 1 // 第几次分组
        };
    },
mounted() {
        this.initGroupData();
    }

methods中的相关方法

initGroupData() {
            if (this.currentGroupKey) { // 存在分组字段
                this.groupData = this.groupByKey(this.items, this.currentGroupKey);
            }
        },
groupByKey(arr, key) {
            let map = {};
            let code = key.code;
            let result = [];
            arr.forEach(item =>{
                if (!map[item[code]]) {
                    let newItem = {
                        key: code,
                        value: item[code],
                        name: key.name,
                        groupList: {[code]: item[code]}
                    };
                    result.push(newItem);
                    map[item[code]] = item;
                }
            });
            return result;
        },

模板中的相关结构

tr(v-if="items.length > 0 && groupKeys.length > 0")
    td(style="padding:0")
        group-table-item(v-for="(model,index) in groupData", :key="`group-${level}-${index}`", :model="model", :groupList="model.groupList", :level="level", v-bind="$attrs")
            template(v-slot:itemTr="props")
                slot(name="itemTr",:model="props.model",:index="props.index")

接收到原始数据和分组字段后,对第一个分组字段通过groupByKey方法得到初始分组数据groupData,然后对groupData进行v-for, 将相关信息传给group-table-item组件,渲染如下

1.2 group-table-item组件中的相关实现

props: {
        model: Object,
        level: Number,
        groupList: Object
    },
data() {
        return {
            groupData: [],
            isExpand: false, // 是否展开
        };
    },
inject: ['groupTableInstance'],
computed: {
    isLastLevel() {
        return this.level === this.groupKeys.length;
    },
    originItems() {
        return this.groupTableInstance.items;
    },
    groupKeys() {
        return this.groupTableInstance.groupKeys;
    },
    currentGroupKey() {
        return this.groupKeys[this.level];
    },
    currentData() { // 原始数据根据当前层的分组字段进行分组后的数据
        let result = [];
        let orignItems = [...this.originItems];
        let groupMap = new Map(Object.entries(this.groupList))
        result = orignItems.filter(item => [...groupMap].every(one => item[one[0]] === one[1]));
        return result;
    }
},
mounted() {
        this.initGroupData();
    },
methods: {
    initGroupData() {
        if (this.currentGroupKey) {
            this.groupData = this.groupByKey(this.originItems, this.currentGroupKey);
        }
    },
    groupByKey(arr, key) {
        let map = {};
        let code = key.code;
        let result = [];
        arr.forEach(item =>{
            if (!map[item[code]]) {
                result.push({
                    key: code,
                    value: item[code],
                    name: key.name,
                    groupList: {...this.groupList, [code]: item[code]}
                });
                map[item[code]] = item;
            }
        });
        return result;
    }
 }

相关模板

table
    tbody
        tr
            template(v-for="(code, index) in columns")
                template(v-if="index === 0")
                    td(class="td1 group-title")
                        div(class="td-title")
                            span(class="group-close",:class="{'group-open':isExpand}", @click="handlerExpand(model)")
                            div.group-info
                                span {{model.name}}:
                                span.group-value {{model.value}}
                                span.ml20.group-count {{level === 1 ? 'Count' : ''}} {{currentData.length}}
                template(v-else)
                    td
        template(v-if="isLastLevel ")
            tr(class="row-item", v-show="isExpand", v-for="(item,index) in renderData", :key="index")
                slot(name="itemTr",:model="item",:index="index")

div(v-show="isExpand",v-if="!isLastLevel")
    group-table-item(v-for="(item,index) in groupData",:key="`group-${level}-${index}`",:model="item",:groupList="item.groupList", :level="level + 1" :singleRenderNum="singleRenderNum")
        template(v-slot:itemTr="props")
            slot(name="itemTr",:model="props.model",:index="props.index")


isLastLevel用来判断是不是最后一层分组,如果是,group-table-item显示分组后的详细数据

如果不是,group-table-item显示分组概要信息,
并将当前分组数据groupData,分组条件groupList,层级level递归传给group-table-item组件,直到最后一层分组

2、统计每一层对某字段进行分组后,相同该字段值的数量

根据group-table-item组件中的计算属性currentData,通过原始数据和当前的分组条件计算当前层分组后数据,可以统计出每一层对某字段进行分组后,相同该字段值的数量

3、统计每一层对某字段进行分组后,能对分组后的数据某个字段进行求和、平均值、最大值,最小值计算

group-table组件props的calculateInfo属性接收列进行计算的信息 如 [{code:'age',type:'sum'}] type的类型为 sum, average, mim, max其中一种

goup-table-item组件中相关代码


inject: ['groupTableInstance'],
computed: {
    calculateInfo() {
        return this.groupTableInstance.calculateInfo;
    },
    calculateCode() { // 需要计算的字段
        return this.calculateInfo.map(one => one.code);
    },
    calculateMap() { // 需要计算的字段和对应的字段方式
        let obj = {};
        this.calculateInfo.forEach(one =>{
            obj[one.code] = one.type;
        });
        return obj;
    },
    currentData() { // 当前分组后的数据
        let result = [];
        let orignItems = [...this.originItems];
        let groupMap = this.objToStrMap(this.groupList);
        result = orignItems.filter(item => [...groupMap].every(one => item[one[0]] === one[1]));
        return result;
    },
},
methods:{
    calculateCol(data, code, type) {
    if (Array.isArray(data) && data.length > 0) {
        let calcList = data.map(one => one[code]);
        if (!calcList || calcList.every(one=>one === undefined || one === null)) {
            return null;
        }
        let values = calcList.map(one=>Number(one || 0));
        let calculateType = {
            sum(values) {
                let sum = values.reduce((a, b)=>a + b);
                return sum;
            },
            average(values) {
                let average = values.reduce((a, b)=>a+b)/data.length;
                return average.toFixed(3);
            },
            min(values) {
                return Math.min(...values);
            },
            max(values) {
                return Math.max(...values);
            },
        };
        return calculateType[type](values);
    }
},
}

相关的模板

td.tc(v-if="calculateCode.includes(code)") {{calculateMap[code].toUpperCase()}}:{{calculateCol(currentData,code, calculateMap[code])}}

如果某列的code在需要计算的calculateCode中,根据当前层的分组数据,和计算的code和计算方式,对该层该列进行计算

4、进行勾选

group-table组件中,对初始数据每一项通过$set添加一个私有属性_isChecked,默认值为false,

// 相应模板
tbody(v-if="isHead")
    tr
        td(width="20px", style="padding-left:8px",v-if="isCheckbox")
            input(type="checkbox",v-model="selectAll") // 进行全选
        slot(name="headTr")

// 计算属性

 selectAll: {
    get() {
        return this.items.every(item => item._isChecked);
    },
    set(val) {
        this.items.forEach(item =>{
            item._isChecked = val;
        });
    },
}

group-table-item中,分组后的详细数据中,input绑定该数据的_isChecked

input.mr10(type="checkbox",v-model="item._isChecked")

分组的概要信息中,通过当前的分组信息,进行该层数据是否全选

// 相关模板
td.pl-8(width="20px",v-if="isCheckbox")
    input(type="checkbox",v-model="isChecked")

// 相关计算属性
isChecked: {
    get() {
        return this.currentData.every(item => item._isChecked);
    },
    set(val) {
        this.currentData.forEach(item => {
            item._isChecked = val;
        });
    }
},