vue2+element实现Table表格嵌套输入框、选择器、日期选择器、表单弹出窗组件的行内编辑功能
vue2+element实现Table表格嵌套输入框、选择器、日期选择器、表单弹出窗组件的行内编辑功能
文章目录
- vue2+element实现Table表格嵌套输入框、选择器、日期选择器、表单弹出窗组件的行内编辑功能
- 前言
- 一、准备工作
- 二、行内编辑
- 1.嵌入Input文本输入框
- 1.1遇到问题
- 1.文本框内容修改失去焦点后,editLoading也设置为false了,但是加载动画一直在转
- 2.嵌入Select选择器
- 3.嵌入DatePicker日期选择器
- 3.1遇到问题
- 1.表格的数据日期格式是时间戳格式,需要将时间戳转化为常见‘年-月-日’格式
- 4.嵌入表单弹出框组件
- 4.1遇到问题
- 1.手动激活模式下的弹窗显示和关闭的问题
- 2.表单的验证功能显示不正常
- 三、总结
前言
提示:本文的开发环境是vue2+element UI2.13.2
最近自己的项目需要使用到表格的行内编辑功能,主要是在表格中集成输入框、弹出窗(表单组件)、选择框、时间选择框等组件,实现表格行内的编辑。
一、准备工作
首先不管表格嵌入什么组件,我们都要对表格的单元格进行索引标记这一准备工作,这样的话我们就可以知道我们点击的是哪一行哪一列的单元格了,我这里利用的是element UI里table自带的属性:row-class-name和cell-class-name,
同时在vue的data中设置另个数据:rowIndex: -1, columnIndex: -1,用来表示当前点击索引坐标。
之后创建一个表格单元格点击事件handleCellClick,输出点击单元格的坐标,坐标索引正确,则说明准备工作完毕,可以加入编辑组件了。准备工作代码如下:
提示:本文的目的是分享行内编辑实现过程,文中存在大量代码示例,读者可自行选择合适的代码进行复制粘贴,然后微调实现自己想要的功能,文章最后附有标题中所有组件的完整代码。
,
<template><div class="app-container"><el-table :data="tableData" border style="width: 800px;" :row-class-name="tableRowIndex" :cell-class-name="tableCellIndex" @cell-click="handleCellClick"><el-table-column fixed prop="date" label="日期" width="150" /><el-table-column prop="name" label="姓名" width="120" /><el-table-column prop="province" label="省份" width="120" /><el-table-column prop="city" label="市区" width="120" /><el-table-column prop="address" label="地址" width="300" /><el-table-column prop="zip" label="邮编" width="120" /><el-table-column fixed="right" label="操作" width="100"><template slot-scope="scope"><el-button type="text" size="small" @click="handleClick(scope.row)">查看</el-button><el-button type="text" size="small">编辑</el-button></template></el-table-column></el-table></div>
</template><script>
export default {name: 'Testdemo',data() {return {tableData: [{ id: 1, date: '2016-05-02', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: '2016-05-04', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: '2016-05-01', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: '2016-05-03', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],// 表格行内编辑相关属性rowIndex: -1, // 行索引columnIndex: -1 // 列索引}},// 监听属性computed: {},// 生命周期mounted() {},created() {},// 方法methods: {handleClick(row) {console.log(row)},// 行内编辑相关基础方法// 把每一行的索引加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格tableRowIndex({ row, rowIndex }) {// console.log(rowIndex)// 初始化行数据,将索引添加到行数据中row.index = rowIndex},// 把每一列的索引加到列数据中tableCellIndex({ column, columnIndex }) {// 初始化行数据,将索引添加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格column.index = columnIndex},/** 表格点击事件 */handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.indexconsole.log('rowIndex', this.rowIndex)console.log('columnIndex', this.columnIndex)}}
}
</script>
<style lang="scss" scoped></style>
二、行内编辑
1.嵌入Input文本输入框
点击‘编辑’图标,则可激活文本输入框对名字进行编辑修改,实现效果如图:
在准备工作的代码中继续完善代码。在data属性中添加iputIndex: -1 ,表示输入框索引。
data() {return {...iputIndex: -1 // 输入框索引}},
编写显示文本框的方法inputShow()
// 显示输入框inputShow(scope) {this.iputIndex = scope.row.indexthis.rowIndex = scope.row.indexthis.columnIndex = scope.column.index},
当输入框显示之后,我们可以对其文本进行编辑和修改,也就是说文本框是获取焦点状态,当它失去焦点的时候,我们默认文本框不显示并且提交修改过的文本到后台。
定义编辑文本方法editName()
// 输入框编辑名字editName(row) {this.iputIndex = -1this.rowIndex = -1this.columnIndex = -1row.editLoading = truesetTimeout(function() {// 使用替换整个对象而不是修改其属性也是一个解决方案。这可以确保 Vue 检测到变化并更新视图row.editLoading = falsethis.$set(this.tableData, row.index, row)console.log('失去焦点', this.tableData)}.bind(this), 1000)}
在DOM中根据 scope.row.index == iputIndex &&scope.column.index == columnIndex && scope.row.index == rowIndex,当点击单元格的索引坐标和数据的索引坐标都一样的时候,显示对应的文本输入框。
...<el-table-column prop="name" label="姓名" width="120"><template slot-scope="scope"><template v-if="scope.row.index == iputIndex && scope.column.index == columnIndex && scope.row.index == rowIndex"><el-input:ref="'sortNumRef' + scope.row.id"v-model="scope.row.name"size="small"autofocus="true"@blur="editName(scope.row)"/></template><template v-if="scope.row.editLoading"><i class="el-icon-loading" /></template><template v-if="scope.row.index != iputIndex && !scope.row.editLoading"><span>{{ scope.row.name }}</span><el-button type="text" icon="el-icon-edit" style="margin-left: 10px" @click="inputShow(scope)" /></template></template></el-table-column>...
同时我们也需要在表格外的div绑定一个方法,当鼠标点击表格外区域时不显示文本输入框,定义handleClickOutside方法。
/** 监听鼠标点击表格外面区域的时候,行内编辑失去焦点*/handleClickOutside(event) {var isTargetOrChild = event.target.classNameif (isTargetOrChild !== '' &&isTargetOrChild !== 'el-icon-edit' &&isTargetOrChild !== 'el-input__inner') {this.rowIndex = -1this.columnIndex = -1this.iputIndex = -1 // 此处解决点击方框极近处}},
到此,我们就实现了Table嵌入Input文本输入框,完整代码如下:
<template><div class="app-container" @click="handleClickOutside"><el-table :data="tableData" border style="width: 800px;" :row-class-name="tableRowIndex" :cell-class-name="tableCellIndex" @cell-click="handleCellClick"><el-table-column fixed prop="date" label="日期" width="150" /><el-table-column prop="name" label="姓名" width="120"><template slot-scope="scope"><template v-if="scope.row.index == iputIndex && scope.column.index == columnIndex && scope.row.index == rowIndex"><el-input:ref="'sortNumRef' + scope.row.id"v-model="scope.row.name"size="small"autofocus="true"@blur="editName(scope.row)"/></template><template v-if="scope.row.editLoading"><i class="el-icon-loading" /></template><template v-if="scope.row.index != iputIndex && !scope.row.editLoading"><span>{{ scope.row.name }}</span><el-button type="text" icon="el-icon-edit" style="margin-left: 10px" @click="inputShow(scope)" /></template></template></el-table-column><el-table-column prop="province" label="省份" width="120" /><el-table-column prop="city" label="市区" width="120" /><el-table-column prop="address" label="地址" width="300" /><el-table-column prop="zip" label="邮编" width="120" /><el-table-column fixed="right" label="操作" width="100"><template slot-scope="scope"><el-button type="text" size="small" @click="handleClick(scope.row)">查看</el-button><el-button type="text" size="small">编辑</el-button></template></el-table-column></el-table></div>
</template><script>
export default {name: 'Testdemo',data() {return {tableData: [{ id: 1, date: '2016-05-02', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: '2016-05-04', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: '2016-05-01', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: '2016-05-03', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],// 表格行内编辑相关属性rowIndex: -1, // 行索引columnIndex: -1, // 列索引iputIndex: -1 // 输入框索引}},// 监听属性computed: {},// 生命周期mounted() {},created() {},// 方法methods: {handleClick(row) {console.log(row)},// 行内编辑相关基础方法// 把每一行的索引加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格tableRowIndex({ row, rowIndex }) {// console.log(rowIndex)// 初始化行数据,将索引添加到行数据中row.index = rowIndex},// 把每一列的索引加到列数据中tableCellIndex({ column, columnIndex }) {// 初始化行数据,将索引添加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格column.index = columnIndex},/** 表格点击事件 */handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.index},/** 监听鼠标点击表格外面区域的时候,行内编辑失去焦点*/handleClickOutside(event) {var isTargetOrChild = event.target.classNameif (isTargetOrChild !== '' &&isTargetOrChild !== 'el-icon-edit' &&isTargetOrChild !== 'el-input__inner') {this.rowIndex = -1this.columnIndex = -1this.iputIndex = -1 // 此处解决点击方框极近处}},// 显示输入框inputShow(scope) {this.iputIndex = scope.row.indexthis.rowIndex = scope.row.indexthis.columnIndex = scope.column.index},// 输入框编辑名字editName(row) {this.iputIndex = -1this.rowIndex = -1this.columnIndex = -1row.editLoading = truesetTimeout(function() {// 使用替换整个对象而不是修改其属性也是一个解决方案。这可以确保 Vue 检测到变化并更新视图/* const updatedRow = { ...row, editLoading: false }this.tableData.splice(this.tableData.indexOf(row), 1, updatedRow) */row.editLoading = falsethis.$set(this.tableData, row.index, row)// console.log('失去焦点', this.tableData)}.bind(this), 1000)}}
}
</script><style lang="scss" scoped></style>
1.1遇到问题
1.文本框内容修改失去焦点后,editLoading也设置为false了,但是加载动画一直在转
在 Vue 中,当你直接修改对象数组中的某个对象的属性时,Vue 可能无法检测到这个变化,因此不会触发视图>更新。这是因为 Vue 使用的是“响应式系统”来追踪数据的变化,但它只能检测到对象属性的添加或删除,或者>>数组元素的添加、删除或顺序改变。对于数组或对象内部属性的直接修改(如 array[index] = newValue 或 >object.property = newValue),Vue 可能无法检测到变化。解决办法:vue 提供了一个 Vue.set 方法(在 Vue 3 中是 this.$set)
this.$set(this.tableData, row.index, row)
2.嵌入Select选择器
在嵌入文本输入框的基础上再加入select下拉选择框,效果如下:
在data属性中添加两个数据,一个是select下拉框的选项数组optionsList,一个是用来保存原始数据的变量originalData,当修改的数据和原始数据不一致的情况下才进行提交数据和刷新表格。
data() {return {.../* --------------------------下拉框相关属性-------------------------- */optionsList: [{ id: 1, name: '上海' },{ id: 2, name: '重庆' },{ id: 3, name: '北京' }],// 原始数据,用来和修改的数据进行对比,有变化才能提交数据,否则不提交。originalData: null}...},
还需要在表格点击事件方法handleCellClick中,对originalData变量赋值。
handleCellClick(row, column, cell, event) {...var ifproperty = Object.prototype.hasOwnProperty.call(column, 'property')if (ifproperty) {const property = column.propertyif (property === 'province') { //判断是否是省份那一列this.$nextTick(() => {this.$refs['sortNum' + row.id].focus()this.originalData = Array.from(row.province)// console.log('原始数据', this.originalData)})}}},
我们还需要用到select选择框的blur、visible-change、remove-tag和change事件。
在DOM中根据 scope.row.index == iputIndex &&scope.column.index == columnIndex,当点击单元格的索引坐标和数据的索引坐标都一样的时候,显示对应的下拉选择框。完整代码如下:
<template><div class="app-container" @click="handleClickOutside"><el-table :data="tableData" border style="width: 800px;" :row-class-name="tableRowIndex" :cell-class-name="tableCellIndex" @cell-click="handleCellClick"><el-table-column fixed prop="date" label="日期" width="150" /><el-table-column prop="name" label="姓名" width="120"><template slot-scope="scope"><template v-if="scope.row.index == iputIndex && scope.column.index == columnIndex && scope.row.index == rowIndex"><el-input:ref="'sortNumRef' + scope.row.id"v-model="scope.row.name"size="small"autofocus="true"@blur="inputEditName(scope.row)"/></template><template v-if="scope.row.editLoading"><i class="el-icon-loading" /></template><template v-if="scope.row.index != iputIndex && !scope.row.editLoading"><span>{{ scope.row.name }}</span><el-button type="text" icon="el-icon-edit" style="margin-left: 10px" @click="inputShow(scope)" /></template></template></el-table-column><el-table-column prop="province" label="省份" width="200"><template slot-scope="scope"><el-selectv-if="rowIndex === scope.row.index && columnIndex === scope.column.index":ref="'sortNum' + scope.row.id"v-model="scope.row.province"style="width: 100%"size="small"filterablemultipleplaceholder="请选择"@blur="selectBlur"@visible-change="selectVisibleChange"@remove-tag="selectTagClose(scope.row.province,scope.row)"@change="selectChange(scope.row.province,scope.row)"><el-option v-for="item in optionsList" :key="item.id" :label="item.name" :value="item.id" /></el-select><span v-else style="cursor: pointer;">{{ Array.isArray(scope.row.province_name)? scope.row.province_name.join(","): "" }}</span></template></el-table-column><el-table-column prop="city" label="市区" width="120" /><el-table-column prop="address" label="地址" width="300" /><el-table-column prop="zip" label="邮编" width="120" /><el-table-column fixed="right" label="操作" width="100"><template slot-scope="scope"><el-button type="text" size="small" @click="handleClick(scope.row)">查看</el-button><el-button type="text" size="small">编辑</el-button></template></el-table-column></el-table></div>
</template><script>
export default {name: 'Testdemo',data() {return {tableData: [{ id: 1, date: '2016-05-02', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: '2016-05-04', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: '2016-05-01', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: '2016-05-03', name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],// 表格行内编辑相关属性rowIndex: -1, // 行索引columnIndex: -1, // 列索引/* --------------------------输入框相关属性-------------------------- */iputIndex: -1, // 输入框索引/* --------------------------下拉框相关属性-------------------------- */optionsList: [{ id: 1, name: '上海' },{ id: 2, name: '重庆' },{ id: 3, name: '北京' }],// 原始数据,用来和修改的数据进行对比,有变化才能提交数据,否则不提交。originalData: null}},// 监听属性computed: {},// 生命周期mounted() {},created() {},// 方法methods: {handleClick(row) {console.log(row)},/* --------------------------行内编辑相关基础方法-------------------------- */// 把每一行的索引加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格tableRowIndex({ row, rowIndex }) {// console.log(rowIndex)// 初始化行数据,将索引添加到行数据中row.index = rowIndex},// 把每一列的索引加到列数据中tableCellIndex({ column, columnIndex }) {// 初始化行数据,将索引添加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格column.index = columnIndex},// 表格点击事件handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.indexvar ifproperty = Object.prototype.hasOwnProperty.call(column, 'property')if (ifproperty) {const property = column.propertyif (property === 'province') {this.$nextTick(() => {this.$refs['sortNum' + row.id].focus()this.originalData = Array.from(row.province)// console.log('原始数据', this.originalData)})}}},// 监听鼠标点击表格外面区域的时候,行内编辑失去焦点handleClickOutside(event) {var isTargetOrChild = event.target.classNameif (isTargetOrChild !== '' &&isTargetOrChild !== 'el-icon-edit' &&isTargetOrChild !== 'el-input__inner') {this.rowIndex = -1this.columnIndex = -1this.iputIndex = -1 // 此处解决点击方框极近处}},/** --------------------------输入框相关事件-------------------------- */// 显示输入框inputShow(scope) {this.iputIndex = scope.row.indexthis.rowIndex = scope.row.indexthis.columnIndex = scope.column.index},// 输入框编辑名字inputEditName(row) {this.iputIndex = -1this.rowIndex = -1this.columnIndex = -1row.editLoading = truesetTimeout(function() {// 使用替换整个对象而不是修改其属性也是一个解决方案。这可以确保 Vue 检测到变化并更新视图/* const updatedRow = { ...row, editLoading: false }this.tableData.splice(this.tableData.indexOf(row), 1, updatedRow) */row.editLoading = falsethis.$set(this.tableData, row.index, row)// console.log('失去焦点', this.tableData)}.bind(this), 1000)},/** --------------------------选择框相关事件-------------------------- */// 失去焦点初始化selectBlur() {this.rowIndex = -1this.columnIndex = -1},// 选择框打开关闭下拉框事件selectVisibleChange(val) {if (!val) {this.selectBlur()}},// 对比选择框两个数组是否相等arraysEqual(arr1, arr2) {if (arr1.length !== arr2.length) return falsereturn arr1.every((value, index) => value === arr2[index])},// 选择框标签关闭事件selectTagClose(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}},// 选择框当前选中值变化事件selectChange(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}}}
}
</script>
<style lang="scss" scoped></style>
3.嵌入DatePicker日期选择器
在之前代码的基础上继续嵌入DatePicker日期选择器。效果如下:
因为日期选择器宽度不够的情况下显示的时间会被遮挡,如图所示
所以我们需要改变列宽,在data属性中添加一个宽度数据dateTimeWidth
data() {return {.../* --------------------------日期选择器相关属性-------------------------- */dateTimeWidth: 150}},
在表格点击单元格事件中修改列宽dateTimeWidth 和保存未修改的原始数据originalData
// 表格点击事件handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.indexvar ifproperty = Object.prototype.hasOwnProperty.call(column, 'property')if (ifproperty) {const property = column.property...if (property === 'date') {this.dateTimeWidth = 200this.$nextTick(() => {this.$refs['dateTime' + row.id].focus()this.originalData = row.date// console.log('原始数据', this.originalData)})}}},
我们还需要用到DatePicker日期选择器的blur和change事件。
在DOM中根据 scope.row.index == iputIndex &&scope.column.index == columnIndex,当点击单元格的索引坐标和数据的索引坐标都一样的时候,显示对应的日期选择器。完整代码如下:
<template><div class="app-container" @click="handleClickOutside"><el-table :data="tableData" border style="width: 800px;" :row-class-name="tableRowIndex" :cell-class-name="tableCellIndex" @cell-click="handleCellClick"><el-table-column fixed prop="date" label="日期" :width="dateTimeWidth"><template slot-scope="scope"><el-date-pickerv-if="scope.row.index === rowIndex && scope.column.index === columnIndex":ref="'dateTime' + scope.row.id"v-model="scope.row.date"type="date"style="width: 100%"format="yyyy-MM-dd"value-format="timestamp"@blur="componentsBlur"@change="dateChange(scope.row)"/><span v-else style="cursor: pointer;">{{ scope.row.date | parseDate("{y}-{m}-{d}") }}</span></template></el-table-column><el-table-column prop="name" label="姓名" width="120"><template slot-scope="scope"><template v-if="scope.row.index == iputIndex && scope.column.index == columnIndex && scope.row.index == rowIndex"><el-input:ref="'sortNumRef' + scope.row.id"v-model="scope.row.name"size="small"autofocus="true"@blur="inputEditName(scope.row)"/></template><template v-if="scope.row.editLoading"><i class="el-icon-loading" /></template><template v-if="scope.row.index != iputIndex && !scope.row.editLoading"><span>{{ scope.row.name }}</span><el-button type="text" icon="el-icon-edit" style="margin-left: 10px" @click="inputShow(scope)" /></template></template></el-table-column><el-table-column prop="province" label="省份" width="200"><template slot-scope="scope"><el-selectv-if="rowIndex === scope.row.index && columnIndex === scope.column.index":ref="'sortNum' + scope.row.id"v-model="scope.row.province"style="width: 100%"size="small"filterablemultipleplaceholder="请选择"@blur="componentsBlur"@visible-change="selectVisibleChange"@remove-tag="selectTagClose(scope.row.province,scope.row)"@change="selectChange(scope.row.province,scope.row)"><el-option v-for="item in optionsList" :key="item.id" :label="item.name" :value="item.id" /></el-select><span v-else style="cursor: pointer;">{{ Array.isArray(scope.row.province_name)? scope.row.province_name.join(","): "" }}</span></template></el-table-column><el-table-column prop="city" label="市区" width="120" /><el-table-column prop="address" label="地址" width="300" /><el-table-column prop="zip" label="邮编" width="120" /><el-table-column fixed="right" label="操作" width="100"><template slot-scope="scope"><el-button type="text" size="small" @click="handleClick(scope.row)">查看</el-button><el-button type="text" size="small">编辑</el-button></template></el-table-column></el-table></div>
</template><script>
export default {name: 'Testdemo',filters: {parseDate(time, cFormat) {if (arguments.length === 0) {return null}const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'let dateif (typeof time === 'object') {date = time} else {if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {time = parseInt(time)}if ((typeof time === 'number') && (time.toString().length === 10)) {time = time * 1000}date = new Date(time)}const formatObj = {y: date.getFullYear(),m: date.getMonth() + 1,d: date.getDate(),h: date.getHours(),i: date.getMinutes(),s: date.getSeconds(),a: date.getDay()}const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {const value = formatObj[key]// Note: getDay() returns 0 on Sundayif (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }return value.toString().padStart(2, '0')})return time_str}},data() {return {tableData: [{ id: 1, date: 1462118400000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 1, address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: 1462291200000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 2, address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: 1462032000000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 3, address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: 1462204800000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 4, address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],// 原始数据,用来和修改的数据进行对比,有变化才能提交数据,否则不提交。originalData: null,// 表格行内编辑相关属性rowIndex: -1, // 行索引columnIndex: -1, // 列索引/* --------------------------输入框相关属性-------------------------- */iputIndex: -1, // 输入框索引/* --------------------------下拉框相关属性-------------------------- */optionsList: [{ id: 1, name: '上海' },{ id: 2, name: '重庆' },{ id: 3, name: '北京' }],/* --------------------------日期选择器相关属性-------------------------- */dateTimeWidth: 150}},// 监听属性computed: {},// 生命周期mounted() {},created() {},// 方法methods: {handleClick(row) {console.log(row)},/* --------------------------行内编辑相关基础方法-------------------------- */// 把每一行的索引加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格tableRowIndex({ row, rowIndex }) {// console.log(rowIndex)// 初始化行数据,将索引添加到行数据中row.index = rowIndex},// 把每一列的索引加到列数据中tableCellIndex({ column, columnIndex }) {// 初始化行数据,将索引添加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格column.index = columnIndex},// 表格点击事件handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.indexvar ifproperty = Object.prototype.hasOwnProperty.call(column, 'property')if (ifproperty) {const property = column.propertyif (property === 'province') {this.$nextTick(() => {this.$refs['sortNum' + row.id].focus()this.originalData = Array.from(row.province)// console.log('原始数据', this.originalData)})}if (property === 'date') {this.dateTimeWidth = 200this.$nextTick(() => {this.$refs['dateTime' + row.id].focus()this.originalData = row.date// console.log('原始数据', this.originalData)})}}},// 监听鼠标点击表格外面区域的时候,行内编辑失去焦点handleClickOutside(event) {var isTargetOrChild = event.target.classNameif (isTargetOrChild !== '' &&isTargetOrChild !== 'el-icon-edit' &&isTargetOrChild !== 'el-input__inner') {this.rowIndex = -1this.columnIndex = -1this.iputIndex = -1 // 此处解决点击方框极近处}},// 组件失去焦点初始化componentsBlur() {this.rowIndex = -1this.columnIndex = -1},/** --------------------------输入框相关事件-------------------------- */// 显示输入框inputShow(scope) {this.iputIndex = scope.row.indexthis.rowIndex = scope.row.indexthis.columnIndex = scope.column.index},// 输入框编辑名字inputEditName(row) {this.iputIndex = -1this.rowIndex = -1this.columnIndex = -1row.editLoading = truesetTimeout(function() {// 使用替换整个对象而不是修改其属性也是一个解决方案。这可以确保 Vue 检测到变化并更新视图/* const updatedRow = { ...row, editLoading: false }this.tableData.splice(this.tableData.indexOf(row), 1, updatedRow) */row.editLoading = falsethis.$set(this.tableData, row.index, row)// console.log('失去焦点', this.tableData)}.bind(this), 1000)},/** --------------------------选择框相关事件-------------------------- */// 选择框打开关闭下拉框事件selectVisibleChange(val) {if (!val) {this.componentsBlur()}},// 对比选择框两个数组是否相等arraysEqual(arr1, arr2) {if (arr1.length !== arr2.length) return falsereturn arr1.every((value, index) => value === arr2[index])},// 选择框标签关闭事件selectTagClose(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}},// 选择框当前选中值变化事件selectChange(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}},/** --------------------------时间选择器相关事件-------------------------- */// 计划结束时间发生变化dateChange(row) {this.dateTimeWidth = 150if (row.date !== this.originalData) {// 表格重新刷新this.$set(this.tableData, row.index, row)} else {this.componentsBlur()this.$message.error('时间不可清空')}}}
}
</script>
<style lang="scss" scoped></style>
3.1遇到问题
1.表格的数据日期格式是时间戳格式,需要将时间戳转化为常见‘年-月-日’格式
这个问题可以用vue的过滤器filters来解决,定义一个时间戳格式化过滤器
4.嵌入表单弹出框组件
继续在之前的代码上嵌入表单弹出框组件,我们首先需要对表格调整下数据,新增一列‘处理状态status’数据。效果如图:
同时,需要在data属性中,添加表单弹窗组件的属性,状态数据列表statusList、状态颜色数据statusColor、表单数据form和表单验证规则数据formRules
data() {return {tableData: [{ id: 1, date: 1462118400000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 1, address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: 1462291200000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 2, address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: 1462032000000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 3, address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: 1462204800000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 4, address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],.../* --------------------------表单弹出框相关属性-------------------------- */statusList: [{ id: 1, name: '新增加' },{ id: 2, name: '处理中' },{ id: 3, name: '已完成' },{ id: 4, name: '延期办理' },{ id: 5, name: '已取消' }],statusColor: ['#52c41a', '#2f54eb', '#b8bac1', '#fa541c', '#b8bac1'],form: {status: null, // 状态handler: null, // 处理人comment: null // 备注内容},formRules: {handler: [{ required: true, message: '请选择处理人', trigger: 'blur' }]}}},
我们要用到Popover弹出框的show和hide事件。
Form 表单组件方面我们需要对其绑定表单数据form、表单验证数据formRules。完整代码如下:
<template><div class="app-container" @click="handleClickOutside"><el-table :data="tableData" border style="width: 800px;" :row-class-name="tableRowIndex" :cell-class-name="tableCellIndex" @cell-click="handleCellClick"><el-table-column fixed prop="date" label="日期" :width="dateTimeWidth"><template slot-scope="scope"><el-date-pickerv-if="scope.row.index === rowIndex && scope.column.index === columnIndex":ref="'dateTime' + scope.row.id"v-model="scope.row.date"type="date"style="width: 100%"format="yyyy-MM-dd"value-format="timestamp"@blur="componentsBlur"@change="dateChange(scope.row)"/><span v-else style="cursor: pointer;">{{ scope.row.date | parseDate("{y}-{m}-{d}") }}</span></template></el-table-column><el-table-column prop="name" label="姓名" width="120"><template slot-scope="scope"><template v-if="scope.row.index == iputIndex && scope.column.index == columnIndex && scope.row.index == rowIndex"><el-input:ref="'sortNumRef' + scope.row.id"v-model="scope.row.name"size="small"autofocus="true"@blur="inputEditName(scope.row)"/></template><template v-if="scope.row.editLoading"><i class="el-icon-loading" /></template><template v-if="scope.row.index != iputIndex && !scope.row.editLoading"><span>{{ scope.row.name }}</span><el-button type="text" icon="el-icon-edit" style="margin-left: 10px" @click="inputShow(scope)" /></template></template></el-table-column><el-table-column prop="province" label="省份" width="200"><template slot-scope="scope"><el-selectv-if="rowIndex === scope.row.index && columnIndex === scope.column.index":ref="'sortNum' + scope.row.id"v-model="scope.row.province"style="width: 100%"size="small"filterablemultipleplaceholder="请选择"@blur="componentsBlur"@visible-change="selectVisibleChange"@remove-tag="selectTagClose(scope.row.province,scope.row)"@change="selectChange(scope.row.province,scope.row)"><el-option v-for="item in optionsList" :key="item.id" :label="item.name" :value="item.id" /></el-select><span v-else style="cursor: pointer;">{{ Array.isArray(scope.row.province_name)? scope.row.province_name.join(","): "" }}</span></template></el-table-column><el-table-column prop="status" label="处理状态" align="center" width="110"><template slot-scope="{ row, $index }"><el-popover:ref="'Popover' + row.id"width="700"trigger="click"@show="showPopover(row)"@hide="hidePopover"><el-row style="padding: 20px"><el-col :span="6"><el-radio-group :ref="'PopoverRadio' + row.id" v-model="form.status" size="small"><el-radiov-for="(item, index) in statusList":key="index"style="margin-bottom: 20px":label="item.id">{{ item.name }}</el-radio></el-radio-group></el-col><el-col :span="18" style="border-left: 1px solid #eee; padding-left: 20px"><el-form :ref="'formData' + row.id" label-width="80px" :model="form" :rules="formRules"><el-form-item label="处理人" prop="handler"><el-input v-model="form.handler" placeholder="请输入姓名" /></el-form-item><el-form-item label="评论" prop="comment"><el-input v-model="form.comment" type="textarea" /></el-form-item><el-form-item><el-button type="primary" @click="handleSubmit(row, $index)">提交</el-button><el-button @click="handleCancel(row, $index)">取消</el-button></el-form-item></el-form></el-col></el-row><el-buttonslot="reference"roundsize="mini":style="'color:' + statusColor[row.status - 1] + ';font-weight:bold;'">{{ statusList[row.status - 1] ? statusList[row.status - 1].name : "" }}</el-button></el-popover></template></el-table-column><el-table-column prop="city" label="市区" width="120" /><el-table-column prop="address" label="地址" width="300" /><el-table-column prop="zip" label="邮编" width="120" /><el-table-column fixed="right" label="操作" width="100"><template slot-scope="scope"><el-button type="text" size="small" @click="handleClick(scope.row)">查看</el-button><el-button type="text" size="small">编辑</el-button></template></el-table-column></el-table></div>
</template><script>
export default {name: 'Testdemo',filters: {parseDate(time, cFormat) {if (arguments.length === 0) {return null}const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'let dateif (typeof time === 'object') {date = time} else {if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {time = parseInt(time)}if ((typeof time === 'number') && (time.toString().length === 10)) {time = time * 1000}date = new Date(time)}const formatObj = {y: date.getFullYear(),m: date.getMonth() + 1,d: date.getDate(),h: date.getHours(),i: date.getMinutes(),s: date.getSeconds(),a: date.getDay()}const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {const value = formatObj[key]// Note: getDay() returns 0 on Sundayif (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }return value.toString().padStart(2, '0')})return time_str}},data() {return {tableData: [{ id: 1, date: 1462118400000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 1, address: '上海市普陀区金沙江路 1518 弄', zip: 200333 },{ id: 2, date: 1462291200000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 2, address: '上海市普陀区金沙江路 1517 弄', zip: 200333 },{ id: 3, date: 1462032000000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 3, address: '上海市普陀区金沙江路 1519 弄', zip: 200333 },{ id: 4, date: 1462204800000, name: '王小虎', province: [1], province_name: ['上海'], city: '普陀区', status: 4, address: '上海市普陀区金沙江路 1516 弄', zip: 200333 }],// 原始数据,用来和修改的数据进行对比,有变化才能提交数据,否则不提交。originalData: null,// 表格行内编辑相关属性rowIndex: -1, // 行索引columnIndex: -1, // 列索引/* --------------------------输入框相关属性-------------------------- */iputIndex: -1, // 输入框索引/* --------------------------下拉框相关属性-------------------------- */optionsList: [{ id: 1, name: '上海' },{ id: 2, name: '重庆' },{ id: 3, name: '北京' }],/* --------------------------日期选择器相关属性-------------------------- */dateTimeWidth: 150,/* --------------------------表单弹出框相关属性-------------------------- */statusList: [{ id: 1, name: '新增加' },{ id: 2, name: '处理中' },{ id: 3, name: '已完成' },{ id: 4, name: '延期办理' },{ id: 5, name: '已取消' }],statusColor: ['#52c41a', '#2f54eb', '#b8bac1', '#fa541c', '#b8bac1'],form: {status: null, // 状态handler: null, // 处理人comment: null // 备注内容},formRules: {handler: [{ required: true, message: '请选择处理人', trigger: 'blur' }]}}},// 监听属性computed: {},// 生命周期mounted() {},created() {},// 方法methods: {handleClick(row) {console.log(row)},/* --------------------------行内编辑相关基础方法-------------------------- */// 把每一行的索引加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格tableRowIndex({ row, rowIndex }) {// console.log(rowIndex)// 初始化行数据,将索引添加到行数据中row.index = rowIndex},// 把每一列的索引加到列数据中tableCellIndex({ column, columnIndex }) {// 初始化行数据,将索引添加到行数据中,以便以后明确点击事件是哪一行哪一列的单元格column.index = columnIndex},// 表格点击事件handleCellClick(row, column, cell, event) {this.rowIndex = row.indexthis.columnIndex = column.indexvar ifproperty = Object.prototype.hasOwnProperty.call(column, 'property')if (ifproperty) {const property = column.propertyif (property === 'province') {this.$nextTick(() => {this.$refs['sortNum' + row.id].focus()this.originalData = Array.from(row.province)// console.log('原始数据', this.originalData)})}if (property === 'date') {this.dateTimeWidth = 200this.$nextTick(() => {this.$refs['dateTime' + row.id].focus()this.originalData = row.date// console.log('原始数据', this.originalData)})}}},// 监听鼠标点击表格外面区域的时候,行内编辑失去焦点handleClickOutside(event) {var isTargetOrChild = event.target.classNameif (isTargetOrChild !== '' &&isTargetOrChild !== 'el-icon-edit' &&isTargetOrChild !== 'el-input__inner') {this.rowIndex = -1this.columnIndex = -1this.iputIndex = -1 // 此处解决点击方框极近处}},// 组件失去焦点初始化componentsBlur() {this.rowIndex = -1this.columnIndex = -1},/** --------------------------输入框相关事件-------------------------- */// 显示输入框inputShow(scope) {this.iputIndex = scope.row.indexthis.rowIndex = scope.row.indexthis.columnIndex = scope.column.index},// 输入框编辑名字inputEditName(row) {this.iputIndex = -1this.rowIndex = -1this.columnIndex = -1row.editLoading = truesetTimeout(function() {// 使用替换整个对象而不是修改其属性也是一个解决方案。这可以确保 Vue 检测到变化并更新视图/* const updatedRow = { ...row, editLoading: false }this.tableData.splice(this.tableData.indexOf(row), 1, updatedRow) */row.editLoading = falsethis.$set(this.tableData, row.index, row)// console.log('失去焦点', this.tableData)}.bind(this), 1000)},/** --------------------------选择框相关事件-------------------------- */// 选择框打开关闭下拉框事件selectVisibleChange(val) {if (!val) {this.componentsBlur()}},// 对比选择框两个数组是否相等arraysEqual(arr1, arr2) {if (arr1.length !== arr2.length) return falsereturn arr1.every((value, index) => value === arr2[index])},// 选择框标签关闭事件selectTagClose(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}},// 选择框当前选中值变化事件selectChange(val, row) {const isEqual = this.arraysEqual(this.originalData, val)if (!isEqual) {row.province_name = val.map(id => {const option = this.optionsList.find(option => option.id === id)return option ? option.name : null // 如果找不到对应的id,则返回null})// 表格重新刷新this.$set(this.tableData, row.index, row)}},/** --------------------------时间选择器相关事件-------------------------- */// 计划结束时间发生变化dateChange(row) {this.dateTimeWidth = 150if (row.date !== this.originalData) {// 表格重新刷新this.$set(this.tableData, row.index, row)} else {this.componentsBlur()this.$message.error('时间不可清空')}},/** --------------------------表单弹出框相关事件-------------------------- */// 初始化弹层窗showPopover(row) {this.form.status = row.status // 状态this.form.originalData = row.status},// 隐藏状态弹层窗hidePopover() {this.form = {status: null, // 状态handler: null, // 处理人comment: null // 备注内容}},// 提交操作handleSubmit(row) {var ref = 'formData' + row.idthis.$refs[ref].validate((valid) => {if (valid) {if (this.form.status !== this.originalData) {// 发起后端请求row.status = this.form.statusthis.$set(this.tableData, row.index, row) // 表格重新刷新this.resetForm(row, row.index) // 重置表单} else {this.resetForm(row, row.index)}// console.log('提交操作', this.form)}})},// 重置表单resetForm(row, $index) {var ref = 'formData' + row.idthis.$refs[ref].resetFields()this.$refs[ref].clearValidate()this.form = {status: null, // 状态handler: null, // 处理人comment: null // 备注内容}document.body.click()},// 取消操作handleCancel(row) {this.resetForm(row, row.index)// console.log('取消操作', this.form)this.$message({message: '取消操作',type: 'warning'})}}
}
</script>
<style lang="scss" scoped></style>
4.1遇到问题
1.手动激活模式下的弹窗显示和关闭的问题
如果需要使用手动模式的弹出框,则不能使用v-model属性,主要原因是,因为表格渲染出多个v-model绑定同一个值的弹出框,所以如果绑定同一个v-model值的话,当v-model为true时,则会同时显示多个弹出框;
第二如果绑定v-model值的话,则无法使用doShow方法,虽然能输出的DOW对象里面包含doShow方法,但是调用的时候却是undefined。
去掉v-model绑定值,则正常显示弹出框。然后通过doClose方法来关闭。手动模式最大的缺点是很难做到点击表格外部来关闭弹出窗,只能将doClose方法绑定到’取消’按钮和重置表单方法上。因此我们使用的click激活模式的弹窗。
2.表单的验证功能显示不正常
主要原因是form表单组件的ref属性值都是一样的,点击提交之后,因为有多个ref所以验证出现问题。解决办法就是将ref属性绑定动态值
三、总结
这次行内编辑,让我学习到很多有用的知识,同时我这边在form弹出窗组件中发现一个关于单选的警告信息,通过网上资料找到是因为aria-hidden属性值为true导致的,一直没想到解决办法,如有知道的大神可告知下解决方法。