背景
根据业务需求,需要开发一个将数据进行分组的表格组件,经过网上查询,好像没有类似的分组表格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、分组表格组件功能
- 根据传入的原始数据和需要分组的字段进行分组
- 统计每一层对某字段进行分组后,相同该字段值的数量
- 统计每一层对某字段进行分组后,能对分组后的数据某个字段进行求和,最大值,最小值计算
- 进行勾选
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显示分组后的详细数据



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;
});
}
},