基于element ui 实现编辑表格,支持文本、下拉、日期、链接等形式,实现了回车到下一个输入框,每一个回车都可以触发业务回车事件,例如计算小计;最后一格回车可以触发自动增行等效果。
实现效果如下
组件入口 index.vue
<template>
<my-form :model="tableDataTmp" ref="tableForm" class="edit-table-dialog" @submit.native.prevent :class="{'edit-table-disabled': disabled}">
<el-row justify="end" type="flex">
<div class="title-tag" v-if="title">{{ title }}</div>
<slot name='btn-more-before'></slot>
<el-button type="primary" plain size="small" icon="el-icon-plus" @click="addRow" v-if="!disabled && (options.showAddBtn==undefined || options.showAddBtn==null || options.showAddBtn)">新增行</el-button>
<el-button type="primary" plain size="small" icon="el-icon-delete" @click="deleteRow" v-if="!disabled && (options.showDelBtn==undefined || options.showDelBtn==null || options.showDelBtn)">删除行</el-button>
<slot name="btn-more"></slot>
</el-row>
<el-table ref="editTableRef" border :data="tableDataTmp.tableData" row-key="__rowKey" @selection-change="selectionChange" @select="handleSelect" class="edit-table" :row-class-name="tableRowClassName" :height="height" empty-text=" ">
<template v-for="item in columnData">
<column ref="columnRef" :key="item.label" :columnInfo="item" :options="options" :tableRules="tableRules" :rowKey="rowKey" :defaultShowEdit="defaultShowEdit"
:selectList="selectList" :isDiff="isDiff" :disabled="disabled" :editIndex="editIndex" @editFocus="editFocus" @keyupEnter="keyupEnter" @onfocus="onfocus" @link="clickLink"></column>
</template>
</el-table>
<table-dialog ref="tableDialogRef" :open.sync="open" :tabName="tabName" :tabParams="tabParams" :hideColumns="hideColumns" @oncheck="oncheck"></table-dialog>
</my-form>
</template>
<script>
import MyForm from './form/form.vue'
import Column from './column.vue'
import TableDialog from '@/components/TableDialog/index.vue'
export default {
name: "EditTable",
components: { TableDialog, MyForm, Column },
props: {
height: {
type: [String, Number]
},
columnData: {
type: Array,
default: () => {
return []
}
},
emptyRow: {
type: Object,
default: () => {
return {}
}
},
tableData: {
type: Array,
default: () => {
return []
}
},
tableRules: {
type: Object,
default: () => {
return {}
}
},
options: {
type: Object,
default: () => {
return {
showAddBtn: true,
showDelBtn: true
}
}
},
rowKey: {
type: [String, Function]
},
defaultShowEdit: {
type: Boolean,
default: false
},
// 是否显示比对
isDiff: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
},
data() {
return {
tableDataTmp: {
tableData: []
},
// 末级列数据
columnDataLast: [],
tabName: null,
tabParams: {},
hideColumns: null,
editIndex: '', // 当前编辑单元格
open: false,
chooseData: {},
selections: [],
rowids: [],
selectList: [],
isEnter: false // 是否回车
}
},
created() {},
watch: {
'tableData': {
handler() {
let rowKeyTmp = new Date().getTime()
this.tableData.forEach((tb, index) => {
tb.rowEdit = (tb.rowEdit === undefined || tb.rowEdit === null) ? true : tb.rowEdit
if (!tb.__rowKey) {
tb.__rowKey = tb[this.rowKey] || (rowKeyTmp+'_'+index)
}
});
this.tableDataTmp = { tableData: this.tableData }
},
immediate: true
},
'columnData': {
handler() {
let columnDataLast = []
let func = (list, level) => {
list.forEach((item, index)=>{
if (item.children && item.children.length>0) {
func(item.children, level+'_'+index)
} else {
columnDataLast.push({ ...item, level__: level+'_'+index })
// 如果没有设置renderHeader,且是必填字段,添加必填星号
if (this.tableRules[item.prop] && !item.renderHeader) {
let valIndex = this.tableRules[item.prop].findIndex((validate)=>{
return validate.required
})
if (valIndex > -1) {
item.renderHeader = (h, data)=>{
return [
h('span', { style: 'color: #F56C6C;margin-right: 5px;' }, '*'),
h('span', item.label)
]
}
}
}
// 如果是dataSelect类型,预处理数据
if (item.editType === 'dataSelect') {
// let _options = { ...item.selectData, headers: { hideMessage: true } }
// request(_options).then((rsp)=>{
// this.$set(this.cacheSelectData, item.prop, rsp.data)
// })
}
}
})
}
func(this.columnData, 'a')
this.columnDataLast = columnDataLast
},
deep: true,
immediate: true
}
},
methods: {
// 表格行样式
tableRowClassName({row, rowIndex}) {
if (!this.isDiff) return ''
if (row.DELROW) {
return 'warning-row';
} else if (row.ADDROW) {
return 'success-row';
}
return '';
},
addRow() {
let index = this.tableDataTmp.tableData.length
let row = { __rowKey: new Date().getTime() }
if(this.emptyRow) row = { ...row, ...this.emptyRow }
this.tableDataTmp.tableData.splice(index, 0, row)
// this.$emit('update:tableData', this.tableDataTmp.tableData)
},
deleteRow() {
if (!this.rowids || this.rowids.length==0) {
this.$modal.msgError("请选择要删除的行");
return
}
this.rowids.forEach((id)=>{
let index = this.tableDataTmp.tableData.findIndex(item=>(item.__rowKey==id))
this.tableDataTmp.tableData.splice(index, 1)
})
// this.$emit('update:tableData', this.tableDataTmp.tableData)
},
selectionChange (selections) {
this.selections = selections
this.rowids = this.selections.map(item=>item.__rowKey)
this.$emit('selection-change', selections)
},
handleSelect(selection, row) {
this.$emit('select', selection, row)
},
getSelection () {
return this.selections
},
selectSame (sameList, checked) {
this.$nextTick(()=>{
sameList.forEach(item=>{
this.$refs.editTableRef.toggleRowSelection(item, checked)
})
})
},
onfocus(index_) {
setTimeout(async () => {
if (this.disabled) return false
this.editIndex = index_
const { rowIndex, colIndex, row, columnConfig, columnValue } = this.getCommonData()
if (columnConfig.editType === 'select') {
row[columnConfig.valprop || columnConfig.prop] = null
this.selectList = await columnConfig.selectData.getData(row)
row[columnConfig.valprop || columnConfig.prop] = columnValue
}
this.$nextTick(() => {
const editEl = this.getElementRef(rowIndex, colIndex)
if (columnConfig.editType === 'select') {
editEl.toggleMenu()
} else {
editEl && editEl.focus()
}
})
}, 200) // 日期回车下一个是下拉时,触发了下拉的回车事件,触发了两次回车
},
getElementRef (rowIndex, colIndex) {
let columnConfig = this.columnDataLast[colIndex]
let levels = columnConfig.level__.split('_')
let editEl = this
for(let n = 1;n<levels.length;n++) {
editEl = editEl.$refs['columnRef'][parseInt(levels[n])]
}
editEl = editEl.$refs['flaga-' + rowIndex + '-' + columnConfig.prop].$children[1]
// if (columnConfig.editType === 'dataSelect') {
// editEl = editEl.$children[0]
// }
return editEl
},
getCommonData () {
let indexs = this.editIndex.split('-')
let rowIndex = parseInt(indexs[0])
let colProp = indexs[1]
let colIndex = this.columnDataLast.findIndex((item)=>item.prop===colProp)
let columnConfig = this.columnDataLast[colIndex]
let row = this.tableDataTmp.tableData[rowIndex]
let columnValue = columnConfig.editType === 'select' ? row[columnConfig.valprop || columnConfig.prop] : row[columnConfig.prop]
return {rowIndex, colProp, colIndex, columnConfig, row, columnValue}
},
// 进入下一个可编辑单元格
nextStep() {
if (!this.editIndex) {
return false
}
let { rowIndex, colIndex, columnConfig } = this.getCommonData()
let nextColIndex = -1
// 查找当前行下一个可编辑项
if (colIndex < this.columnDataLast.length - 1) {
for (let ind = colIndex + 1; ind < this.columnDataLast.length; ind++) {
if (this.columnDataLast[ind].edit && this.columnDataLast[ind].editType !== 'link') {
nextColIndex = ind
columnConfig = this.columnDataLast[ind]
break
}
}
}
let editEl = this.getElementRef(rowIndex, colIndex)
// 找到下一个编辑列
if (nextColIndex !== -1) {
editEl.blur()
this.onfocus(rowIndex + '-' + columnConfig.prop)
}
// 未找到判断是否有下一行,有下一行进入下一行第一个
else if (rowIndex < this.tableDataTmp.tableData.length - 1) {
let nextRowIndex = this.tableDataTmp.tableData.findIndex((row, index)=>{
return index > rowIndex && row.rowEdit
})
if (nextRowIndex > -1) {
for (let ind = 0; ind < this.columnDataLast.length; ind++) {
if (this.columnDataLast[ind].edit && this.columnDataLast[ind].editType !== 'link') {
nextColIndex = ind
columnConfig = this.columnDataLast[ind]
break
}
}
// const editEl = this.getElementRef(rowIndex, colIndex)
editEl.blur()
this.onfocus(nextRowIndex + '-' + columnConfig.prop)
}
}
},
// 聚焦
editFocus(index_) {
this.editIndex = index_
// let {rowIndex, colIndex, columnConfig, row, columnValue} = this.getCommonData()
// let editEl = this.getElementRef(rowIndex, colIndex)
// if (columnConfig.editType === 'select') {
// editEl.toggleMenu()
// } else {
// editEl.focus()
// }
},
next () {
let {rowIndex, colIndex, columnConfig, row, columnValue} = this.getCommonData()
if (columnConfig.editType === 'date') {
this.nextStep()
return
} else if (columnConfig.editType === 'dialog') {
this.nextStep()
} else {
this.nextStep()
}
},
// 回车事件
keyupEnter(isNext=true, e) {
// console.log('====', isNext, this.isEnter, e)
if (isNext) {
this.isEnter = true
} else if (!isNext) {
let curField = this.getCommonData()
// 展示下拉项时,不要执行操作 | dialog失焦时不执行操作
if (curField.columnConfig.editType === 'select' && e == true) {
return false
} else if (curField.columnConfig.editType === 'dialog') {
return false
}
// 如果当前已标记为回车,不执行接下来的操作
if (this.isEnter) {
this.isEnter = false
return
}
// 下面这段逻辑暂时好像不需要
// else {
// let curField = this.getCommonData()
// if (curField.columnConfig.editType === 'select' && ) {
// // e===false && (this.editIndex=null)
// } else {
// this.editIndex = null
// }
// }
}
// console.log('====keyupEnter', this.editIndex)
let {rowIndex, colIndex, columnConfig, row, columnValue} = this.getCommonData()
// 如果是弹框,直接弹框
if (columnConfig.editType === 'dialog') {
// 单元格内容
this.tabName = columnConfig.dialogData.tabName
let tabParams = { [columnConfig.prop]: columnValue }
// 解析传入参数
let tabParamsIn = {}
if (columnConfig.dialogData.tabParams && typeof columnConfig.dialogData.tabParams == 'function') {
tabParamsIn = columnConfig.dialogData.tabParams(row)
} else if (columnConfig.dialogData.tabParams) {
tabParamsIn = columnConfig.dialogData.tabParams
}
tabParams = {...tabParamsIn, ...tabParams}
this.tabParams = tabParams
this.hideColumns = columnConfig.dialogData.hideColumns
this.open = true
this.$nextTick(()=>{
this.$refs.tableDialogRef.focusDialog()
})
return
} else if (columnConfig.editType === 'select') {
let current = this.selectList.find(item=>item[columnConfig.selectData.value] == columnValue)
if (!current) current = {}
this.chooseData = current
let updateFields = (columnConfig.selectData ? columnConfig.selectData.updateFields : null)
if (!updateFields && columnConfig.selectData) {
let defaultUpdateFields = {}
if (columnConfig.valprop) {
defaultUpdateFields = { [columnConfig.valprop]: columnConfig.selectData.value, [columnConfig.prop]: columnConfig.selectData.label }
} else {
defaultUpdateFields = { [columnConfig.prop]: columnConfig.selectData.value }
}
updateFields = defaultUpdateFields
}
this.updateChooseData(rowIndex, updateFields, true)
}
this.validateNext(isNext)
},
// 验证与下一步
validateNext (isNext = true) {
let {rowIndex, colIndex, columnConfig, row, columnValue} = this.getCommonData()
if (columnConfig.editType !== 'dialog') {
// this.$emit('update:tableData', this.tableDataTmp.tableData)
}
// 弹框传递额外参数
let otherParams = (columnConfig.editType === 'dialog') ? this.updateChooseData(rowIndex, (columnConfig.dialogData ? columnConfig.dialogData.updateFields : null)) : null
// let otherParams = JSON.parse(JSON.stringify(this.tableDataTmp.tableData[rowIndex]))
// 验证单元格数据,验证不通过不继续执行
this.$refs.tableForm.validateField('tableData.'+rowIndex+'.'+columnConfig.prop, (result) => {
if (result) {
if (columnConfig.editType === 'dialog') this.$modal.msgError(result)
return
}
if (columnConfig.editType === 'dialog') {
this.updateChooseData(rowIndex, (columnConfig.dialogData ? columnConfig.dialogData.updateFields : null), true)
// this.$emit('update:tableData', this.tableDataTmp.tableData)
this.open = false
}
// 判断是否跳转到下一步
let next = isNext ? this.next : ()=>{}
// 判断是否有业务回调 判断是否跳转到下一步
if (columnConfig.callback) {
columnConfig.callback({index: rowIndex, row: row, isEnter: isNext}, next )
return
}
next()
}, otherParams)
},
// 弹框选择带回
oncheck (chooseData) {
if (!chooseData) return;
this.chooseData = chooseData
// let {rowIndex, columnConfig} = this.getCommonData()
// this.updateChooseData(rowIndex, (columnConfig.dialogData ? columnConfig.dialogData.updateFields : null), true)
// this.open = false
this.validateNext()
},
/**
* 更新弹框数据。isTemp传true-更新数据,传false-返回临时数据
*/
updateChooseData (rowIndex, updateFields, isTemp = false) {
let tmpData = JSON.parse(JSON.stringify(this.tableDataTmp.tableData[rowIndex]))
if (updateFields) {
if (updateFields instanceof Array) {
updateFields.forEach((key) => {
tmpData[key] = this.chooseData[key]
isTemp && this.$set(this.tableDataTmp.tableData[rowIndex], key, this.chooseData[key])
})
} else {
var objkeys = Object.keys(updateFields)
objkeys.forEach((key) => {
tmpData[key] = this.chooseData[updateFields[key]]
isTemp && this.$set(this.tableDataTmp.tableData[rowIndex], key, this.chooseData[updateFields[key]])
})
}
}
return tmpData
},
validate (callback) {
return new Promise((resolve, reject) => {
this.$refs.tableForm.validate(callback).then((result)=>{
resolve(result)
}).catch(e=>{
let errorArr = Object.values(e)
this.$modal.msgError(errorArr[0][0].message);
reject(e)
})
})
},
validateRow (index, callback) {
return this.$refs.tableForm.validateRow(index, callback)
},
resetTableform() {
this.resetForm('tableForm')
},
clickLink(prop, scope) {
this.$emit('link', prop, scope)
}
},
};
</script>
<style lang="scss" scoped>
// .edit-table-dialog ::v-deep .table-form-item.is-error:hover .el-form-item__error{
// display: block!important;
// }
.edit-table-dialog{
// 标题按钮行
.title-tag {
margin: 4px 0;
flex-grow: 1;
}
.edit-table {
margin-top: 10px;
::v-deep .el-table__body-wrapper {
tr:hover > td.el-table__cell{
background-color: #fff;
}
// overflow: unset; // 超出的字段滚动
.el-table__cell {
padding: 0!important;
&.el-table-column--selection .el-checkbox {
margin-left: 16px;
}
.cell {
// overflow: unset;
padding: 0!important;
.column-text {
width: 100%;
height: 48px;
line-height: 48px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
.edit-border {
cursor: pointer;
}
.date {
position: relative;
width: calc(100% - 12px);
height: 36px;
line-height: 36px;
border: 1px solid #F0F2F7;
border-radius: 4px;
text-align: left;
&::before {
content: "";
font-family: "element-icons";
color: #C0C4CC;
width: 30px;
position: relative;
top: -1px;
line-height: 34px;
text-align: center;
display: inline-block;
}
}
.select {
position: relative;
width: calc(100% - 12px);
height: 36px;
line-height: 36px;
border: 1px solid #F0F2F7;
border-radius: 4px;
text-align: left;
padding: 0 30px 0 10px;
&::after {
content: "";
color: #C0C4CC;
font-size: 14px;
font-family: "element-icons";
width: 25px;
text-align: center;
display: inline-block;
position: absolute;
right: 5px;
top: 0;
transform: rotate(180deg);
}
}
.input {
position: relative;
width: calc(100% - 12px);
height: 36px;
line-height: 36px;
border: 1px solid #F0F2F7;
border-radius: 4px;
text-align: left;
padding: 0 10px;
}
.dialog {
position: relative;
width: calc(100% - 12px);
height: 36px;
line-height: 36px;
border: 1px solid #F0F2F7;
border-radius: 4px;
text-align: left;
padding: 0 10px;
&::after {
content: "";
color: #C0C4CC;
font-size: 14px;
font-family: "element-icons";
width: 25px;
text-align: center;
display: inline-block;
position: absolute;
right: 4px;
top: 0;
}
}
}
.table-form-item {
margin: 0;
.diff-tag {
display: none;
}
/**.edit-tag {
position: absolute;
top: 0;
left: 0;
width: 8px!important;
height: 0;
border-left: 8px solid #1890ff;
border-bottom: 5px solid #fff;
z-index: 1;
}
&.is-error .edit-tag{
border-left-color: #ff4949;
} */
/** 错误 */
&.is-error .edit-border {
background: #FFF7F5;
border: 1px solid #F2B1A7;
}
&.diff{
color: #ff4949;
.diff-tag{
display: block;
position: absolute;
top: 30px;
left: 0;
}
}
.el-form-item__content{
& >div >div{
width: calc(100% - 14px);
&.has-unit {
width: 80%;
float: left;
}
// .el-date-editor,.el-input {
// width: calc(100% - 12px);
// }
/*&.el-select .el-input input {
height: 45px;
line-height: 45px;
border-radius: 0;
border-color: #fff;
padding: 0 30px 0 10px;
&:focus {
outline: none;
border-color: #1890ff;
}
}
&.el-input input {
height: 45px;
line-height: 45px;
border-radius: 0;
border-color: #fff;
padding: 0 30px 0 10px;
&:focus {
outline: none;
border-color: #1890ff;
}
}
&.el-date-editor input {
height: 45px;
line-height: 45px;
border-radius: 0;
padding: 0 30px;
}*/
}
.el-form-item__error{
display: none;
position: absolute;
top: unset;
bottom: 42px;
left: 0;
background-color: antiquewhite;
z-index: 999999;
width: 100%;
padding: 10px;
border-radius: 5px;
line-height: 20px;
&::after{
content: "";
height: 0;
width: 0;
border: 4px solid transparent;
border-top-color: antiquewhite;
position: absolute;
top: 100%;
right: 20px;
}
}
}
}
}
/** 复选框 */
&.el-table-column--selection .cell {
padding: 0 10px;
}
}
.el-table__empty-block {
background-image: none;
min-height: 60px!important;;
margin-bottom: 0;
span {
bottom: 0;
}
}
}
::v-deep .warning-row {
background: oldlace;
}
::v-deep .success-row {
background: #f0f9eb;
}
}
&.edit-table-disabled .edit-table {
::v-deep .el-table__body-wrapper {
.el-table__cell .column-text .edit-border{
cursor: not-allowed;
background-color: #f9f9f9!important;
border-color: #f0f2f7;
color: #BBBBBB;
}
}
}
}
.no-btns {
.edit-table {
margin-top: 0;
}
}
</style>
column.vue
import Vue from 'vue';
// import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom';
// import ElCheckbox from 'element-ui/packages/checkbox';
// import FilterPanel from './filter-panel.vue';
// import LayoutObserver from './layout-observer';
// import { mapStates } from './store/helper';
export default {
name: 'EditColumnRender',
props: {
column: {
type: Object
},
scope: {
type: Object
},
store: {
type: Object
// required: true
},
},
render(h) {
return (
<div>
{
this.column.renderTemp
? this.column.renderTemp.call(this._renderProxy, h, { column: this.column, scope: this.scope, store: this.store, _self: this.$parent.$vnode.context })
: ''
}
</div>
);
},
components: {
},
created() {
},
mounted() {
},
beforeDestroy() {
},
methods: {
},
data() {
return {
draggingColumn: null,
dragging: false,
dragState: {}
};
}
};
支持column-render.js ,支持render自定义列内容
import Vue from 'vue';
// import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom';
// import ElCheckbox from 'element-ui/packages/checkbox';
// import FilterPanel from './filter-panel.vue';
// import LayoutObserver from './layout-observer';
// import { mapStates } from './store/helper';
export default {
name: 'EditColumnRender',
props: {
column: {
type: Object
},
scope: {
type: Object
},
store: {
type: Object
// required: true
},
},
render(h) {
return (
<div>
{
this.column.renderTemp
? this.column.renderTemp.call(this._renderProxy, h, { column: this.column, scope: this.scope, store: this.store, _self: this.$parent.$vnode.context })
: ''
}
</div>
);
},
components: {
},
created() {
},
mounted() {
},
beforeDestroy() {
},
methods: {
},
data() {
return {
draggingColumn: null,
dragging: false,
dragState: {}
};
}
};
使用方法如下
我这里全局引用了,你也可以局部引用进来
<edit-table
ref="editTable"
:empty-row="emptyRow"
:column-data="columnData"
:table-data.sync="tableData"
:table-rules="tableRules"
row-key="id"
></edit-table>
data() {
return {
symbolList: [
{label: '相似', value: "%%%"},
{label: '左相似', value: "%%"},
{label: '右相似', value: "%"},
{label: '大于', value: ">"},
{label: '大于等于', value: ">="},
{label: '等于', value: "="},
{label: '小于', value: "<"},
{label: '小于等于', value: "<="}
],
relationList: [
{label: '并且', value: '&&'},
{label: '或者', value: '||'}
],
columnData: [
{ prop: 'name', type: 'selection' },
{
label: "列名",
prop: "queryColumn",
edit: true,
editType: "select",
selectData: {
label: "desc",
value: "value",
getData: () => {
return this.fields
}
},
format: (scope) => {
let current = this.fields.find(
(item) => item.value === scope.row.queryColumn
);
if (current) return current.desc;
}
},
{
label: "计算符",
prop: "querySymbol",
edit: true,
editType: "select",
selectData: {
label: "label",
value: "value",
getData: () => {
return this.symbolList
}
},
format: (scope) => {
let current = this.symbolList.find(
(item) => item.value === scope.row.querySymbol
);
if (current) return current.label;
}
},
{ label: "值", prop: "queryValue", edit: true, editType: 'template', renderTemp: (h, {scope})=>{
return h(ColumnEdit, {
props: {
column: 'queryValue',
scope: scope
}
}
)
}},
{
label: "列关系",
prop: "queryRelation",
edit: true,
editType: "select",
selectData: {
label: "label",
value: "value",
getData: () => {
return this.relationList
}
},
format: (scope) => {
let current = this.relationList.find(
(item) => item.value === scope.row.queryRelation
);
if (current) return current.label;
}
},
],
emptyRow: {
queryColumn: null,
querySymbol: null,
queryValue: null,
queryRelation: '&&'
},
tableData: [],
tableRules: {
queryColumn: [
{ required: true, message: "列名不能为空", trigger: "change" },
],
querySymbol: [
{ required: true, message: "计算符不能为空", trigger: "change" },
],
queryValue: [
{ required: true, message: "值不能为空", trigger: "change" },
],
queryRelation: [
{ required: true, message: "列关系不能为空", trigger: "change" },
],
},
};
版权归原作者 hurry1989 所有, 如有侵权,请联系我们删除。