需求
- 点击单元格可编辑内容
- 右键单元格可选择"向下合并"或"拆分"
- 点击"新增行"按钮添加新行
- 点击"删除"按钮删除行(不能删除被合并的行)
说明
- 仅选择了具有特殊属性的列做合并与拆分操作
- 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-table
ref="table"
:data="tableData"
border
:span-method="objectSpanMethod"
style="width: 100%">
<el-table-column type="index" label="序号" width="50px"></el-table-column>
<el-table-column
v-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-input
v-model="scope.row[col.prop]"
@blur="saveEdit"
size="small"
autofocus />
</div>
<div
v-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-button
size="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 大于 1
if (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列的rowspan
const 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)
// 获取当前行的父元素的父元素,设置它的rowspan
const parentNode = elementById.parentNode.parentNode
// 获取目标行的rowspan
const targetRowspan = this.spanArr[targetRowIndex];
console.log("目标行的rowspan", targetRowspan);
// 更新spanArr数据:将当前单元格的rowspan增加目标单元格的rowspan
this.spanArr[rowIndex] += targetRowspan;
// 获取当前行的父元素的父元素,设置它的rowspan
parentNode.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.parentNode
nextParentNode.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为1
const elementById = document.getElementById('checkResult-' + rowIndex);
if (elementById) {
const parentNode = elementById.parentNode.parentNode;
parentNode.rowSpan = 1;
}
// 将当前行的rowspan重置为1
this.spanArr[rowIndex] = 1;
// 恢复被合并行的rowspan为1
for (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值为1
this.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>