使用react我们可以用ant.design的
EditableProTable
,现成的行内编辑组件,并且封装的很好,我之前也有项目使用过,虽然有的地方配置灵活度不够高,但是自己稍微做些改动也能使。那么使用vue的话怎么办呢?vue常用的组件库element
不论什么版本并没有提供表格的行内编辑功能,在github也没有搜到符合自己设想的案例,有一些要么就是验证做的不好,要么就是交互方式不是自己想要的,接下来我们就按照 design的EditableProTable
来使用vue针对element
封装一层,完成灵活的行内编辑。
如何编辑?
首先我们要知道什么是行内编辑, 这里我们先来看 antd
的 可编辑表格 是什么样子。
- 这里是我在项目中使用到的,使用在了业务中的修改以及新增功能中, 其强大的优势就是可以使用
jsx
语法作为render
自己对行内可编辑的组件进行定制化~ - 使用jsx 来对行内编辑 和 非编辑状态渲染进行重置 更多详细的功能可以点击上面的超链接,访问
antdPro
的文档进行查看即可。
那么我们需求就是,想要一个和上面这种差不多的一个行内编辑的功能,但是是使用vue来搞,目前我这个项目使用的是 vite + vue3 + elementPlus, 最后可以到我的代码仓库查看示例 - 在这之前我在网上搜了下,看有没有别人开源出来的一些可编辑的方案, 要么就是这种,可编辑的交互是点击对应的行,弹出提示编辑 要么就是这种只做了最简单的封装,表单验证,插槽等都没有做 再有这种可编辑在表头的,属于是可配置的搜索列了
- 当然还有一个最终极的方案,就是使用 ag-grid, 这是属于找到的可编辑表格的天花板了,试了一下他的
demo
性能也优化的很流畅, 不过其功能过于强大,而文档又没有中文文档,社区对其深入研究的案例也不多,而且针对很多功能是要付费的,比如下面的 e 就是付费使用,我们也不是要搞一个多么强大的,所以最终没有选择这个。
二次封装
其实我这里应该属于三次封装了哈哈,因为我是在社区上已经实现的把 Element
进行了一次二次封装基础之上进行改造,链接===> juejin.cn/post/716606... , 我没有实现 EditableProTable
的多行编辑,因为多行编辑处理的状态可能更多更复杂,而我的业务对于这类需求又不强烈,暂时先不考虑。
- 这里最核心的就是,点击编辑按钮,切换到可编辑的组件,如下
javaScript
<el-table-column
v-bind="item"
v-slot="scope"
:showOverflowTooltip="
item.showOverflowTooltip ?? item.prop !== 'operation'
"
:sortable="item.sortable"
:sort-method="(a, b) => a.prop - b.prop"
v-if="!item.type && item.prop && item.prop !== 'operation'"
:align="item.align ?? 'center'"
>
<EditTableColumn
:column="item"
:realTimeVerification="realTimeVerification"
:tableData="scope.row"
:editData="editData"
@validateHandle="validateHandle"
v-if="editData && editData.id === scope.row.id"
/>
<TableColumn v-else :column="item" :tableData="scope.row" />
</el-table-column>
这里我看首先要清楚, 这个 el-table-column
是什么,没错,他就是我们的每一行,其中的item
就是我们 column
中的每一个对象了, 我在这里都是只简述关键业务逻辑,全部代码请 clone
自行查看。 那么我们就需要判定这一列在什么情况下是可编辑的 什么情况下是不可编辑的, 这里使用 editData
, 当点击编辑按钮的时候,获取当前这一数据,并且存储, 我们拿当前存储的可编辑这一行的数据去和当前表格对比, 这里使用的是 id, 源于我们的列表数据正常情况下必带的是 id
而且唯一。
- 第二核心就是表单验证,可编辑组件的模块如下
javaScript
<!--
* @Description: 可编辑行部分的单独配置
* @Autor: codeBo
* @Date: 2023-05-15 15:37:52
* @LastEditors: gjzxlihaibo@163.com
* @LastEditTime: 2023-07-25 15:49:01
-->
<template>
<div>
<el-form
:model="ruleForm"
ref="ruleFormRef"
:rules="rules"
:show-message="false"
@validate="validateHandle"
>
<template v-if="column.valueType">
<el-form-item :prop="column.prop">
<el-tooltip
placement="top"
effect="dark"
:visible="realTimeVerification && visible"
:content="errMsg || '请输入'"
>
<el-input
v-bind="$props.column.editable"
v-if="column.valueType === ValueType.Input"
v-model="ruleForm[column.prop]"
/>
<!-- number 组件有bug, change 事件无法实时捕获, input事件无法捕获到清空以后得错误, 只能使用 blur 事件 -->
<el-input-number
v-bind="$props.column.editable"
v-else-if="column.valueType === ValueType.InputNumber"
v-model="ruleForm[column.prop]"
/>
<el-select
v-bind="$props.column.editable"
v-else-if="column.valueType === ValueType.Select"
v-model="ruleForm[column.prop]"
>
<el-option
v-for="item in $props.column.enum"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div v-else-if="column.valueType === ValueType.DateTimePicker">
<el-date-picker
style="width: 100%"
v-bind="$props.column.editable"
v-model="ruleForm[column.prop]"
placeholder="请选择时间"
/>
</div>
</el-tooltip>
</el-form-item>
</template>
<div v-else>{{ tableData && renderCellData(column, tableData) }}</div>
</el-form>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
inject,
ref,
reactive,
onMounted,
} from 'vue'
import { filterEnum, formatValue, handleRowAccordingToProp } from '../utils'
import { ColumnProps, ValueType } from '../types'
import type { FormInstance, FormItemProp } from 'element-plus'
/**
* 每个组件事件类型 支持两种,一个是 change 一个是 blur ,改变和失焦
*/
export default defineComponent({
emits: ['validateHandle'],
props: {
column: {
type: Object,
required: true,
},
tableData: {
type: Object,
},
editData: {
type: Object,
default: () => null,
},
realTimeVerification: {
type: Boolean,
required: true,
},
editableProps: {
type: Object,
},
},
/**
*
* @param props
*
*/
setup(props, { emit }) {
// 表格部分
const column = computed(() => props.column)
const tableData = computed(() => props.tableData)
const enumMap = inject('enumMap', ref(new Map()))
// 渲染表格数据, 增加处理层级属性的能力 如 props: config.id
const renderCellData = (item: ColumnProps, scope: any) => {
// 增加处理下拉框多选时候, 值是数组的情况
if (Array.isArray(handleRowAccordingToProp(scope, item.prop!))) {
return handleRowAccordingToProp(scope, item.prop!)
}
return enumMap.value.get(item.prop) && item.isFilterEnum
? filterEnum(
handleRowAccordingToProp(scope, item.prop!),
enumMap.value.get(item.prop)!,
item.fieldNames,
)
: formatValue(handleRowAccordingToProp(scope, item.prop!))
}
// 表单部分
const ruleFormRef = ref<FormInstance>() // 表单实例
const rules = reactive<any>(column.value.rules || {}) // 验证规则
const ruleForm = reactive<Record<string, any>>({
[column.value.prop]: renderCellData(column.value, tableData.value),
}) // 表单绑定的数据
// 可编辑状态的类型 ValueType
if (column.value.valueType) {
console.log('prop', column.value, ruleForm)
}
const visible = ref(false) // 是否显示错误提示
const errMsg = ref<any>('') // 错误信息
const changeFields = () => {
if (!props.realTimeVerification) return
ruleFormRef.value?.validateField(column.value.prop, (result) => {
visible.value = !result
})
}
onMounted(() => {
// 组件挂载以后 ,验证一次,再分别把可编辑的 实例传给父组件
ruleFormRef.value?.validateField(column.value.prop, (result) => {
visible.value = !result
emit(
'validateHandle',
{
[column.value.prop]: result,
},
ruleForm,
ruleFormRef.value,
)
})
})
// 表单项被效验以后触发
const validateHandle = (
prop: FormItemProp,
isValid: boolean,
message: string,
) => {
errMsg.value = message
visible.value = !isValid
}
return {
renderCellData,
validateHandle,
changeFields,
ValueType,
visible,
ruleFormRef,
errMsg,
rules,
ruleForm,
}
},
})
</script>
<style lang="scss" scoped>
:deep(.el-form-item) {
margin-bottom: 0;
}
</style>
因为我们这里的表单是独立的,也就是可编辑行中 每一个可编辑组件就是一个表单,源于我们使用的是二次封装以后的设计,这里面对tableColumns
进行了 for
循环, 所以在表单验证的过程中,这里我在组件挂载的时候会主动触发一次验证, 接下来就是侦听 form
的 validate
事件, 每一次验证都触发,给父组件进行处理。
javaScript
// 同步子组件编辑行数据各类状态
const validateHandle = (prop: any, data: any, ref: any) => {
editRef.value.push(ref)
Object.assign(editDataValidate.value, prop)
editAfterData.value.push(data)
}
父组件在验证触发的时候, 需要同步几个数据,1是ref,2是编辑之前的数据,3是编辑之后的数据
- 第三核心就是保存的时候如何同步
javaScript
// 保存 需刷新页面 因为 validate 是异步验证,所以用一个 Promise 队列 同步 await
const handleSave = async (oldValue: any) => {
const validatePromises: Promise<void>[] = []
const resultObj: any = {
data: {},
err: [],
}
editRef.value.forEach((item: any) => {
const validatePromise = new Promise<void>((resolve) => {
item?.validate((result: boolean, err: any) => {
if (!result) {
resultObj.err.push(err)
}
resolve()
})
})
validatePromises.push(validatePromise)
})
editAfterData.value.forEach((item: any) => {
Object.assign(resultObj.data, item)
})
await Promise.all(validatePromises)
props.editSave(resultObj, oldValue, clearEditData)
}
这里就是点击保存的事件,因为从点击编辑按钮开始,我们就已经与可编辑组件的每一个数据实时同步,保存的时候只需要依次处理即可。
展示
就拿上面这张图简单介绍一下:
- 点击编辑以后, 按钮变成 保存和取消, 后面 分配权限的按钮是插槽,增加了配置项,可以控制这里在这两个编辑按钮的前面还是后面。
- 只允许当行编辑,当有未保存的数据时,点击其他的编辑按钮会提示请保存。
ruls
规则同form
相同,验证仅支持两种事件,change
和blur
。
- 更多详细的介绍我这里就不赘述了,如下面的截图一样,在我项目的代码里面,有更多的解释, 这里有很多
any
,ts
还没完善,请见谅哈哈, 后面有可能会再次完善和更新,敬请期待。
此 demo
在页面中的角色管理部分。
项目地址:gitee.com/li-haibo-19... 分支 lowCode
。
特别鸣谢: @白哥学前端 我前进路上最大的导师。