用el-table实现的可编辑的动态表格组件
用el-table实现的可编辑的动态表格组件
- 需求
- 说明
- 实现效果
- 代码
需求
- 点击单元格可编辑内容
- 右键单元格可选择"向下合并"或"拆分"
- 点击"新增行"按钮添加新行
- 点击"删除"按钮删除行(不能删除被合并的行)
说明
- 仅选择了具有特殊属性的列做合并与拆分操作
- span-method没有生效,【暂不清楚】,当前是使用的操作dom的方式来改变合并状态
- 删除时,如果是合并项的第一项会自动拆分并删除,其他则提示不能删除
实现效果
代码
使用数据
cs_columns: any = [{ label: '项目', prop: 'proName' },{ label: '位置', prop: 'projectLocation' },{ label: '有无地铁', prop: 'subway' },{ label: '通勤时间', prop: 'time' },{ label: '结论', prop: 'checkResult' },]
cs_checkItemsList: any = [{proName: '土壤重金属检测',projectLocation: '农田北区',subway: '无',time: '30分钟',checkResult: '合格'},{proName: '水质农药残留',projectLocation: '灌溉水渠',subway: '有',time: '45分钟',checkResult: '不合格'},{proName: '空气污染物检测',projectLocation: '加工厂周边',subway: '无',time: '60分钟',checkResult: '合格'},{proName: '农产品营养成分',projectLocation: '果蔬大棚',subway: '有',time: '25分钟',checkResult: '合格'},{proName: '饲料添加剂检测',projectLocation: '养殖场',subway: '无',time: '40分钟',checkResult: '不合格'}]
使用EditTable组件
<edit-table ref="editTable" :columns="columns" :initial-data="checkItemsList"></edit-table>
EditTable组件具体实现
<template><div><el-button class="m-y-16" @click="addRow" type="primary" size="small">新增行</el-button><el-tableref="table":data="tableData"border:span-method="objectSpanMethod"style="width: 100%"><el-table-column type="index" label="序号" width="50px"></el-table-column><el-table-columnv-for="col in columns":key="col.prop":prop="col.prop":label="col.label":width="col.width"><template #default="scope"><div v-if="editingCell.rowIndex === scope.$index && editingCell.colKey === col.prop"><el-inputv-model="scope.row[col.prop]"@blur="saveEdit"size="small"autofocus /></div><divv-else:id="col.prop + '-' + scope.$index"@click="handleCellClick(scope.$index, col.prop)"@contextmenu="handleContextMenu($event, scope.$index, col.prop)">{{ scope.row[col.prop] || '--' }}</div></template></el-table-column><el-table-column label="操作" width="100"><template #default="scope"><el-buttonsize="small"@click="deleteRow(scope.$index)"type="danger">删除</el-button></template></el-table-column></el-table><!-- 右键菜单 --><div v-show="contextMenu.visible":style="{left: contextMenu.left+'px', top: contextMenu.top+'px'}"class="context-menu"><div class="menu-item" @click="mergeCells">向下合并</div><div class="menu-item" @click="splitCells">拆分</div></div></div>
</template><script>
export default {name: 'EditTable',props: {initialData: {type: Array,default: () => []},columns: {type: Array,default: () => []},},data() {return {tableData: this.initialData.length > 0 ? this.initialData : [],spanArr: [],editingCell: {rowIndex: -1,colKey: ''},contextMenu: {visible: false,left: 0,top: 0,rowIndex: -1},colIndex: -1,checkItemsList: []}},created() {this.colIndex = this.columns.findIndex(item => item.prop === 'checkResult');this.initSpanArr();// 点击其他地方关闭右键菜单document.addEventListener('click', () => {this.contextMenu.visible = false})},mounted() {// 初始调用保持不变this.$nextTick(() => {this.initEditTable();});},methods: {// 初始化合并规则initSpanArr() {this.spanArr = this.tableData.map(item => {return item.rowspan || 1})},initEditTable() {// 添加更多的检查条件确保元素已渲染if (this.$refs.table && this.tableData.length > 0 && this.spanArr.length > 0) {this.tableData.forEach((item, index) => {// 确保元素存在且 rowspan 大于 1if (item.rowspan > 1 && index < this.spanArr.length) {const elementById = document.getElementById('checkResult-' + index);if (elementById) {const parentNode = elementById.parentNode.parentNode;parentNode.rowSpan = item.rowspan;// 隐藏被合并的行for (let i = 1; i < item.rowspan && (index + i) < this.tableData.length; i++) {const nextElementById = document.getElementById('checkResult-' + (index + i));if (nextElementById) {const nextParentNode = nextElementById.parentNode.parentNode;nextParentNode.style.display = 'none';}}}}});}},// 合并单元格方法objectSpanMethod({ column, rowIndex }) {if (column.prop === 'checkResult') {if (this.spanArr[rowIndex]) {return {rowspan: this.spanArr[rowIndex],colspan: 1}}}},// 单元格点击编辑handleCellClick(rowIndex, colKey) {this.editingCell = { rowIndex, colKey }},// 保存编辑saveEdit() {this.editingCell = { rowIndex: -1, colKey: '' }},// 处理右键菜单handleContextMenu(event, rowIndex, colKey) {if (colKey === 'checkResult') {event.preventDefault();this.contextMenu = {visible: true,left: event.clientX,top: event.clientY,rowIndex};}},// 合并单元格mergeCells() {const { rowIndex } = this.contextMenu;if (rowIndex === -1) return;// 获取当前行在checkResult列的rowspanconst currentRowspan = this.spanArr[rowIndex];// 检查是否可以合并下一行(防止越界)console.log("当前行的index:",rowIndex,"当前行的rowSpan:", currentRowspan, "表数据行数", this.tableData.length);if (rowIndex + currentRowspan >= this.tableData.length) {this.$message.warning('无法向下合并,已到达表格底部');this.contextMenu.visible = false;return;}// 检查目标行是否已被其他单元格合并const targetRowIndex = rowIndex + currentRowspan;console.log("目标行的index", targetRowIndex);if (this.spanArr[targetRowIndex] === 0) {this.$message.warning('无法合并,目标行已被其他单元格合并');this.contextMenu.visible = false;return;}// 检查下一行的值是否相同console.log("当前行的值", this.tableData[rowIndex][this.columns[this.colIndex].prop]);console.log("下一行的值", this.tableData[targetRowIndex][this.columns[this.colIndex].prop]);if (this.tableData[targetRowIndex][this.columns[this.colIndex].prop] !== this.tableData[rowIndex][this.columns[this.colIndex].prop]) {this.$message.warning('无法合并,目标行单元格的值不相同');this.contextMenu.visible = false;return;}// 获取当前行的元素const elementById = document.getElementById('checkResult-' + rowIndex)// 获取当前行的父元素的父元素,设置它的rowspanconst parentNode = elementById.parentNode.parentNode// 获取目标行的rowspanconst targetRowspan = this.spanArr[targetRowIndex];console.log("目标行的rowspan", targetRowspan);// 更新spanArr数据:将当前单元格的rowspan增加目标单元格的rowspanthis.spanArr[rowIndex] += targetRowspan;// 获取当前行的父元素的父元素,设置它的rowspanparentNode.rowSpan = this.spanArr[rowIndex]// 将被合并的行标记为rowspan=0,表示被合并// 如果目标行还合并了其他行,需要将这些行也标记为被当前行合并for (let i = 0; i < targetRowspan; i++) {if (targetRowIndex + i < this.spanArr.length) {let nextElementById = document.getElementById('checkResult-' + (targetRowIndex + i))let nextParentNode = nextElementById.parentNode.parentNodenextParentNode.style.display = 'none'this.spanArr[targetRowIndex + i] = 0;}}console.log(this.spanArr);this.contextMenu.visible = false;},// 拆分单元格splitCells() {const { rowIndex } = this.contextMenu;if (rowIndex === -1) return;const currentRowspan = this.spanArr[rowIndex];console.log("当前行行数", rowIndex,"当前行的所占行数",currentRowspan);// 如果当前单元格没有合并其他行,则无需拆分if (currentRowspan <= 1) {this.$message.warning('当前单元格未合并其他行');this.contextMenu.visible = false;return;}// 获取当前行的元素并设置rowSpan为1const elementById = document.getElementById('checkResult-' + rowIndex);if (elementById) {const parentNode = elementById.parentNode.parentNode;parentNode.rowSpan = 1;}// 将当前行的rowspan重置为1this.spanArr[rowIndex] = 1;// 恢复被合并行的rowspan为1for (let i = 1; i < currentRowspan; i++) {const targetIndex = rowIndex + i;// 显示被隐藏的单元格const nextElementById = document.getElementById('checkResult-' + targetIndex);if (nextElementById) {const nextParentNode = nextElementById.parentNode.parentNode;nextParentNode.style.display = 'table-cell';nextParentNode.rowSpan = 1;}// 恢复被合并行的span值为1this.spanArr[targetIndex] = 1;this.tableData[targetIndex][this.columns[this.colIndex].prop] = this.tableData[rowIndex][this.columns[this.colIndex].prop];}console.log(this.spanArr);this.contextMenu.visible = false;},// 新增行addRow() {const newRow = {};this.columns.forEach(col => {newRow[col.prop] = '';});this.tableData.push(newRow);this.spanArr.push(1);},// 删除行deleteRow(index) {// 如果该行是被合并的行,则不允许删除if (this.spanArr[index] === 0) {this.$message.warning('不能删除被合并的行,请先拆分单元格');return;}// 如果是合并的起始行,需要先拆分if (this.spanArr[index] > 1) {// 设置当前行索引用于拆分this.contextMenu.rowIndex = index;// 执行拆分this.splitCells();}this.tableData.splice(index, 1)this.spanArr.splice(index, 1)}}
}
</script><style scoped>
.context-menu {position: fixed;z-index: 9999;background: #fff;border: 1px solid #ebeef5;border-radius: 4px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);padding: 10px;display: flex;flex-direction: column;
}.menu-item {padding: 8px 20px;cursor: pointer;color: #606266;
}.menu-item:hover {background: #f5f7fa;color: #409eff;
}
</style>