用el-table实现的可编辑的动态表格组件

用el-table实现的可编辑的动态表格组件

  • 需求
  • 说明
  • 实现效果
  • 代码

需求

  1. 点击单元格可编辑内容
  2. 右键单元格可选择"向下合并"或"拆分"
  3. 点击"新增行"按钮添加新行
  4. 点击"删除"按钮删除行(不能删除被合并的行)

说明

  1. 仅选择了具有特殊属性的列做合并与拆分操作
  2. span-method没有生效,【暂不清楚】,当前是使用的操作dom的方式来改变合并状态
  3. 删除时,如果是合并项的第一项会自动拆分并删除,其他则提示不能删除

实现效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码

使用数据

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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/120363.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

动手学习深度学习-深度学习知识大纲

使用数据集训练一个模型的过程&#xff1a;1、构造随机初始化参数的模型2、获取数据集3、构造损失和优化器用于调整参数&#xff0c;使模型在数据样本中表现更好4、重复第2步和第3步1、机器学习组件1.1、数据处理&#xff08;包含一定数学知识&#xff09;数据样本/数据点/数据…

选择跨网文件交换系统的核心因素有哪些?

在数字化进程加速的今天&#xff0c;企业常面临办公网、生产网、研发网等多网络环境并存的情况。传统的U盘拷贝或FTP传输方式&#xff0c;不仅效率低下&#xff0c;更存在严重的安全与管理隐患——例如明文传输导致的数据泄露、缺乏审计日志引发合规风险&#xff0c;以及面对TB…

中兴通讯联合深兰科技发布“AI问诊助手智能体”全场景解决方案

2025年7月26日&#xff0c;2025世界人工智能大会(WAIC2025)在上海开幕。全球领先的ICT解决方案提供商中兴通讯以“算力普惠 AI向实”为主题参展&#xff0c;围绕AI与产业深度融合&#xff0c;突出智算基础设施、行业大模型、全场景AI应用三大领域的创新&#xff0c;强调通过“连…

【AI大模型】披着羊皮的狼--自动化生成越狱提示的系统(ReNeLLM)

南京大学 & 美团联合团队发表了一篇 NAACL 2024 论文《A Wolf in Sheep’s Clothing: Generalized Nested Jailbreak Prompts can Fool Large Language Models Easily》&#xff08;披着羊皮的狼&#xff09;。非常有意思的名字&#xff0c;他们提出了一套名叫 ReNeLLM 的自…

50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | VerifyAccountUi(验证码组件)

&#x1f4c5; 我们继续 50 个小项目挑战&#xff01;—— VerifyAccountUi组件 仓库地址&#xff1a;https://github.com/SunACong/50-vue-projects 项目预览地址&#xff1a;https://50-vue-projects.vercel.app/ 使用 Vue 3 的 <script setup> 语法结合 Tailwind CS…

Solana新手上路:完成第一笔SOL转账

大家好&#xff01;如果大家正对Solana生态系统充满好奇&#xff0c;渴望在代码层面与这个高性能区块链进行第一次“亲密接触”&#xff0c;那么来对地方了。今天&#xff0c;我们将一起完成一件激动人心的事情&#xff1a;通过编写一段简单的JavaScript代码&#xff0c;在Sola…

jangow-01-1.0.1靶机教程攻略

第一步搭建环境 靶机下载地址&#xff1a;https://download.vulnhub.com/jangow/jangow-01-1.0.1.ova 下载好了直接用VM打开 右击虚拟机设置&#xff0c;网络连接改成nat模式 第二步信息收集 开启虚拟机&#xff0c;右击编辑查看一下靶机的网段 用cali的nmap扫一下 nmap 1…

Linux和shell

最快入门的方式是使用苹果系统。此外&#xff0c;累计补充学习&#xff1a;一、目录结构/bin&#xff0c;二进制文件 /boot&#xff0c;启动文件 /dev&#xff0c;设备文件 /home&#xff0c;主目录&#xff0c;一般外接包、安装包放在这里 /lib&#xff0c;库文件 /opt&#x…

机器学习之逻辑回归(Logistic Regression)

一、什么是逻辑回归 逻辑回归也称作logistic回归分析&#xff0c;是一种由线性回归衍生出来的分析模型&#xff0c;属于机器学习中的监督学习。其推导过程与计算方式类似于回归的过程&#xff0c;但实际上主要是用来解决二分类问题&#xff08;也可以解决多分类问题&#x…

mac环境配置rust

rustup 是一个命令行工具&#xff0c;用于管理 Rust 编译器和相关工具链 sh 体验AI代码助手 代码解读复制代码curl --proto ‘https’ --tlsv1.2 -sSf https://sh.rustup.rs | sh使得 Rust 的安装在当前 shell 环境中生效 如果你使用的是 bash, zsh 或其他类似的 shell&#xf…

LPC2132GPIO

LPC2132具有多达47个通用I/O口&#xff08;GPIO&#xff0c;General I/O port&#xff09;&#xff0c;分别为P0[31:0]、P1[31:16]&#xff0c;其中&#xff0c;P0.24未用&#xff0c;P0.31仅为输出口。由于口线与其它功能复用&#xff0c;需要进行相关的管脚连接模块&#xff…

【AlphaFold3】网络架构篇(2)|Input Embedding 对输入进行特征嵌入

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; Yaoyao2024往期回顾&#xff1a;【AlphaFold3】网络架构篇&#xff08;1&#xff09;|概览预测算法每日一言&#x1f33c;: 去留无意&#xff0c;闲看庭前花开花落&#xff1b…

【传奇开心果系列】Flet框架左右两边堆叠图片前移补位轮播组件自定义模板

一、效果展示GIF动图二、使用场景介绍 Flet左右两边堆叠图片前移补位轮播组件 是一个基于 Flet 框架的自定义组件&#xff0c;适用于需要展示多张图片并实现动态轮播效果的应用场景。该组件特别适合用于以下场景&#xff1a; 产品展示&#xff1a;在电商网站或应用中&#xff0…

【Kubernetes 指南】基础入门——Kubernetes 201(三)

三、资源限制- Kubernetes 通过 cgroups 提供容器资源管理的功能&#xff0c;可以限制每个容器的 CPU 和内存使用&#xff0c;比 如对于上一讲创建的 deployment&#xff1b;- 可以通过下面的命令限制 nginx 容器最多只用 50% 的 CPU 和 128MB 的内存&#xff1a;- 这等同于在每…

【目标检测】d-fine模型部署

官网介绍显示&#xff0c;d-fine模型效果很好&#xff0c;例如下图中&#xff0c;非常模糊的人也能被识别出来。官网教程有些细节没写&#xff0c;这里补充一下。1.数据格式 数据长这样&#xff08;图中的unrrelated_pic无用&#xff09;。具体的格式可以由大模型生成。我这里有…

用 AI 解析采购订单,从上传到自动生成 Draft 订单全流程实战

网罗开发&#xff08;小红书、快手、视频号同名&#xff09;大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等方…

远程仓库地址发生变化

代码写着写着&#xff0c;组长突然说git地址迁移了&#xff0c;让我把自己的代码也迁移过去。以前没遇到过&#xff0c;今天试了试&#xff0c;迁移成功了&#xff0c;值得记录一下。一、场景1首先是最简单的场景&#xff0c;新建的仓库是空的&#xff0c;然后本地代码在master…

【硬件-笔试面试题】硬件/电子工程师,笔试面试题-48,(知识点:BUCK电路的损耗,开关损耗,导通损耗,电感损耗、驱动损耗)

目录 1、题目 2、解答 一、开关损耗&#xff08;Switching Losses&#xff09; &#xff1a;与开关频率成正比&#xff0c;但提高频率&#xff0c;可减小所需电感电容的体积&#xff0c;需平衡 二、导通损耗&#xff08;Conduction Losses&#xff09;&#xff1a;与导通时…

机械零件深凹槽检测方法的探究 - 激光频率梳 3D 轮廓检测

一、引言在机械制造领域&#xff0c;机械零件深凹槽的检测质量直接影响设备的性能与可靠性。以航空发动机止动螺母为例&#xff0c;其矩形凹槽深度公差通常在微米级&#xff0c;传统检测方法面临诸多挑战。平台推表检测法因基准不重合导致误差较大&#xff0c;工作型三坐标测量…

九章数学体系:打破“吃苦悖论”,重构学习真谛

在我们的成长历程中&#xff0c;“你不吃儿时读书之苦&#xff0c;就得吃长大工作之苦”这句话耳熟能详&#xff0c;它俨然成为家长与社会教育孩子的励志金句。然而&#xff0c;神奇的九章数学体系却为我们揭示&#xff0c;这背后实则隐藏着一个值得深度探究的悖论。现在&#…