初衷
正常情况下如果我们需要在
element-ui
或element-plus
的表格中实现行内编辑功能,需要使用<template #default="{ row }">
,然后通过v-if
去判断显示span
还是el-input
。如果行内多处需要进行行内编辑时,就明显的增加了代码量。(当然最原始的初衷就是摸鱼时看看能不能实现)
优缺点
优点:template的代码量减少,每个table自己维护自己的编辑方法,代码逻辑统一封装,使用也很方便;现已支持使用element组件或者自定义的组件。
指令具体实现
代码中提供了注释,目前只支持
input
输入类型。思考过动态插入element
的表单组件,实力有限试过很多方法后始终有问题。
/**
* 表格编辑指令
* @param {Array} value - [editableList, props, save, getList]
* @param {Array} editableList - 可编辑列的索引
* @param {Array} props - 可编辑列对应的prop名称列表
* @param {Function} save - 保存编辑的方法
* @param {Function} getList - 获取列表的方法
* @example
* <el-table v-table-edit="[editableList, props, save, getList]">
* <el-table-column prop="name" label="姓名"></el-table-column>
* <el-table-column prop="age" label="年龄"></el-table-column>
* <el-table-column label="操作">
* 只需要完成编辑按钮的逻辑
* 具体可参考/geneGenieDx/dataset代码
* </el-table-column>
* </el-table>
* @description
* 待更新功能:
* a. 支持其他类型的编辑列的输入框,目前只支持文本框
* b. 分页会带着编辑的状态,需要优化;(已解决:应该有些状态需要通过store去管理)
* c. edit-cancel然后分页之后,会导致数据错乱(已解决:设置font-size=0px)
*/
import _ from 'lodash'
import { parse, stringify } from 'flatted'
import modal from '@/plugins/modal'
import store from '@/store'
import i18n from '@/lang'
const { t } = i18n.global
let editableList = []
let targetRow = null
// 连续点击了多行的编辑按钮后,只有最后一行的编辑状态会被保存,multiRowEdit用于区分这种情况下的调用
function updateRowDom(targetRow, editableList, multiRowEdit = true) {
Array.from(targetRow.children).forEach((item, i) => {
if (editableList.includes(i)) {
item.children[0].style.fontSize = '14px'
item.children[0].lastElementChild?.tagName === 'INPUT' && item.children[0].lastElementChild.remove()
}
})
if (multiRowEdit) {
targetRow = null
store.dispatch('table/setEditId', '')
}
}
export default {
mounted(el, bind) {
const { value } = bind
const [indexList, props, save, getList] = value
let newRow = {}
editableList = indexList
el.addEventListener('click', e => {
if (['Edit', 'edit', '编辑'].includes(e.target.innerText)) {
// 多次点击不同行的编辑清空上一次的编辑状态
store.state.table.editId && targetRow && updateRowDom(targetRow, editableList, false)
// 编辑
const rowData = store.state.table.rowData
newRow = parse(stringify(rowData.row))
targetRow = e.querySelectorAll('tbody .el-table__row')[rowData.index]
Array.from(targetRow.children).forEach((item, i) => {
if (editableList.includes(i)) {
const input = document.createElement('input')
input.type = rowData.inputType
input.value = newRow[props[i]]
input.style.width = '100%'
input.style.padding = '4px 6px'
input.style.boxSizing = 'border-box'
input.style.outline = 'none'
input.style.fontSize = '14px'
input.onblur = e => {
newRow[props[i]] = e.target.value
}
item.children[0].style.fontSize = '0px'
// item.children[0].innerHTML = ''
item.children[0].appendChild(input)
}
})
} else if (['Save', 'save', '保存'].includes(e.target.innerText)) {
// 保存编辑
save(newRow).then(() => {
updateRowDom(targetRow, editableList)
modal.msgSuccess(t('pub.operationSuc'))
getList()
}).catch(() => {
updateRowDom(targetRow, editableList)
})
} else if (['Cancel', 'cancel', '取消'].includes(e.target.innerText)) {
// 取消编辑
updateRowDom(targetRow, editableList)
}
})
},
beforeUpdate() {
if (store.state.table.pageChange && targetRow) {
updateRowDom(targetRow, editableList)
store.dispatch('table/setPageChange', false)
}
}
};
通过store管理编辑状态
// store.js
const state = {
pageChange: false,
editId: '',
rowData: {},
}
const mutations = {
SET_PAGE_CHANGE: (state, view) => {
state.pageChange = view
},
SET_EDIT_ID: (state, view) => {
state.editId = view
},
SET_ROW_DATA: (state, view) => {
state.rowData = view
},
}
const actions = {
setPageChange({ commit }, view) {
commit('SET_PAGE_CHANGE', view)
},
setEditId({ commit }, view) {
commit('SET_EDIT_ID', view)
},
setRowData({ commit }, view) {
commit('SET_ROW_DATA', view)
}
}
通过hook(composable)对store的操作进行封装
export const useTableEdit = (keyName) => {
const store = useStore()
const editId = computed(() => store.state.table.editId)
const handleEdit = (row, index, inputType = 'text') => {
store.dispatch('table/setEditId', row[keyName])
store.dispatch('table/setRowData', { row, index, inputType })
}
return {
editId,
handleEdit,
}
}
使用例子
参数说明见指令的注释
// template
<el-table v-tableEdit="[[0, 1], ['caseSetName', 'descInfo'], saveRow, getCases]" :data="tableData">
<el-table-column :label="t('task.dataset')" prop="caseSetName" min-width="100">
</el-table-column>
<el-table-column :label="t('sample.description')" prop="descInfo" min-width="200">
<template #default="{ row }">
<span>{{ row.descInfo || '-' }}</span>
</template>
</el-table-column>
<el-table-column :label="t('sample.number')" prop="caseNum" />
<el-table-column :label="t('sample.creator')" prop="createBy" />
<el-table-column :label="t('pub.operates')" width="200">
<template #default="{ row, $index }">
<el-button link class="del-text" @click="delCase(row.caseSetId, $index)">{{
t('pub.delete')
}}</el-button>
<el-button
v-if="editId !== row.caseSetId"
link
@click="handleEdit(row, $index)"
>{{t('pub.edit')}}</el-button>
<template v-else>
<el-button type="primary" link>{{ t('pub.save') }}</el-button>
<el-button link>{{ t('pub.cancel') }}</el-button>
</template>
</template>
</el-table-column>
</el-table>
// script
// 使用hooks提供的useTableEdit
const { editId, handleEdit } = useTableEdit('caseSetId')
支持使用element组件、自定义组件等
从
document.createElement('input')
开始的一段创建input的逻辑都可以使用render
函数去使用element-plus
的组件。还可以在render
中添加一个type
参数用于判断编辑时的表单类型,具体实现可以自己尝试一下
import { h, createApp } from 'vue'
import { ElInput } from 'element-plus'
function render(target, value) {
const app = createApp({
render: () => h(
ElInput,
{
modelValue: value,
'onUpdate:modelValue': (nVal) => emit('update:modelValue', nVal)
},
),
})
const div = document.createElement('div')
app.mount(div)
target.appendChild(div)
}
展望与未来
save
的按钮其实可以通过input
的回车事件实现,如果有其他表单可能就不行了,比如说select
之类的。