一、实现效果:
(1)数据为空时:
(2)有数据时:存在数据合并;可自定义表头和列的内容
(3)新增行:
(4)删除行:
(5)双击编辑单元格内容:
二、组件代码:
(1)components / CustomTable / index.vue
<template>
<div class="custom-table-dialog">
<el-table :data="paginatedData" border :span-method="spanMethod"
highlight-current-row @row-click="rowClick" @cell-dblclick="cellDbClick"
@selection-change="handleSelection" :header-row-style="props?.headerRowStyle"
:header-cell-style="props?.headerCellStyle"
:row-style="props?.rowStyle" :cell-style="props?.cellStyle" :empty-text="props.emptyText">
<!-- 多选 -->
<el-table-column v-if="isSelection" type="selection" width="55" align="center"></el-table-column>
<template v-for="column in columnList" :key="column.label">
<custom-table-column :column="column">
<template v-slot:[`header-${column.prop}`]="scope">
<slot :name="`header-${column.prop}`" v-bind="scope"></slot>
</template>
<template v-slot:[`default-${column.prop}`]="scope">
<slot :name="`default-${column.prop}`" v-bind="scope"/>
</template>
</custom-table-column>
</template>
</el-table>
<el-pagination
size="small"
:hide-on-single-page="true"
background
layout="prev, pager, next"
:total="tableData.length"
:current-page="currentPage"
:page-size="pageSize"
@current-change="handlePageChange"
/>
</div>
</template>
<script setup lang="ts">
import CustomTableColumn from "@/components/CustomTable/components/CustomTableColumn.vue";
import {computed, ref} from "vue";
import type {ColumnItem} from "@/types/table";
import EventBus from "@/plugins/event-bus";
const props = defineProps<{
tableData: any[] // 表格数据
columnList: ColumnItem[] // 表头数据
pageSize: number // 每页显示条数
selection?: boolean // 是否多选
merge?: boolean // 是否合并
mergeColumns?: string[] // 哪些列中的单元格需要合并
distinguishColumns?: string[] // 哪些列中单元格的值不一样,就不合并
headerRowStyle?: any // 表头行样式
headerCellStyle?: any // 表头单元格样式
rowStyle?: any // 行样式
cellStyle?: any // 单元格样式
emptyText?: string // 空数据时显示的文本内容
}>()
const currentPage = ref(1)
const pageSize = ref(props.pageSize)
const paginatedData = computed(() => { // 分页数据
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return props.tableData.slice(start, end)
})
const isSelection = computed(() => { // 是否多选(勾选行)
let selection = false
if (props.selection) {
selection = props.selection
}
return selection
})
const colFields = computed(() => { // 所有列的 props
let fields: string[] = []
const properties = props.columnList.map((item: any) => {
return item.prop
})
if (props.selection) {
fields.push('')
fields.push(...properties)
} else {
fields = properties
}
return fields
})
let spanArr: any[] = [] //存储合并单元格的开始位置
const handlePageChange = (page: number) => {
currentPage.value = page
}
/* 行点击事件 */
const rowClick = (row: any, column: any) => {
EventBus.emit('row-click', {row, column})
}
/* 单元格双击事件 */
const cellDbClick = (row: any, column: any) => {
EventBus.emit('cell-dbClick', {row, column})
}
/* 行选择事件 */
const handleSelection = (data: any) => { // data 为所有处于勾选状态的行的数组
EventBus.emit('row-selection', data)
}
// 分析每一列,找出相同的
const getSpanArr = () => {
let mergeColumns: string[] = []
let distinguishColumns: string[] = []
if (props.mergeColumns) {
mergeColumns = props.mergeColumns
}
if (props.distinguishColumns) {
distinguishColumns = props.distinguishColumns
}
for (let i = 0; i < paginatedData.value.length; i++) {
let row = i;
// let col = i % this.colCount;
if (row === 0) {
// i 表示行 j表示列
for (let j = 0; j < colFields.value.length; j++) {
spanArr[i * colFields.value.length + j] = {
rowspan: 1,
colspan: 1,
}
}
} else {
for (let j = 0; j < colFields.value.length; j++) {
// 当前和上一次的一样
// 合并所有列的相同数据单元格
if (mergeColumns.includes(colFields.value[j])) { // Check if the column is in mergeColumns
if (distinguishColumns.some(col => paginatedData.value[row][col] !== paginatedData.value[row - 1][col])) {
// If any distinguish column is different, do not merge
spanArr[row * colFields.value.length + j] = {
rowspan: 1,
colspan: 1,
}
} else if (paginatedData.value[row][colFields.value[j]] === paginatedData.value[row - 1][colFields.value[j]]) {
let beforeItem =
spanArr[(row - 1) * colFields.value.length + j]
spanArr[row * colFields.value.length + j] = {
rowspan: 1 + beforeItem.rowspan, // Merge rows
colspan: 1, // Merge columns
}
beforeItem.rowspan = 0
beforeItem.colspan = 0
} else {
// No merge
spanArr[row * colFields.value.length + j] = {
rowspan: 1,
colspan: 1,
}
}
}
}
}
}
// 对数据进行倒序
let stack = []
for (let i = 0; i < colFields.value.length; i++) {
for (let j = 0; j < paginatedData.value.length; j++) {
// console.log("i=" + i + " j=" + j);
// i 表示列 j表示行
if (j === 0) {
if (spanArr[j * colFields.value.length + i].rowspan === 0) {
stack.push(spanArr[j * colFields.value.length + i])
}
} else {
if (spanArr[j * colFields.value.length + i]?.rowspan === 0) {
stack.push(spanArr[j * colFields.value.length + i])
} else {
stack.push(spanArr[j * colFields.value.length + i])
while (stack.length > 0) {
let pop = stack.pop()
let len = stack.length
spanArr[(j - len) * colFields.value.length + i] = pop
}
}
}
}
}
}
const spanMethod = (data: { row: any, column: any, rowIndex: any, columnIndex: any }) => {
if (props.merge) {
getSpanArr()
return spanArr[data.rowIndex * colFields.value.length + data.columnIndex]
} else {
return
}
}
/*// 合并固定列
const spanMethod = (data: { row: any, column: any, rowIndex: any, columnIndex: any }) => {
if (data.column.property === 'column1') { // 指定列相邻单元格的值相等就合并
const currentValue = data.row[data.column.property] // 当前单元格的值
const preRow = props.tableData[data.rowIndex - 1] // 上一行
const preValue = preRow ? preRow[data.column.property] : null // 上一行相同列的值
if (currentValue === preValue) { // 将当前单元格隐藏
return {
rowspan: 0,
colspan: 0
}
} else { // 合并 —— 计算当前单元格应该跨多少行
let rowSpan = 1 // 初始跨 1 行
for (let i = data.rowIndex + 1; i < props.tableData.length; i++) {
const nextRow = props.tableData[i] // 下一行
const nextValue = nextRow ? nextRow[data.column.property] : null // 下一行相同列的值
if (nextValue === currentValue) {
rowSpan++
} else {
break
}
}
return {
rowspan: rowSpan,
colspan: 1
}
}
}
}*/
</script>
<style lang="scss">
.custom-table-dialog {
//height: 100%;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
//border: 1px solid red;
.el-table {
--el-table-border-color: rgba(218, 226, 237); // 边框颜色
--el-table-bg-color: transparent;
--el-table-text-color: rgba(165, 176, 193); // 表格数据部分文字颜色
--el-table-header-text-color: rgba(251, 251, 251); // 表头文字颜色
--el-table-row-hover-bg-color: rgba(252, 250, 255); // 鼠标悬浮时表行的背景色
--el-table-header-bg-color: rgba(201, 213, 229); // 表头背景色
--el-table-tr-bg-color: rgba(255, 255, 255); // 表格数据部分表行背景色
background-color: transparent;
.el-table__body tr.current-row > td.el-table__cell { // 高亮行单元格的背景色
background: rgba(252, 250, 255);
}
.el-table__empty-block { // 数据为空时 表格 body 的样式
background-color: #FFFFFF;
}
}
.el-checkbox__input.is-checked .el-checkbox__inner {
background-color: rgba(192, 172, 233);
border-color: rgba(192, 172, 233);
}
.el-checkbox__input.is-indeterminate .el-checkbox__inner {
background-color: rgba(192, 172, 233);
border-color: rgba(192, 172, 233);
}
.el-checkbox__inner:hover {
border-color: rgba(192, 172, 233);
}
//:deep(.custom-table-dialog .el-pagination) {
// //margin-top: 100px;
// position: absolute;
// left: 50%;
// transform: translateX(-50%);
// bottom: 20px;
//}
.el-pagination { /* 如要固定分页器的位置在容器底部,在父组件中使用:deep(.custom-table-dialog .el-pagination) {}修改,示例如上 */
margin-top: 10px;
}
.el-pagination.is-background .btn-next, .el-pagination.is-background .btn-prev, .el-pagination.is-background .el-pager li {
background-color: rgba(201, 213, 229);
color: rgba(251, 251, 251);
}
.el-pagination.is-background .btn-next.is-active, .el-pagination.is-background .btn-prev.is-active, .el-pagination.is-background .el-pager li.is-active {
background: rgba(192, 172, 233);
}
}
</style>
(2)components / CustomTable / components / CustomTableColumn.vue
<template>
<el-table-column v-if="column.children?.length" v-bind="getColumnProps(column)">
<template #header="scope">
<slot :name="`header-${column.prop}`" v-bind="scope">
<span>{{ column.label }}</span>
</slot>
</template>
<template v-for="child in column.children" :key="child.label">
<custom-table-column :column="child"/>
</template>
</el-table-column>
<el-table-column v-else v-bind="getColumnProps(column)">
<template #header="scope">
<slot :name="`header-${column.prop}`" v-bind="scope">
<span>{{ column.label }}</span>
</slot>
</template>
<template #default="scope">
<slot :name="`default-${column.prop}`" v-bind="scope">
<span>{{ scope.row[column.prop] }}</span>
</slot>
</template>
</el-table-column>
</template>
<script setup lang="ts">
import type {ColumnItem} from "@/types/table";
defineProps<{
column: ColumnItem
}>()
const getColumnProps = (column: ColumnItem) => {
const {children, ...props} = column
return props
}
</script>
<style scoped lang="scss">
</style>
(3)types / table.ts
/*
* 表列属性【 ?. 代表非必传,否则必传】
* */
export interface ColumnItem {
label: string,
prop: string,
children?: ColumnItem[],
align?: string, // 对齐方式
width?: string | number, // 宽度
sortable?: boolean | string, // 对应列是否可以排序,如果设置为 'custom',则代表用户希望远程排序,需要监听 Table 的 sort-change 事件
/* 对象可以具有任意数量的键,这些键的类型为 string,对应的值的类型为 any */
[key: string]: any
}
export enum TableSize {
Large = 'large',
Default = 'default',
Small = 'small',
}
(4) plugins / event-bus.ts
import mitt from 'mitt' // 首先 npm install mitt
const EventBus = mitt()
export default EventBus
三、使用代码:
MyTable.vue
<template>
<!-- 二次封装 el-table -->
<div class="custom-table-container">
<div class="table-header">
<div>title</div>
<div class="table-buttons">
<div class="table-button" @click="addRow">
<el-icon>
<Plus/>
</el-icon>
</div>
<div class="table-button" @click="deleteRows">
<el-icon>
<Delete/>
</el-icon>
</div>
</div>
</div>
<custom-table :column-list="columnList" :page-size="15" :table-data="tableData" :selection="true" :merge="true"
:merge-columns="mergeColumns" :distinguish-columns="distinguishColumns" empty-text="Please set Data">
<!-- 自定义表头单元格内容示例 -->
<template #header-id="{row, column}">
{{ column.label }} Here
</template>
<!-- 自定义列内容示例 -->
<template #default-column1="{row, column}">
<el-input ref="inputRef" v-model="row.column1"
v-if="editRow === row.id && editColumn === column.id" @blur="stopEdit"
size="small"/>
<span v-else>{{ row.column1 }}</span>
</template>
<template #default-number1="{row, column}">
<el-input ref="inputRef" v-model="row.number1"
v-if="editRow === row.id && editColumn === column.id" @blur="stopEdit"
size="small"/>
<span v-else>{{ row.number1 }}</span>
</template>
<template #default-select1="{row, column}">
<el-select ref="inputRef" v-model="row.select1"
v-if="editRow === row.id && editColumn === column.id" @blur="stopEdit"
size="small">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else>{{ row.select1 }}</span>
</template>
</custom-table>
</div>
</template>
<script setup lang="ts">
import CustomTable from "@/components/CustomTable/index.vue";
import type {ColumnItem} from "@/types/table";
import {nextTick, onMounted, ref} from "vue";
import EventBus from "@/plugins/event-bus";
import {Delete, Plus} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus";
const columnList: ColumnItem[] = [
{
label: 'id',
prop: 'id',
align: 'center'
},
{
label: 'column1',
prop: 'column1',
align: 'center',
width: 100
},
{
label: 'number1',
prop: 'number1',
align: 'center',
width: 100
},
{
label: 'select1',
prop: 'select1',
align: 'center'
},
{
label: 'time1',
prop: 'time1',
align: 'center'
},
{
label: 'time2',
prop: 'time2',
align: 'center'
}
]
const mergeColumns = ['column1', 'select1'] // Specify merge columns
const distinguishColumns = ['column1'] // Specify distinguish columns
const headerCellStyle = (row: any) => { // 表头单元格样式示例
return {
backgroundColor: 'lightpink'
}
}
const rowStyle = (data: any) => { // 行样式示例
if (data.row.column1 === 'row1') {
return {
backgroundColor: 'lightpink'
}
} else {
return {
backgroundColor: 'lightgreen'
}
}
}
const tableData = ref<any[]>([ // 表格数据
{
id: 1,
column1: 'row1',
number1: 1,
select1: 'option1',
time1: 'test',
time2: 'test'
},
{
id: 2,
column1: 'row1',
number1: 2,
select1: 'option1',
time1: 'test',
time2: 'test'
},
{
id: 3,
column1: 'row2',
number1: 3,
select1: 'option1',
time1: 'test',
time2: 'test'
},
{
id: 4,
column1: 'row2',
number1: 4,
select1: 'option1',
time1: 'test',
time2: 'test'
},
{
id: 5,
column1: 'row3',
number1: 4,
select1: 'option1',
time1: 'test',
time2: 'test'
},
{
id: 6,
column1: 'row4',
number1: 5,
select1: 'option1',
time1: 'test',
time2: 'test'
}
])
const options = [
{
value: 'option1',
label: 'option1'
},
{
value: 'option2',
label: 'option2'
}
]
const editRow = ref<any>(null) // 正在编辑的行
const editColumn = ref<any>(null) // 正在编辑的列
const inputRef = ref<any>(null)
const selectedRows = ref<any[]>([]) // 选中的行
onMounted(() => {
EventBus.on('cell-dbClick', (data: any) => { // 单元格双击事件
editRow.value = null
editColumn.value = null
editRow.value = data.row.id
editColumn.value = data.column.id
nextTick(() => {
inputRef.value.focus() // 输入框自动聚焦
})
})
})
const stopEdit = () => { // 停止编辑
editRow.value = null
editColumn.value = null
inputRef.value = null
/* 每次编辑后根据 column1 的内容重新排序 */
const groupedTableData = tableData.value.reduce((acc, curr) => {
(acc[curr.column1] = acc[curr.column1] || []).push(curr)
return acc
}, {})
tableData.value = []
Object.keys(groupedTableData).forEach((key: any) => {
tableData.value.push(...groupedTableData[key])
})
}
onMounted(() => {
EventBus.on('row-selection', (data: any) => { // 行选择事件
selectedRows.value = data
})
})
const addRow = async () => { // 新增行
const rowData = {
id: tableData.value.length + 1,
column1: '',
number1: '',
select1: '',
time1: '',
time2: ''
}
tableData.value.push(rowData)
}
const deleteRows = () => { // 删除行
if (selectedRows.value.length > 0) {
selectedRows.value.forEach((item: any) => {
tableData.value = tableData.value.filter((data: any) => data.id !== item.id)
})
} else {
ElMessage.error('Please select the row to delete')
}
}
</script>
<style scoped lang="scss">
.custom-table-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(233, 237, 246);
padding: 20px;
.table-header {
background: rgba(200, 180, 243);
color: rgba(254, 255, 253);
font-family: Consolas;
font-size: 20px;
font-weight: bolder;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
}
.table-buttons {
display: flex;
.table-button {
color: rgba(255, 254, 254);
background: rgba(192, 172, 233);
font-size: 20px;
font-family: Consolas;
width: fit-content;
padding: 10px;
margin-right: 10px;
cursor: pointer;
}
.table-button:last-child { // 最右边的按钮 margin-right = 0
margin-right: 0;
}
}
}
</style>
四、参考文章
el-table表格动态合并相同数据单元格(可指定列+自定义合并)_el-table 合并单元格动态-CSDN博客