vxe-table 默认是单行编辑模式。通过"查看插槽 + 编辑插槽共用同一模板"的方式,可以伪实现多行同时编辑,并完整保留增删改查、数据校验等原生能力。多行校验通过 validConfig.msgMode: 'full' 设置,调用 fullValidate 即可一次性校验所有行。
vxe-table 如何实现多行同时编辑?
为每个可编辑列同时配置 editRender 与 slots.default,插槽内根据 row.isEdit 切换渲染输入组件或纯文本。点击"编辑"时设置 row.isEdit = true 并调用 setEditRow,即可让多行同时处于可编辑状态。
- 核心思路:
- editRender 保证列具备编辑能力与校验能力。
- slots.default 接管渲染:row.isEdit 为 true 时渲染输入组件,否则渲染纯文本。
- 每行独立维护 isEdit 与 isLoading 状态,互不干扰,因此可多行同时编辑。
- 生效前提:
- editConfig.mode 设为 'row',trigger 设为 'manual',由代码手动控制编辑状态。
- 行主键 rowConfig.keyField 必须唯一,否则编辑状态可能错乱。
- 每行的 isEdit、isLoading 需作为数据字段维护,确保响应式更新。
- 预期结果:点击某行"编辑"后该行变为输入组件,其他已处于编辑状态的行不受影响;可同时编辑多行,逐行保存或统一保存。
vxe-table 如何实现多行同时校验?
设置 validConfig.msgMode: 'full',调用 grid.fullValidate() 或 grid.fullValidate(row),会一次性返回所有校验不通过的行与规则,而不是只提示第一个错误。
- 生效前提:
- editRules 中为需要校验的字段配置规则。
- 校验前相关行应处于编辑状态或已被 setEditRow 激活,否则部分字段可能不参与校验。
- fullValidate() 不传参数时校验全表;传入 row 时校验指定行。
- 预期结果:返回 errMap 对象,键为行标识,值为该行的错误规则数组。校验不通过时,可将对应行重新置为编辑状态,方便用户定位修改。
代码
下面的配置实现了多行编辑、行内保存/取消/删除、批量新增/删除/保存,以及全量校验:

html
<template>
<div>
<vxe-button status="primary" @click="addEvent">新增</vxe-button>
<vxe-button status="error" @click="removeEvent">删除</vxe-button>
<vxe-button status="success" @click="saveEvent">保存</vxe-button>
<vxe-grid ref="gridRef" v-bind="gridOptions">
<template #name_default="slotParams">
<vxe-input v-if="slotParams.row.isEdit" v-model="slotParams.row.name" @change="changeEditEvent(slotParams)"></vxe-input>
<span v-else>{{ slotParams.row.name }}</span>
</template>
<template #sex_default="slotParams">
<vxe-select v-if="slotParams.row.isEdit" v-model="slotParams.row.sex" :options="sexList" @change="changeEditEvent(slotParams)"></vxe-select>
<span v-else>{{ slotParams.row.sex }}</span>
</template>
<template #age_default="slotParams">
<vxe-input v-if="slotParams.row.isEdit" v-model="slotParams.row.age" @change="changeEditEvent(slotParams)"></vxe-input>
<span v-else>{{ slotParams.row.age }}</span>
</template>
<template #action="{ row }">
<vxe-button v-if="row.isEdit" mode="text" status="success" :loading="row.isLoading" @click.stop="handleRowSave(row)">保存</vxe-button>
<vxe-button v-else mode="text" status="primary" @click.stop="handleRowEdit(row)">编辑</vxe-button>
<vxe-button v-if="row.isEdit" mode="text" @click.stop="cancelRowEdit(row)">取消</vxe-button>
<vxe-button mode="text" status="error" @click.stop="handleRowRemove(row)">删除</vxe-button>
</template>
</vxe-grid>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { VxeUI } from 'vxe-pc-ui'
import XEUtils from 'xe-utils'
const gridRef = ref()
const sexList = ref([
{ label: '男', value: 'Man' },
{ label: '女', value: 'Women' }
])
const gridOptions = reactive({
border: true,
showOverflow: true,
keepSource: true,
rowConfig: {
keyField: 'id'
},
editConfig: {
trigger: 'manual',
mode: 'row',
showStatus: true
},
validConfig: {
msgMode: 'full'
},
editRules: {
name: [
{ required: true, message: '必须填写' }
],
sex: [
{ required: true, message: '必须填写' }
]
},
columns: [
{ type: 'checkbox', width: 60, align: 'center' },
{ type: 'seq', width: 70 },
{ field: 'name', title: 'Name', editRender: { name: 'VxeInput' }, slots: { default: 'name_default' } },
{ field: 'sex', title: 'Sex', editRender: { name: 'VxeSelect', options: sexList.value }, slots: { default: 'sex_default' } },
{ field: 'age', title: 'Age', editRender: { name: 'VxeInput' }, slots: { default: 'age_default' } },
{ field: 'action', title: '操作', width: 160, slots: { default: 'action' } }
],
data: []
})
const loadList = () => {
// 模拟后端接口
gridOptions.loading = true
setTimeout(() => {
gridOptions.data = [
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'test abc', isEdit: false, isLoading: false },
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou', isEdit: false, isLoading: false },
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai', isEdit: false, isLoading: false },
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai', isEdit: false, isLoading: false }
]
gridOptions.loading = false
}, 200)
}
const handleRowEdit = (row) => {
const $grid = gridRef.value
if (!$grid) {
return
}
row.isEdit = true
// 激活不自动聚焦
$grid.setEditRow(row)
// 激活并自动聚焦第一个可编辑列
// $grid.setEditRow(row, true)
}
const cancelRowEdit = async (row) => {
const $grid = gridRef.value
if (!$grid) {
return
}
row.isEdit = false
$grid.clearEdit(row)
$grid.clearValidate(row)
}
const handleRowRemove = async (row) => {
const $grid = gridRef.value
if (!$grid) {
return
}
$grid.remove(row)
}
const handleRowSave = async (row) => {
const $grid = gridRef.value
if (!$grid) {
return
}
const errMap = await $grid.fullValidate(row)
if (errMap) {
// 将校验不通过行显示编辑状态
XEUtils.each(errMap, errRules => {
errRules.forEach(errRule => {
handleRowEdit(errRule.row)
})
})
VxeUI.modal.message({
status: 'error',
content: '校验不通过!'
})
return
}
row.isLoading = true
// 模拟后端接口
setTimeout(() => {
VxeUI.modal.message({
status: 'success',
content: '保存成功'
})
row.isLoading = false
cancelRowEdit(row)
}, 300)
}
const addEvent = async () => {
const $grid = gridRef.value
if (!$grid) {
return
}
const record = {
isEdit: false,
isLoading: false
}
const { row: newRow } = await $grid.insert(record)
handleRowEdit(newRow)
}
const removeEvent = async () => {
const $grid = gridRef.value
if (!$grid) {
return
}
const selectRecords = $grid.getCheckboxRecords()
if (selectRecords.length > 0) {
const type = await VxeUI.modal.confirm({
title: '系统提示',
content: '请您确认是否删除?'
})
if (type === 'confirm') {
$grid.removeCheckboxRow()
VxeUI.modal.message({
content: '已删除选中',
status: 'success'
})
}
} else {
VxeUI.modal.message({
content: '未选择数据',
status: 'info'
})
}
}
const saveEvent = async () => {
const $grid = gridRef.value
if (!$grid) {
return
}
const errMap = await $grid.fullValidate()
if (errMap) {
// 将校验不通过行显示编辑状态
XEUtils.each(errMap, errRules => {
errRules.forEach(errRule => {
handleRowEdit(errRule.row)
})
})
VxeUI.modal.message({
status: 'error',
content: '校验不通过!'
})
return
}
const { insertRecords, updateRecords, removeRecords } = $grid.getRecordset()
if (insertRecords.length > 0 || updateRecords.length > 0 || removeRecords.length > 0) {
console.log(`新增:${insertRecords.length} 行,修改:${updateRecords.length} 行,删除:${removeRecords.length} 行`)
loadList()
VxeUI.modal.message({
content: '保存成功',
status: 'success'
})
} else {
VxeUI.modal.message({
content: '数据未改动',
status: 'warning'
})
}
}
const changeEditEvent = (slotParams) => {
const $grid = gridRef.value
if ($grid) {
$grid.updateStatus(slotParams)
}
}
loadList()
</script>
- 预期结果:
- 点击某行"编辑",该行切换为输入组件,其他行可继续编辑,支持多行同时处于编辑状态。
- 行内"保存"仅校验当前行;顶部"保存"校验全表,失败时自动将错误行置为编辑状态并提示。
- "新增"插入一行并立即进入编辑;"删除"支持行内删除与勾选批量删除。
关键配置参数
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| editConfig.mode | String | 'cell' | 设为 'row' 表示行编辑模式 |
| editConfig.trigger | String | 'click' | 设为 'manual' 由代码控制编辑状态 |
| editConfig.showStatus | Boolean | false | 是否显示编辑状态样式 |
| validConfig.msgMode | String | 'single' | 设为 'full' 返回全部错误,支持多行校验 |
| rowConfig.keyField | String | --- | 行主键字段,必须唯一 |
| keepSource | Boolean | false | 保留原始数据,getRecordset() 依赖此配置 |
| row.isEdit | Boolean | false | 自定义字段,控制当前行是否处于编辑态 |
| row.isLoading | Boolean | false | 自定义字段,控制行内保存按钮的加载状态 |
常用方法
| 方法 | 说明 |
|---|---|
| $grid.setEditRow(row) | 将指定行置为编辑状态;第二参数传 true 时自动聚焦第一个可编辑列 |
| $grid.clearEdit(row) | 清除指定行的编辑状态 |
| $grid.clearValidate(row) | 清除指定行的校验提示 |
| $grid.fullValidate(row?) | 校验全表或指定行,返回 errMap;msgMode: 'full' 时返回全部错误 |
| $grid.insert(record) | 插入一行,返回 { row } |
| $grid.remove(row) | 删除指定行 |
| $grid.removeCheckboxRow() | 删除复选框选中的行 |
| $grid.getCheckboxRecords() | 获取复选框选中的行 |
| $grid.getRecordset() | 获取变更集:{ insertRecords, updateRecords, removeRecords } |
| $grid.updateStatus(slotParams) | 更新行状态,标记数据已修改 |
通过以上就能直接实现多行可编辑表格