上效果图:
完整代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vue2-Element ui实现 复杂动态报表(同组合并,同组小计,二级表头,内容总合计)</title>
<!-- CDN方式引入element-ui可能会失败,建议浏览器输入下方网址将源码复制出来放到本地 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- <link rel="stylesheet" href="./element.css"> -->
</head>
<body>
<div id="app">
<h2>某平台双十一电脑销量报表</h2>
<div class="tableBox" style="width: 800px;">
<el-table :data="tableData" border :summary-method="getSummaries" :span-method="tablespan" ref="tabRef"
:header-cell-style="{background:'#f5f7fa',color:'#666'}" :cell-style="cellClass" show-summary>
<el-table-column align="center" prop="brandName" label="品牌名称" width="130"></el-table-column>
<el-table-column align="center" prop="saleNumber" label="销售数量" width="110"></el-table-column>
<el-table-column align="center" prop="salePrice" label="销售额" width="110"></el-table-column>
<el-table-column align="center" prop="profit" label="毛利" width="110"></el-table-column>
<el-table-column align="center" label="产品类型">
<el-table-column align="center" prop="Desktop" label="台式机" width="110"></el-table-column>
<el-table-column align="center" prop="laptop" label="笔记本" width="110"></el-table-column>
</el-table-column>
<el-table-column align="center" prop="saleTime" label="统计日期"></el-table-column>
</el-table>
</div>
</div>
</body>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<!-- CDN方式引入element-ui可能会失败,建议浏览器输入下方网址将源码复制出来放到本地 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- <script src="./element.js"></script> -->
<script type="text/javascript">
const app = new Vue({
el: '#app', // 这里的的app 是盒子定义的id名
data() {
return {
// 模拟接口返回的表单数据
// 字段说明: 品牌(brandName),品牌代码(brandCode),销售数量(saleNumber),销售额(salePrice).....
list: [
{id:1,brandName:"联想",brandCode:"01",saleNumber:"8",salePrice:"9995",profit:"2100",Desktop:"5",laptop:"3",saleTime:"2023-11-10"},
{id:2,brandName:"惠普",brandCode:"02",saleNumber:"16",salePrice:"13995",profit:"5150",Desktop:"7",laptop:"9",saleTime:"2023-11-11"},
{id:3,brandName:"机械革命",brandCode:"03",saleNumber:"12",salePrice:"8259",profit:"1850",Desktop:"2",laptop:"10",saleTime:"2023-11-10"},
{id:4,brandName:"联想",brandCode:"01",saleNumber:"23",salePrice:"21295",profit:"3620",Desktop:"8",laptop:"15",saleTime:"2023-11-12"},
{id:5,brandName:"惠普",brandCode:"02",saleNumber:"31",salePrice:"59995",profit:"12100",Desktop:"2",laptop:"29",saleTime:"2023-11-13"},
{id:6,brandName:"联想",brandCode:"01",saleNumber:"11",salePrice:"16585",profit:"3250",Desktop:"2",laptop:"9",saleTime:"2023-11-14"},
{id:7,brandName:"联想",brandCode:"01",saleNumber:"7",salePrice:"16540",profit:"1350",Desktop:"2",laptop:"5",saleTime:"2023-11-12"},
{id:8,brandName:"惠普",brandCode:"02",saleNumber:"8",salePrice:"9995",profit:"2100",Desktop:"5",laptop:"3",saleTime:"2023-11-11"},
{id:9,brandName:"联想",brandCode:"01",saleNumber:"6",salePrice:"10500",profit:"1500",Desktop:"1",laptop:"5",saleTime:"2023-11-15"},
{id:10,brandName:"机械革命",brandCode:"03",saleNumber:"8",salePrice:"10850",profit:"1825",Desktop:"5",laptop:"3",saleTime:"2023-11-11"}
],
tableData: []
};
},
mounted() {
this.GetTabListFun()
},
methods: {
// 这些都是根据调试经验得出的计算,代码可能看起来有点繁杂,不必纠结,只要知道如何使用即可
GetTabListFun() {
this.tableData = this.countClassFun({
Tabchildren: this.$refs.tabRef.$children, // 表格对象的表头列表
DataList: this.list, // 数组对象类型: 接口数据
Countfield: "brandCode", // 字符串类型: 用于归类统计的类别代号字段名,如:根据品牌代码(brandCode)进行归类
countcolumnName: ["销售数量", "销售额", "毛利", "台式机", "笔记本"] // 数组字符串类型:需要统计的列名称(对应列的表头名称)
})
},
// 合并行
tablespan({
row,
column,
rowIndex,
columnIndex
}) {
return this.tablespanFun({
DataList: this.tableData, // 表格数据
RuleField: 'brandCode', // 用于归类统计的类别代号字段名
columnNameList: ['品牌名称'], // 需要合并列的表头名称
row,
column,
rowIndex
})
},
getSummaries(param) { // 总合计
return this.getSummariesFun({
param,
CountList: ["销售数量", "销售额", "毛利", "台式机", "笔记本"] // 需要总合计的对应列
})
},
//设置单元格背景色
cellClass(prop) {
if (this.tableData.length) {
if (prop.row[this.$refs.tabRef.$children[0].prop].includes("合计")) {
return { // 直接在这里改你想要的颜色即可
"background-color": "#fdf3ea",
"color": "red"
}
}
}
},
// 下面是封装好的合并与统计规则,可不必理会,直接复制进去按照上方的用法使用即可================
flattenTree(arr) { // 递归解析多级表头,并将树形表头扁平化为一维对象数组
let newData = []
const expanded = (item) => {
if (item.length) {
item.forEach(e => {
if (e.label && e.prop) {
newData.push(e)
}
expanded(e.$children)
})
}
}
expanded(arr)
return newData
},
countClassFun({
Tabchildren,
DataList,
Countfield,
countcolumnName
}) {
if (!(DataList instanceof Array)) return []
try {
let newList = JSON.parse(JSON.stringify(DataList)) // 深拷贝一份,避免影响源数据
let arr = [] // 将数据按指定字段进行 分组(Countfield)
let idArr = [] // 存放各组id
let tableData = [] // 处理完成后的最终数据
// 自动分配表头的key、value(表头名称、名称对应的列表字段名)
let TabLabel = this.flattenTree(Tabchildren).map((item) => {
return {
label: item.label,
key: item.prop
}
})
newList.forEach((item) => { // 循环查出数据一共有多少组不同的归类id(Countfield)
if (!idArr.includes(item[Countfield])) {
idArr.push(item[Countfield])
}
})
idArr.forEach((id, i) => { // 根据id将数据进行切割分组
let list = []
newList.forEach((item) => {
if (item[Countfield] == id) {
list.push(item)
}
})
// 创建一条合计数据,字段与普通数据相同
let rowData = JSON.parse(JSON.stringify(newList[0]))
Object.keys(rowData).forEach((key, v) => {
rowData[key] = ""
});
TabLabel.forEach((val) => {
if (countcolumnName.includes(val.label)) { // 根据判断当前字段是否需要累加
rowData[val.key] = list.reduce((prev, cur) => { // 累加器
if (!isNaN(cur[val.key])) { // 判断传入的的字段名是否是数字
return prev + Number(cur[val.key]);
}
}, 0);
}
})
rowData[TabLabel[0].key] = "合计:"
list.push(rowData) // 处理完毕后,往当前分组尾端插入一条计算好的数据
arr[i] = list
})
arr.forEach((item) => { // 最后将将分组数据扁平化
tableData.push(...item)
})
return tableData
} catch (e) {
return []
}
},
// 报表合并规则
tablespanFun({
DataList,
RuleField,
columnNameList,
row,
column,
rowIndex
}) {
// RuleField:数组类型,接收指定有何列的名称
let newList = JSON.parse(JSON.stringify(DataList))
if (columnNameList.includes(column.label)) {
if (rowIndex > 0 && row[RuleField] == newList[rowIndex - 1][RuleField]) {
return {
rowspan: 0,
colspan: 0
};
} else {
let _row = 1;
for (let i = rowIndex + 1; i < newList.length; i++) {
if (row[RuleField] == newList[i][RuleField]) {
_row++;
} else {
break;
}
}
return {
rowspan: _row,
colspan: 1
};
}
}
},
// 总合计
getSummariesFun({
param,
CountList
}) {
// CountList:*数组类型,接受需要合并的列名称
let {
columns,
data
} = param;
let sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = "总合计:"
return;
}
if (CountList.includes(column.label)) {
const values = data.map((item) => { // 如果是合计行,则不对这行数值累加
if (!Object.values(item)[1].includes("合计")) {
return Number(item[column.property])
}
});
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return prev + curr;
} else {
return prev;
}
}, 0);
}
}
})
return sums;
},
}
})
</script>
<style>
* {
padding: 0px;
margin: 0px;
box-sizing: border-box;
}
#app {
padding: 30px;
}
h2 {
text-align: center;
}
.tableBox {
margin: 20px auto;
}
/* 组件自带的边框线不太明显,稍微改动一下 */
.el-table .cell {
padding: 0px 5px !important;
}
.el-date-editor .el-range-separator {
min-width: 28px !important;
}
.el-table td.el-table__cell {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border .el-table__cell {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border .el-table__cell {
border-right: 1px solid #d0d0d0 !important;
}
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border .el-table__cell {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border:after,
.el-table--group:after,
.el-table:before {
background-color: #d0d0d0 !important;
}
.el-table--border,
.el-table--group {
border-color: #d0d0d0 !important;
}
.el-table td,
.el-table th.is-leaf {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border th,
.el-table--border th.gutter:last-of-type {
border-bottom: 1px solid #d0d0d0 !important;
}
.el-table--border td,
.el-table--border th {
border-right: 1px solid #d0d0d0 !important;
}
.el-table__footer-wrapper td.el-table__cell {
border-top: 1px solid #d0d0d0 !important;
}
</style>
</html>