目录
- 需求 - 要实现的效果
- 初始代码
- 代码升级(可供多个表格使用)
- `CommonTable.vue 子组件 `
- 使用子组件1 - `父组件 - 图1~图3使用`
- 效果展示
- 使用子组件2 - `父组件 - 图4使用`
- 效果展示
- 注意
- 【代码优化 - 解决bug】
需求 - 要实现的效果
父组件中 info 数据示例
const info = {
itemMap: {
警告: [
{
total: 28,
cfzl: '1',
cfzlView: '警告',
wfxl: '12',
wfxlView: '超速行驶',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 3,
cfzl: '1',
cfzlView: '警告',
wfxl: '17',
wfxlView: '未低速通过',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 6,
cfzl: '1',
cfzlView: '警告',
wfxl: '26',
wfxlView: '违法停车',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 21,
cfzl: '1',
cfzlView: '警告',
wfxl: '28',
wfxlView: '违法装载',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 3,
cfzl: '1',
cfzlView: '警告',
wfxl: '49',
wfxlView: '其他影响安全行为',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 1,
cfzl: '1',
cfzlView: '警告',
wfxl: '28',
wfxlView: '违法装载',
jtfs: 'B21',
jtfsView: '中型栏板半挂车'
}
],
罚款: [
{
total: 56,
cfzl: '2',
cfzlView: '罚款',
wfxl: '12',
wfxlView: '超速行驶',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 6,
cfzl: '2',
cfzlView: '罚款',
wfxl: '17',
wfxlView: '未低速通过',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 12,
cfzl: '2',
cfzlView: '罚款',
wfxl: '26',
wfxlView: '违法停车',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 42,
cfzl: '2',
cfzlView: '罚款',
wfxl: '28',
wfxlView: '违法装载',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 6,
cfzl: '2',
cfzlView: '罚款',
wfxl: '49',
wfxlView: '其他影响安全行为',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
total: 2,
cfzl: '2',
cfzlView: '罚款',
wfxl: '28',
wfxlView: '违法装载',
jtfs: 'B21',
jtfsView: '中型栏板半挂车'
}
]
},
columns: [
{
// total: 28,
// cfzl: '1',
// cfzlView: '警告',
// wfxl: '12',
// wfxlView: '超速行驶',
jtfs: 'B11',
jtfsView: '重型栏板半挂车'
},
{
// total: 1,
// cfzl: '1',
// cfzlView: '警告',
// wfxl: '28',
// wfxlView: '违法装载',
jtfs: 'B21',
jtfsView: '中型栏板半挂车'
}
]
}
初始代码
父组件
<!-- info 数据来源 → info 数据示例 -->
<CommonTable :info="info"/>
CommonTable.vue 子组件
<template>
<el-table
:data="tableData"
border
stripe
max-height="400"
size="mini"
:span-method="firstColMergeSpan"
>
<el-table-column align="center" prop="cfzl" label="处罚种类" />
<el-table-column align="center" prop="wfxwfl" label="违法行为分类" />
<el-table-column
v-for="(item, index) in columns"
:key="index"
align="center"
:prop="item.jtfs"
:label="item.jtfsView || '-'"
>
<template slot-scope="{ row, $index }">
<span>{{ row[item.jtfs] | noDataFilter }}</span>
</template>
</el-table-column>
<el-table-column align="center" prop="xj" label="小计" />
</el-table>
</template>
<script>
export default {
name: 'CommonTable',
components: {},
props: {
info: {
type: Object,
required: true
}
},
data() {
return {
spanArr: []
}
},
computed: {
tableData() {
const list = []
Object.entries(this.info?.itemMap || {}).forEach((i) => {
i[1]?.forEach((ii) => {
list.push({
// cfzl: ii.cfzlView,
cfzl: i[0],
wfxwfl: ii.wfxlView,
[ii.jtfs]: ii.total,
jtfs: ii.jtfs
})
})
})
return list.map((item) => {
return {
...item,
xj: this.columnKeyList.reduce((a, b) => {
return a + (item[b] || 0)
}, 0)
}
})
},
columns() {
return this.info.columns || []
},
columnKeyList() {
return this.columns.map((item) => item.jtfs)
}
},
watch: {
info() {
this.xjPosition()
this.firstColMergeCount()
}
},
created() {},
methods: {
// 计算小计行插入位置
xjPosition(Human = this.tableData) {
const doctorMap = {}
for (let i = 0; i < Human.length; i++) {
// 找出相同名称的行数
const doctorName = Human[i].cfzl
if (doctorMap[doctorName] !== undefined) {
doctorMap[doctorName].push(i)
} else {
doctorMap[doctorName] = [i]
}
}
const keyArr = []
for (const k in doctorMap) {
// 取出key并倒序,防止正序插入会影响行下标
keyArr.unshift(k)
}
keyArr.forEach((ele, index) => {
const lastIndex = doctorMap[ele][doctorMap[ele].length - 1] // 找出相同名称最后一行插入合计数据
const obj = this.xjRowDataCalc(Human, ele) // 计算出小计行数据
Human.splice(lastIndex + 1, 0, obj) // 插入
})
},
// 小计行计算
xjRowDataCalc(data, name) {
const obj = {
cfzl: name, // 第一列用于合并单元格
wfxwfl: '小计'
}
this.columnKeyList.forEach((key) => {
obj[key] = 0
})
data.forEach((item) => {
// “处罚种类” 相同的加起来
if (item.cfzl === name) {
this.columnKeyList.forEach((key) => {
obj[key] += Number(item[key] || 0)
})
}
})
obj.xj = this.columnKeyList.reduce((a, b) => {
return a + (obj[b] || 0)
}, 0)
return obj
},
// 合并单元格
firstColMergeSpan({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex]
const _col = _row > 0 ? 1 : 0
return {
rowspan: _row,
colspan: _col
}
}
},
// 计算要合并的单元格
firstColMergeCount() {
let contactDot = 0
this.spanArr = []
this.tableData.forEach((item, index) => {
item.index = index
if (index === 0) {
this.spanArr.push(1)
} else {
// 根据相同 “处罚种类” 来合并
if (item.cfzl === this.tableData[index - 1].cfzl) {
this.spanArr[contactDot] += 1
this.spanArr.push(0)
} else {
contactDot = index
this.spanArr.push(1)
}
}
})
}
}
}
</script>
<style lang='scss' scoped>
</style>
代码升级(可供多个表格使用)
图1
图2
图3
图4
根据接口返回数据不同(数据格式一致,只是部分字段名不一致),向子组件传入不同的字段名。
CommonTable.vue 子组件
<template>
<el-table
:data="tableData"
border
stripe
max-height="400"
size="mini"
:span-method="firstColMergeSpan"
>
<!-- 第一列 -->
<el-table-column align="center" :prop="oneColPropField" :label="oneColLabelField" />
<!-- 第二列 -->
<el-table-column align="center" :prop="twoColPropField" :label="twoColLabelField" />
<!-- 其他数量列 -->
<el-table-column
v-for="(item, index) in columns"
:key="index"
align="center"
:prop="item[countColPropsField]"
:label="item[countColLabelField] || '-'"
>
<template slot-scope="{ row, $index }">
<span>{{ row[item[countColPropsField]] | noDataFilter }}</span>
</template>
</el-table-column>
<!-- “小计”列 -->
<el-table-column align="center" prop="xj" label="小计" />
</el-table>
</template>
<script>
export default {
name: 'CommonTable',
components: {},
props: {
info: {
type: Object,
required: true
},
// 自定义表格列
selfColumns: {
type: Array,
default: () => []
},
// 表格列非自定义时(接口获取)列字段名
columnKeyField: {
type: String,
default: 'jtfs'
},
// 第一列字段名
oneColPropField: {
type: String,
required: true
},
// 第一列展示header文本
oneColLabelField: {
type: String,
required: true
},
// 第一列数据来源字段名
oneColDataField: {
type: String,
required: true
},
// 第二列字段名
twoColPropField: {
type: String,
required: true
},
// 第二列展示header文本
twoColLabelField: {
type: String,
required: true
},
// 第二列数据来源字段名
twoColDataField: {
type: String,
required: true
},
// 其他数量 列 字段名
countColPropsField: {
type: String,
default: 'jtfs'
},
// 其他数量 列 展示header文本字段名
countColLabelField: {
type: String,
default: 'jtfsView'
},
// 其他数量 列 数据来源字段名
countColDataField: {
type: String,
default: 'total'
}
},
data() {
return {
spanArr: []
}
},
computed: {
// 表格数据处理
tableData() {
const list = []
Object.entries(this.info?.itemMap || {}).forEach((i) => {
i[1]?.forEach((ii) => {
list.push({
// [this.oneColPropField]: ii[this.oneColDataField] // 第一列数据(第一列数据部分表格 ii 中无第一列数据,所以使用↓↓↓下一行代码 i[0] 取第一列数据)
[this.oneColPropField]: i[0], // 第一列数据
[this.twoColPropField]: ii[this.twoColDataField], // 第二列数据
[ii[this.countColPropsField]]: ii[this.countColDataField] // 其他数量列数据
// jtfs: ii.jtfs
})
})
})
return list.map((item) => {
return {
...item,
// 计算小计数量
xj: this.columnKeyList.reduce((a, b) => {
return a + (item[b] || 0)
}, 0)
}
})
},
columns() {
/**
* 表格列获取
* 父组件有传表格列 selfColumns 就用 selfColumns
* 否则用接口获的表格列
*/
return (this.selfColumns.length && this.selfColumns) || this.info.columns || []
},
columnKeyList() {
// 获取表格每列字段名组成数组
return this.columns.map((item) => item[this.columnKeyField])
}
},
watch: {
info() {
this.xjPosition()
this.oneColMergeCount()
}
},
created() {},
methods: {
// 计算小计行插入位置
xjPosition(Human = this.tableData) {
const doctorMap = {}
for (let i = 0; i < Human.length; i++) {
// 找出相同名称的行数
const doctorName = Human[i][this.oneColPropField]
if (doctorMap[doctorName] !== undefined) {
doctorMap[doctorName].push(i)
} else {
doctorMap[doctorName] = [i]
}
}
const keyArr = []
for (const k in doctorMap) {
// 取出key并倒序,防止正序插入会影响行下标
keyArr.unshift(k)
}
keyArr.forEach((ele, index) => {
const lastIndex = doctorMap[ele][doctorMap[ele].length - 1] // 找出相同名称最后一行插入合计数据
const obj = this.xjRowDataCalc(Human, ele) // 计算出小计行数据
Human.splice(lastIndex + 1, 0, obj) // 插入
})
},
// 小计行数据计算
xjRowDataCalc(data, name) {
const obj = {
[this.oneColPropField]: name, // 第一列用于合并单元格
[this.twoColPropField]: '小计' // 第二列数据
}
this.columnKeyList.forEach((key) => {
obj[key] = 0 // 其他书两列数据
})
data.forEach((item) => {
// 第一列 oneColPropField 数据相同的加起来
if (item[this.oneColPropField] === name) {
this.columnKeyList.forEach((key) => {
obj[key] += Number(item[key] || 0)
})
}
})
// 小计列数据总和(小计行和小计列交汇处数据)
obj.xj = this.columnKeyList.reduce((a, b) => {
return a + (obj[b] || 0)
}, 0)
return obj
},
// 合并单元格
firstColMergeSpan({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex]
const _col = _row > 0 ? 1 : 0
return {
rowspan: _row,
colspan: _col
}
}
},
// 计算要合并的单元格
oneColMergeCount() {
let contactDot = 0
this.spanArr = []
this.tableData.forEach((item, index) => {
item.index = index
if (index === 0) {
this.spanArr.push(1)
} else {
// 第一列相同的合并
if (item[this.oneColPropField] === this.tableData[index - 1][this.oneColPropField]) {
this.spanArr[contactDot] += 1
this.spanArr.push(0)
} else {
contactDot = index
this.spanArr.push(1)
}
}
})
}
}
}
</script>
<style lang='scss' scoped>
</style>
使用子组件1 - 父组件 - 图1~图3使用
<!-- info 数据来源 → info 数据示例 -->
<CommonTable
:info="info"
one-col-prop-field="cfzl"
one-col-label-field="处罚种类"
one-col-data-field="cfzlView"
two-col-prop-field="wfxwfl"
two-col-label-field="违法行为分类"
two-col-data-field="wfxlView"
/>
效果展示
使用子组件2 - 父组件 - 图4使用
<CommonTable
:info="info"
one-col-prop-field="cfzl"
one-col-label-field="处罚种类"
one-col-data-field="cfzlView"
two-col-prop-field="wfxwfl"
two-col-label-field="违法行为分类"
two-col-data-field="wfxlView"
column-key-field="timenum"
count-col-props-field="timenum"
count-col-label-field="label"
:self-columns="columns"
/>
<script>
export default {
data() {
return {
columns: [...Array(24).keys()].map((item) => {
return {
timenum: `${item}`,
label: `${item}-${item + 1}`
}
}),
info: {
itemMap: {
警告: [
{
timenum: 17,
total: 9,
cfzl: '1',
cfzlView: '警告',
wfxl: '69',
wfxlView: '其他影响安全行为',
jtfs: null,
jtfsView: null
},
{
timenum: 17,
total: 3,
cfzl: '1',
cfzlView: '警告',
wfxl: '58',
wfxlView: '违法上道路行驶',
jtfs: null,
jtfsView: null
}
]
},
columns: []
}
}
}
}
</script>
效果展示
注意
- 使用子组件1 和 使用子组件2 中 info 数据不同
【代码优化 - 解决bug】
解决数据重复问题
<template>
<el-table
:data="tableData"
border
stripe
max-height="400"
size="mini"
:span-method="firstColMergeSpan"
>
<!-- 第一列 -->
<el-table-column align="center" :prop="oneColPropField" :label="oneColLabelField" />
<!-- 第二列 -->
<el-table-column align="center" :prop="twoColPropField" :label="twoColLabelField" />
<!-- 其他数量列 -->
<el-table-column
v-for="(item, index) in columns"
:key="index"
align="center"
:prop="item[countColPropsField]"
:label="item[countColLabelField] || '-'"
>
<template slot-scope="{ row, $index }">
<span>{{ row[item[countColPropsField]] | noDataFilter }}</span>
</template>
</el-table-column>
<!-- “小计”列 -->
<el-table-column align="center" prop="xj" label="小计" />
</el-table>
</template>
<script>
export default {
name: 'CommonTable',
components: {},
props: {
info: {
type: Object,
required: true
},
// 自定义表格列
selfColumns: {
type: Array,
default: () => []
},
// 表格列非自定义时(接口获取)列字段名
columnKeyField: {
type: String,
default: 'jtfs'
},
// 第一列字段名
oneColPropField: {
type: String,
required: true
},
// 第一列展示header文本
oneColLabelField: {
type: String,
required: true
},
// 第一列数据来源字段名
oneColDataField: {
type: String,
required: true
},
// 第二列字段名
twoColPropField: {
type: String,
required: true
},
// 第二列展示header文本
twoColLabelField: {
type: String,
required: true
},
// 第二列数据来源字段名
twoColDataField: {
type: String,
required: true
},
// 其他数量 列 字段名
countColPropsField: {
type: String,
default: 'jtfs'
},
// 其他数量 列 展示header文本字段名
countColLabelField: {
type: String,
default: 'jtfsView'
},
// 其他数量 列 数据来源字段名
countColDataField: {
type: String,
default: 'total'
}
},
data() {
return {
spanArr: []
}
},
computed: {
// 表格数据处理
tableData() {
const list = []
Object.entries(this.info?.itemMap || {}).forEach((i) => {
i[1]?.forEach((ii) => {
/** ** 解决数据重复问题 start ****/
const listDataIndex = list.findIndex(
(listItem) => listItem[this.twoColPropField] === ii[this.twoColDataField]
)
// 判断即将要 push 进 list 的数据 是否与 list 中已有数据的第二列数据有重复,有重复的话,就在 list 中重复的第二列数据的行数据中只添加当前数据为“其他数量列数据”
if (listDataIndex !== -1) {
list[listDataIndex][ii[this.countColPropsField]] = ii[this.countColDataField]
return
}
/** ** 解决数据重复问题 end ****/
list.push({
// [this.oneColPropField]: ii[this.oneColDataField] // 第一列数据(第一列数据部分表格 ii 中无第一列数据,所以使用↓↓↓下一行代码 i[0] 取第一列数据)
[this.oneColPropField]: i[0], // 第一列数据
[this.twoColPropField]: ii[this.twoColDataField], // 第二列数据
[ii[this.countColPropsField]]: ii[this.countColDataField] // 其他数量列数据
// jtfs: ii.jtfs
})
})
})
return list.map((item) => {
return {
...item,
// 计算小计数量
xj: this.columnKeyList.reduce((a, b) => {
return a + (item[b] || 0)
}, 0)
}
})
},
columns() {
/**
* 表格列获取
* 父组件有传表格列 selfColumns 就用 selfColumns
* 否则用接口获的表格列
*/
return (this.selfColumns.length && this.selfColumns) || this.info.columns || []
},
columnKeyList() {
// 获取表格每列字段名组成数组
return this.columns.map((item) => item[this.columnKeyField])
}
},
watch: {
info() {
this.xjPosition()
this.oneColMergeCount()
}
},
created() {},
methods: {
// 计算小计行插入位置
xjPosition(Human = this.tableData) {
const doctorMap = {}
for (let i = 0; i < Human.length; i++) {
// 找出相同名称的行数
const doctorName = Human[i][this.oneColPropField]
if (doctorMap[doctorName] !== undefined) {
doctorMap[doctorName].push(i)
} else {
doctorMap[doctorName] = [i]
}
}
const keyArr = []
for (const k in doctorMap) {
// 取出key并倒序,防止正序插入会影响行下标
keyArr.unshift(k)
}
keyArr.forEach((ele, index) => {
const lastIndex = doctorMap[ele][doctorMap[ele].length - 1] // 找出相同名称最后一行插入合计数据
const obj = this.xjRowDataCalc(Human, ele) // 计算出小计行数据
Human.splice(lastIndex + 1, 0, obj) // 插入
})
},
// 小计行数据计算
xjRowDataCalc(data, name) {
const obj = {
[this.oneColPropField]: name, // 第一列用于合并单元格
[this.twoColPropField]: '小计' // 第二列数据
}
this.columnKeyList.forEach((key) => {
obj[key] = 0 // 其他书两列数据
})
data.forEach((item) => {
// 第一列 oneColPropField 数据相同的加起来
if (item[this.oneColPropField] === name) {
this.columnKeyList.forEach((key) => {
obj[key] += Number(item[key] || 0)
})
}
})
// 小计列数据总和(小计行和小计列交汇处数据)
obj.xj = this.columnKeyList.reduce((a, b) => {
return a + (obj[b] || 0)
}, 0)
return obj
},
// 合并单元格
firstColMergeSpan({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex]
const _col = _row > 0 ? 1 : 0
return {
rowspan: _row,
colspan: _col
}
}
},
// 计算要合并的单元格
oneColMergeCount() {
let contactDot = 0
this.spanArr = []
this.tableData.forEach((item, index) => {
item.index = index
if (index === 0) {
this.spanArr.push(1)
} else {
// 第一列相同的合并
if (item[this.oneColPropField] === this.tableData[index - 1][this.oneColPropField]) {
this.spanArr[contactDot] += 1
this.spanArr.push(0)
} else {
contactDot = index
this.spanArr.push(1)
}
}
})
}
}
}
</script>
<style lang='scss' scoped>
</style>
【注】计算小计插入位置等部分方法参考文章 https://blog.csdn.net/seeeeeeeeeee/article/details/133122424