前言
大家好,我是大华!
在我们日常的后台管理开发中,表格可以说是最常用的数据展示和操作组件之一了。
很多用户还希望能够直接在线编辑表格数据、插入新行、删除不需要的行,甚至还需要支持各种类型的数据输入。
这时候,一个通用的可编辑表格组件就显得尤为重要。
所以我加班加点整出了这么一个表格组件。
功能预览
先来看下效果图:

我们看看上面的组件效果图具备了哪些功能:
- 支持单元格双击编辑
- 支持右键菜单操作(插入行、删除行)
- 支持多种输入类型(文本、数字、下拉选择、日期选择)
- 支持汇总行计算
- 响应式数据更新
- 灵活的列配置
核心代码实现
我们一步步来实现这可编辑的表格组件。
1. 组件基础结构
首先,我们定义组件的基本结构和Props
:
html
<!-- EditableTable.vue -->
<template>
<!--
el-table 是 Element Plus 的表格组件
:data 绑定表格数据
@cell-dblclick 监听单元格双击事件
@row-contextmenu 监听行右键点击事件
:summary-method 指定汇总行计算方法
:row-class-name 指定行类名生成方法
v-bind="$attrs" 继承所有未声明的属性
-->
<el-table
:data="tableData"
@cell-dblclick="handleCellDblClick"
@row-contextmenu="handleRowRightClick"
:summary-method="getSummaries"
:row-class-name="tableRowClassName"
:border="border"
:show-summary="showSummary"
v-bind="$attrs"
>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
type="index"
label="序号"
width="60"
/>
<!-- 表格列渲染 -->
<el-table-column
v-for="column in columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
>
<!-- 使用作用域插槽自定义单元格内容 -->
<template #default="scope">
<!-- 根据列类型渲染不同的输入组件 -->
<!-- 数字输入框 -->
<el-input-number
v-if="column.type === 'number' && scope.row[`${column.prop}_editing`]"
v-model.number="scope.row[column.prop]"
:min="column.min || 0"
:max="column.max || 100"
:step="column.step || 1"
@blur="scope.row[`${column.prop}_editing`] = false"
size="small"
/>
<!-- 下拉选择框 -->
<el-select
v-else-if="column.type === 'select' && scope.row[`${column.prop}_editing`]"
v-model="scope.row[column.prop]"
:multiple="column.multiple || false"
@blur="scope.row[`${column.prop}_editing`] = false"
size="small"
>
<el-option
v-for="item in column.options || []"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="column.type === 'date' && scope.row[`${column.prop}_editing`]"
v-model="scope.row[column.prop]"
type="date"
placeholder="选择日期"
@blur="scope.row[`${column.prop}_editing`] = false"
size="small"
/>
<!-- 文本编辑框 -->
<el-input
v-else-if="scope.row[`${column.prop}_editing`]"
v-model="scope.row[column.prop]"
@blur="scope.row[`${column.prop}_editing`] = false"
size="small"
autofocus
/>
<!-- 文本显示(非编辑状态) -->
<div
v-else
class="cell-text"
@dblclick.stop="handleCellDblClick(scope.row, { property: column.prop })"
>
{{ formatCellValue(scope.row[column.prop], column) }}
</div>
</template>
</el-table-column>
<!-- 右键菜单 -->
<div
v-show="showContextMenu"
class="context-menu"
:style="{ top: contextMenuTop + 'px', left: contextMenuLeft + 'px' }"
@mouseleave="hideContextMenu"
>
<el-button @click="insertRowAbove" size="small">上方插入一行</el-button>
<el-button @click="insertRowBelow" size="small">下方插入一行</el-button>
<el-button @click="openInsertMultipleDialog(false)" size="small">上方插入多行</el-button>
<el-button @click="openInsertMultipleDialog(true)" size="small">下方插入多行</el-button>
<el-button type="danger" @click="deleteCurrentRow" size="small">删除当前行</el-button>
</div>
</el-table>
<!-- 插入多行对话框 -->
<el-dialog
v-model="showInsertMultipleDialog"
:title="insertMultipleBelow ? '在下方插入多行' : '在上方插入多行'"
width="400px"
>
<el-input-number
v-model="insertRowCount"
:min="1"
:max="10"
label="插入行数"
/>
<template #footer>
<el-button @click="showInsertMultipleDialog = false">取消</el-button>
<el-button type="primary" @click="insertMultipleRows">确定</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
// 导入 Vue 相关功能
import { ref, computed, nextTick, watch } from 'vue'
// 导入 Element Plus 组件
import { ElMessageBox, ElMessage } from 'element-plus'
// 定义表格列的接口
interface TableColumn {
prop: string // 字段名
label: string // 列标题
type?: 'text' | 'number' | 'select' | 'date' // 输入类型
width?: string // 列宽度
min?: number // 最小值(数字类型)
max?: number // 最大值(数字类型)
step?: number // 步长(数字类型)
options?: Array<{ // 选项(选择类型)
value: string | number
label: string
}>
multiple?: boolean // 是否多选(选择类型)
formatter?: (value: any) => string // 自定义格式化函数
}
// 定义组件接收的属性
const props = defineProps({
data: { // 表格数据
type: Array,
default: () => []
},
columns: { // 列配置
type: Array as () => TableColumn[],
required: true
},
showIndex: { // 是否显示序号列
type: Boolean,
default: false
},
border: { // 是否显示边框
type: Boolean,
default: true
},
showSummary: { // 是否显示汇总行
type: Boolean,
default: false
},
summaryMethod: { // 自定义汇总方法
type: Function,
default: null
},
disabledColumns: { // 禁止编辑的列
type: Array as () => string[],
default: () => []
}
})
// 定义组件可触发的事件
const emit = defineEmits(['update:data', 'row-added', 'row-deleted'])
// 使用计算属性处理表格数据,确保响应式
const tableData = computed({
get: () => props.data,
set: (value) => emit('update:data', value)
})
// 右键菜单相关状态
const showContextMenu = ref(false)
const contextMenuTop = ref(0)
const contextMenuLeft = ref(0)
const currentContextRow = ref<any>(null)
// 插入多行相关状态
const showInsertMultipleDialog = ref(false)
const insertRowCount = ref(1)
const insertMultipleBelow = ref(false)
// 检查列是否被禁用编辑
const isDisabledColumn = (prop: string) => {
return props.disabledColumns.includes(prop)
}
// 格式化单元格显示值
const formatCellValue = (value: any, column: TableColumn) => {
if (column.formatter) {
return column.formatter(value)
}
if (column.type === 'select' && column.options) {
const option = column.options.find(opt => opt.value === value)
return option ? option.label : value
}
return value
}
</script>
<style scoped>
.context-menu {
position: fixed;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2000;
display: flex;
flex-direction: column;
gap: 5px;
}
.cell-text {
width: 100%;
height: 100%;
padding: 8px 0;
cursor: default;
}
.cell-text:hover {
background-color: #f5f7fa;
}
</style>
2. 实现单元格编辑功能
单元格编辑是核心功能之一,我们支持多种输入类型:
typescript
// 在 script setup 中添加以下代码
// 单元格双击事件处理
const handleCellDblClick = (row: any, column: any) => {
const prop = column.property
if (!prop || isDisabledColumn(prop)) return
// 关闭其他单元格的编辑状态
props.columns.forEach(col => {
if (col.prop !== prop) {
row[`${col.prop}_editing`] = false
}
})
// 开启当前单元格编辑
row[`${prop}_editing`] = true
}
// 行右键点击事件处理
const handleRowRightClick = (row: any, column: any, event: MouseEvent) => {
// 阻止浏览器默认右键菜单
event.preventDefault()
// 设置当前右键点击的行
currentContextRow.value = row
// 设置菜单位置
contextMenuTop.value = event.clientY
contextMenuLeft.value = event.clientX
// 显示菜单
showContextMenu.value = true
}
// 隐藏右键菜单
const hideContextMenu = () => {
showContextMenu.value = false
}
3. 实现行操作功能
右键菜单提供了丰富的行操作功能:
typescript
// 创建新行
const createNewRow = () => {
const newRow: any = {}
// 根据列配置初始化新行的值
props.columns.forEach(column => {
// 设置默认值
if (column.type === 'number') {
newRow[column.prop] = 0
} else if (column.type === 'select' && column.options && column.options.length > 0) {
newRow[column.prop] = column.multiple ? [] : column.options[0].value
} else {
newRow[column.prop] = ''
}
// 初始化编辑状态
newRow[`${column.prop}_editing`] = false
})
return newRow
}
// 在上方插入一行
const insertRowAbove = () => {
if (!currentContextRow.value) return
const index = tableData.value.indexOf(currentContextRow.value)
if (index === -1) return
const newRow = createNewRow()
tableData.value.splice(index, 0, newRow)
emit('row-added', { index, row: newRow })
hideContextMenu()
}
// 在下方插入一行
const insertRowBelow = () => {
if (!currentContextRow.value) return
const index = tableData.value.indexOf(currentContextRow.value)
if (index === -1) return
const newRow = createNewRow()
tableData.value.splice(index + 1, 0, newRow)
emit('row-added', { index: index + 1, row: newRow })
hideContextMenu()
}
// 打开插入多行对话框
const openInsertMultipleDialog = (below: boolean) => {
insertMultipleBelow.value = below
insertRowCount.value = 1
showInsertMultipleDialog.value = true
hideContextMenu()
}
// 插入多行
const insertMultipleRows = () => {
if (!currentContextRow.value) return
const index = tableData.value.indexOf(currentContextRow.value)
if (index === -1) return
const startIndex = insertMultipleBelow.value ? index + 1 : index
const newRows = Array.from({ length: insertRowCount.value }, createNewRow)
tableData.value.splice(startIndex, 0, ...newRows)
// 触发多个行添加事件
newRows.forEach((row, i) => {
emit('row-added', { index: startIndex + i, row })
})
showInsertMultipleDialog.value = false
}
// 删除当前行
const deleteCurrentRow = () => {
if (!currentContextRow.value) return
const index = tableData.value.indexOf(currentContextRow.value)
if (index === -1) return
ElMessageBox.confirm('确定要删除这一行吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const deletedRow = tableData.value.splice(index, 1)
emit('row-deleted', { index, row: deletedRow[0] })
ElMessage.success('删除成功')
hideContextMenu()
}).catch(() => {
// 用户取消删除
hideContextMenu()
})
}
4. 实现汇总行功能
汇总行可以自动计算数值列的总和:
typescript
// 汇总行计算方法
const getSummaries = (param: any) => {
// 如果提供了自定义汇总方法,使用自定义方法
if (props.summaryMethod) {
return props.summaryMethod(param)
}
const { columns, data } = param
const sums: string[] = []
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计'
return
}
// 只对数字类型的列进行汇总
const colConfig = props.columns.find(col => col.prop === column.property)
if (!colConfig || colConfig.type !== 'number') {
sums[index] = ''
return
}
// 计算总和
const values = data.map((item: any) => Number(item[column.property]))
if (values.every((value: any) => isNaN(value))) {
sums[index] = ''
} else {
const sum = values.reduce((prev: number, curr: number) => {
const value = Number(curr)
return isNaN(value) ? prev : prev + value
}, 0)
sums[index] = `${sum}`
}
})
return sums
}
// 行类名生成方法,用于设置汇总行样式
const tableRowClassName = ({ rowIndex }: { rowIndex: number }) => {
if (props.showSummary && rowIndex === tableData.value.length) {
return 'summary-row'
}
return ''
}
使用示例
现在让我们看看如何使用这个组件:
html
<!-- Example.vue -->
<template>
<div class="container">
<h1>可编辑表格示例</h1>
<EditableTable
:data="tableData"
:columns="columns"
:show-index="true"
:show-summary="true"
@update:data="handleDataUpdate"
@row-added="handleRowAdded"
@row-deleted="handleRowDeleted"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import EditableTable from './EditableTable.vue'
// 表格数据
const tableData = ref<any>([
{ id: 1, name: '张三', age: 25, gender: 'male', score: 85, birthdate: '1998-05-12' },
{ id: 2, name: '李四', age: 30, gender: 'female', score: 92, birthdate: '1993-08-24' },
{ id: 3, name: '王五', age: 28, gender: 'male', score: 78, birthdate: '1995-11-03' }
])
// 列配置
const columns = ref<any>([
{ prop: 'name', label: '姓名', width: '120px' },
{
prop: 'age',
label: '年龄',
type: 'number',
min: 0,
max: 150
},
{
prop: 'gender',
label: '性别',
type: 'select',
options: [
{ value: 'male', label: '男' },
{ value: 'female', label: '女' }
]
},
{
prop: 'score',
label: '分数',
type: 'number',
min: 0,
max: 100
},
{
prop: 'birthdate',
label: '出生日期',
type: 'date'
}
])
// 处理数据更新
const handleDataUpdate = (newData: any[]) => {
tableData.value = newData
console.log('数据已更新:', newData)
}
// 处理行添加事件
const handleRowAdded = ({ index, row }: { index: number; row: any }) => {
console.log(`在第 ${index} 行添加了新行:`, row)
}
// 处理行删除事件
const handleRowDeleted = ({ index, row }: { index: number; row: any }) => {
console.log(`删除了第 ${index} 行:`, row)
}
</script>
<style scoped>
.container {
padding: 20px;
}
</style>
功能扩展建议
这个组件基本的操作是够用的,你也可以根据实际需求进一步扩展:
- 数据验证 - 添加单元格数据验证功能,确保输入数据的正确性
- 撤销重做 - 实现操作历史记录,支持撤销和重做操作
- 批量操作 - 支持批量编辑和删除,提高操作效率
- 列配置 - 允许用户自定义显示哪些列,以及列的显示顺序
- 导入导出 - 支持 Excel 导入导出功能,方便数据交换
- 分页功能 - 集成分页支持大数据量,提高性能
- 行拖拽排序 - 支持通过拖拽调整行顺序
- 列宽调整 - 支持通过拖拽调整列宽度
EditableTable 组件完整代码
html
<template>
<el-table
:data="tableData"
@cell-dblclick="handleCellDblClick"
@row-contextmenu="handleRowRightClick"
:summary-method="getSummaries"
:row-class-name="tableRowClassName"
:border="border"
:show-summary="showSummary"
v-bind="$attrs"
>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
type="index"
label="序号"
align="center"
:resizable="false"
width="70"
/>
<!-- 动态列 -->
<el-table-column
v-for="column in columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:align="column.align || 'left'"
:width="column.width"
:resizable="column.resizable !== false"
>
<template #default="scope">
<!-- 数字输入框 -->
<el-input-number
v-if="column.type === 'number'"
v-model.number="scope.row[column.prop]"
:min="column.min || 0"
:max="column.max || 100"
:step="column.step || 1"
:precision="column.precision || 0"
:controls="column.controls !== false"
:disabled="isDisabled(column, scope.row)"
style="width: 100%"
/>
<!-- 单选下拉框 -->
<el-select
v-else-if="column.type === 'select'"
v-model="scope.row[column.prop]"
:multiple="column.multiple || false"
:multiple-limit="column.multipleLimit || 1"
:filterable="column.filterable !== false"
:clearable="column.clearable !== false"
:disabled="isDisabled(column, scope.row)"
:placeholder="column.placeholder || '请选择'"
style="width: 100%"
>
<el-option
v-for="item in column.options || []"
:key="item[column.valueKey || 'value']"
:label="item[column.labelKey || 'label']"
:value="item[column.valueKey || 'value']"
/>
</el-select>
<!-- 日期选择 -->
<el-date-picker
v-else-if="column.type === 'date'"
v-model="scope.row[column.prop]"
:type="column.dateType || 'date'"
:format="column.format || 'YYYY-MM-DD'"
:value-format="column.valueFormat || 'YYYY-MM-DD'"
:disabled="isDisabled(column, scope.row)"
:placeholder="column.placeholder || '选择日期'"
style="width: 100%"
/>
<!-- 普通文本显示 -->
<div
v-else-if="!scope.row[`${column.prop}_editing`]"
class="cell-text"
v-html="formatCellValue(scope.row[column.prop], column.formatter)"
/>
<!-- 编辑状态下的文本输入框 -->
<el-input
v-else
:ref="setInputRef(scope.$index, column.prop)"
v-model="scope.row[column.prop]"
:type="column.inputType || 'text'"
:autosize="{ minRows: 1, maxRows: 4 }"
:disabled="isDisabled(column, scope.row)"
@blur="scope.row[`${column.prop}_editing`] = false"
@keyup.enter="scope.row[`${column.prop}_editing`] = false"
/>
</template>
</el-table-column>
</el-table>
<!-- 右键菜单 -->
<div
v-show="showContextMenu"
id="context-menu"
class="context-menu"
@mouseleave="hideContextMenu"
>
<el-button type="primary" @click="insertRowAbove">上方插入一行</el-button>
<el-button @click="openInsertMultipleDialog(false)">上方插入多行</el-button>
<el-button type="primary" @click="insertRowBelow">下方插入一行</el-button>
<el-button @click="openInsertMultipleDialog(true)">下方插入多行</el-button>
<el-button type="danger" @click="deleteCurrentRow">删除当前行</el-button>
</div>
<!-- 插入多行对话框 -->
<el-dialog
v-model="showInsertDialog"
title="插入多行"
width="300px"
append-to-body
>
<el-input-number
v-model="insertRowCount"
:min="1"
:max="20"
style="width: 100%"
/>
<template #footer>
<el-button @click="showInsertDialog = false">取消</el-button>
<el-button type="primary" @click="insertMultipleRows">确定</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, nextTick, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
interface TableColumn {
prop: string
label: string
type?: 'text' | 'number' | 'select' | 'date'
width?: string | number
align?: 'left' | 'center' | 'right'
resizable?: boolean
min?: number
max?: number
step?: number
precision?: number
options?: any[]
valueKey?: string
labelKey?: string
multiple?: boolean
multipleLimit?: number
filterable?: boolean
clearable?: boolean
placeholder?: string
dateType?: string
format?: string
valueFormat?: string
inputType?: string
disabled?: boolean | ((row: any) => boolean)
formatter?: (value: any) => string
controls?: boolean
}
const props = defineProps({
// 表格数据
data: {
type: Array,
default: () => []
},
// 列配置
columns: {
type: Array as () => TableColumn[],
required: true
},
// 是否显示序号列
showIndex: {
type: Boolean,
default: true
},
// 是否显示边框
border: {
type: Boolean,
default: true
},
// 是否显示汇总行
showSummary: {
type: Boolean,
default: false
},
// 汇总方法
summaryMethod: {
type: Function,
default: null
},
// 禁止编辑的列
disabledColumns: {
type: Array as () => string[],
default: () => []
}
})
const emit = defineEmits(['update:data', 'row-added', 'row-deleted'])
// 表格数据
const tableData = ref<any[]>([])
// 显示右键菜单
const showContextMenu = ref(false)
// 当前右键的行信息
const currentContextRow = reactive({
index: null as number | null,
column: null as string | null,
isHeader: false
})
// 插入多行对话框
const showInsertDialog = ref(false)
// 插入行数
const insertRowCount = ref(1)
// 是否在下方插入
const insertBelow = ref(false)
// 输入框引用
const inputRefs = ref<Record<string, any>>({})
// 初始化表格数据
watch(() => props.data, (newData) => {
if (newData && newData.length > 0) {
tableData.value = newData.map(row => {
const safeRow = (typeof row === 'object' && row !== null) ? { ...row } : {}
props.columns.forEach(col => {
safeRow[`${col.prop}_editing`] = false
})
return safeRow
})
} else {
tableData.value = []
}
}, { immediate: true, deep: true })
// 创建新行数据
const createNewRow = () => {
const newRow: any = {}
props.columns.forEach(col => {
newRow[col.prop] = col.type === 'number' ? 0 : ''
newRow[`${col.prop}_editing`] = false
})
return newRow
}
// 设置输入框引用
const setInputRef = (rowIndex: number, prop: string) => (el: any) => {
inputRefs.value[`${rowIndex}-${prop}`] = el
}
// 单元格双击事件
const handleCellDblClick = (row: any, column: any) => {
const prop = column.property
if (!prop || isDisabledColumn(prop) || isDisabled(row, prop)) return
// 关闭其他单元格的编辑状态
props.columns.forEach(col => {
row[`${col.prop}_editing`] = false
})
// 开启当前单元格编辑状态
row[`${prop}_editing`] = true
// 聚焦输入框
nextTick(() => {
const inputKey = `${row.row_index}-${prop}`
const input = inputRefs.value[inputKey]
if (input) {
input.focus()
}
})
}
// 行右键事件
const handleRowRightClick = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
showContextMenu.value = false
// 定位右键菜单
const menu = document.getElementById('context-menu')
if (menu) {
menu.style.left = `${event.clientX}px`
menu.style.top = `${event.clientY}px`
}
showContextMenu.value = true
currentContextRow.index = row.row_index
currentContextRow.column = column.property
currentContextRow.isHeader = false
}
// 隐藏右键菜单
const hideContextMenu = () => {
showContextMenu.value = false
}
// 在上方插入一行
const insertRowAbove = () => {
if (currentContextRow.index === null) return
const newRow = createNewRow()
tableData.value.splice(currentContextRow.index, 0, newRow)
emit('row-added', { index: currentContextRow.index, row: newRow })
hideContextMenu()
}
// 在下方插入一行
const insertRowBelow = () => {
if (currentContextRow.index === null) return
const newRow = createNewRow()
tableData.value.splice(currentContextRow.index + 1, 0, newRow)
emit('row-added', { index: currentContextRow.index + 1, row: newRow })
hideContextMenu()
}
// 打开插入多行对话框
const openInsertMultipleDialog = (below: boolean) => {
insertBelow.value = below
insertRowCount.value = 1
showInsertDialog.value = true
}
// 插入多行
const insertMultipleRows = () => {
if (currentContextRow.index === null) return
const newRows = Array.from({ length: insertRowCount.value }, () => createNewRow())
const insertIndex = insertBelow.value ? currentContextRow.index + 1 : currentContextRow.index
tableData.value.splice(insertIndex, 0, ...newRows)
emit('row-added', {
index: insertIndex,
rows: newRows,
count: insertRowCount.value
})
showInsertDialog.value = false
hideContextMenu()
}
// 删除当前行
const deleteCurrentRow = () => {
if (currentContextRow.index === null) return
ElMessageBox.confirm('确定要删除这一行吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const deletedRow = tableData.value.splice(currentContextRow.index!, 1)
emit('row-deleted', { index: currentContextRow.index, row: deletedRow[0] })
hideContextMenu()
ElMessage.success('删除成功')
}).catch(() => {
hideContextMenu()
})
}
// 设置行索引
const tableRowClassName = ({ row, rowIndex }: { row: any, rowIndex: number }) => {
row.row_index = rowIndex
}
// 格式化单元格值
const formatCellValue = (value: any, formatter?: (value: any) => string) => {
if (formatter) {
return formatter(value)
}
if (value === null || value === undefined) {
return ''
}
return String(value).replace(/(\r\n|\n)/g, '<br/>')
}
// 检查列是否禁用
const isDisabledColumn = (prop: string) => {
return props.disabledColumns.includes(prop)
}
// 检查单元格是否禁用
const isDisabled = (column: TableColumn | string, row?: any) => {
if (typeof column === 'string') {
return isDisabledColumn(column)
}
if (column.disabled === undefined) {
return isDisabledColumn(column.prop)
}
if (typeof column.disabled === 'function') {
return column.disabled(row)
}
return column.disabled
}
// 汇总行计算方法
const getSummaries = (param: any) => {
if (props.summaryMethod) {
return props.summaryMethod(param)
}
const { columns, data } = param
const sums: string[] = []
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计'
return
}
const colConfig = props.columns.find(col => col.prop === column.property)
if (!colConfig || colConfig.type !== 'number') {
sums[index] = ''
return
}
const values = data.map((item: any) => Number(item[column.property]))
if (values.every((value: any) => isNaN(value))) {
sums[index] = ''
} else {
sums[index] = `${values.reduce((prev: number, curr: number) => {
const value = Number(curr)
return isNaN(value) ? prev : prev + value
}, 0)}`
}
})
return sums
}
</script>
<style scoped>
.cell-text {
width: 100%;
min-height: 100%;
word-break: break-word;
}
.context-menu {
position: fixed;
z-index: 9999;
background: white;
border: 1px solid #ebeef5;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 10px;
display: flex;
flex-direction: column;
gap: 5px;
}
.context-menu button {
width: 100%;
text-align: left;
}
::v-deep(.el-button+.el-button){
margin-left: 0;
}
</style>
总结
这个组件不仅提供了基本的数据展示功能,还支持多种编辑方式、右键操作菜单、自动汇总等功能。
关键实现要点:
- 使用动态渲染支持多种输入类型
- 利用双击和右键事件提供直观的操作方式
- 通过统一的 API 设计保证组件易用性
- 提供丰富的自定义配置选项
希望这个组件能够帮助你在实际项目中提高开发效率!如果你有任何问题或建议,欢迎在评论区留言讨论。
记得给文章点个赞,收藏起来,下次需要时可快速查找哦!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot 中的 7 种耗时统计方式,你用过几种?》
《Java8 都出这么多年了,Optional 还是没人用?到底卡在哪了?》