vue2中,在table单元格上右键,对行、列的增删操作(模拟wps里的表格交互)


HTML
<template><div><divclass="editable-area"v-html="htmlContent"contenteditable@blur="handleBlur"@contextmenu.prevent="showContextMenu"></div><button @click="transformToMd">点击转成MD</button><!-- 右键菜单 --><divv-if="contextMenu.visible"class="context-menu":style="contextMenuStyle"><div class="menu-item" @mouseenter="showSubMenu('insert')">插入<i class="el-icon-arrow-right"></i><div v-if="subMenu === 'insert'" class="sub-menu" :style="subMenuStyle"><div class="sub-menu-item" @click="insertColumn('left')">在左侧插入表列</div><divv-if="isRightmostCell"class="sub-menu-item"@click="insertColumn('right')">在右侧插入表列</div><divclass="sub-menu-item"@click="insertRow('above')":class="{ disabled: isHeader }">在上方插入表行</div><divv-if="isLastRow"class="sub-menu-item"@click="insertRow('bottom')">在下方插入表行</div></div></div><div class="menu-item" @mouseenter="showSubMenu('delete')">删除<i class="el-icon-arrow-right"></i><div v-if="subMenu === 'delete'" class="sub-menu" :style="subMenuStyle"><div class="sub-menu-item" @click="deleteColumn">表列</div><divclass="sub-menu-item"@click="deleteRow":class="{ disabled: isHeader }">表行</div></div></div></div></div>
</template>
JS
<script>
const MarkdownIt = require("markdown-it");
import { htmlToMarkdown } from "./utils";export default {data() {return {markdownText: `| 列 1 | 列 2 | 列 3 |
| :----: | :----: | :----: |
| 数据 1 | 数据 2 | 数据 3 |
| 数据 4 | 数据 5 | 数据 6 |
`,htmlContent: "",lastHtmlContent: "", // 记录上一次的 HTML 内容contextMenu: {visible: false, // 右键菜单是否显示x: 0, // 右键菜单的 X 坐标y: 0, // 右键菜单的 Y 坐标targetCell: null, // 右键点击的单元格},subMenu: "", // 当前显示的二级菜单(insert 或 delete)isHeader: false, // 是否点击了表头windowWidth: window.innerWidth, // 窗口宽度subMenuWidth: 0, // 二级菜单的宽度isRightmostCell: false, // 是否点击了最右侧单元格isLastRow: false, //是否点击了最后一行};},computed: {// 动态计算右键菜单的位置contextMenuStyle() {let left = this.contextMenu.x;let top = this.contextMenu.y;// 如果右侧空间不足,将菜单显示在左侧if (left + 150 > this.windowWidth) {left = this.contextMenu.x - 150;}return {left: left + "px",top: top + "px",};},// 动态计算二级菜单的位置subMenuStyle() {const menuWidth = 150; // 主菜单宽度const totalWidth = menuWidth + this.subMenuWidth; // 总宽度let left = menuWidth; // 默认显示在右侧if (this.contextMenu.x + totalWidth > this.windowWidth) {left = -this.subMenuWidth; // 如果右侧空间不足,显示在左侧}return {left: left + "px",};},},methods: {MarkdownToHtml(markdown) {const md = new MarkdownIt();const result = md.render(markdown);console.log("点击转成html=>", result);this.lastHtmlContent = result;this.$nextTick(() => {this.htmlContent = result;});},transformToMd() {// 获取editable-area元素的HTML内容this.htmlContent = document.querySelector(".editable-area").innerHTML;this.htmlContent = this.htmlContent.replace(/<\/strong><strong>/g, "");console.log("当前的html=>", this.htmlContent);const markdownTxt = htmlToMarkdown(this.htmlContent);this.MarkdownToHtml(markdownTxt);},handleBlur() {console.log("失去焦点");// 获取当前最新的 HTML 内容const currentHtml = document.querySelector(".editable-area").innerHTML;// 判断是否与上一次的 HTML 内容一致if (currentHtml !== this.lastHtmlContent) {console.log("内容发生变化,执行特定逻辑");// 在这里执行你的逻辑,例如:// this.someLogic();}this.lastHtmlContent = currentHtml;},showContextMenu(event) {const target = event.target;if (target.tagName === "TD" || target.tagName === "TH") {// 显示右键菜单this.contextMenu.visible = true;this.contextMenu.x = event.clientX;this.contextMenu.y = event.clientY;this.contextMenu.targetCell = target;// 判断是否点击了表头this.isHeader = target.tagName === "TH";const table = target.closest("table");if (table) {// 判断是否点击了最右侧单元格const cellIndex = target.cellIndex;const totalColumns = table.rows[0].cells.length;this.isRightmostCell = cellIndex === totalColumns - 1;// 判断是否点击了最后一行const rowIndex = target.parentElement.rowIndex;const totalRows = table.rows.length;this.isLastRow = rowIndex === totalRows - 1;}} else {// 点击非表格区域,隐藏右键菜单this.contextMenu.visible = false;}},showSubMenu(type) {this.subMenu = type;// 计算二级菜单的宽度this.$nextTick(() => {const subMenu = this.$el.querySelector(".sub-menu");if (subMenu) {// 设置二级菜单的宽度this.subMenuWidth = subMenu.offsetWidth;}});},insertColumn(position) {const table = this.contextMenu.targetCell.closest("table");const cellIndex = this.contextMenu.targetCell.cellIndex;const textAlign = this.contextMenu.targetCell.style.textAlign; // 获取当前单元格的对齐方式// 遍历每一行,插入新列for (let i = 0; i < table.rows.length; i++) {const row = table.rows[i];const isHeaderRow = row.parentElement.tagName === "THEAD"; // 判断是否属于表头// 创建新单元格const newCell = document.createElement(isHeaderRow ? "th" : "td");newCell.style.textAlign = textAlign; // 应用对齐方式到新单元格// 插入新单元格到指定位置if (position === "left") {row.insertBefore(newCell, row.cells[cellIndex]);} else {row.insertBefore(newCell, row.cells[cellIndex + 1] || null);}}this.closeContextMenu();},insertRow(position) {if (this.isHeader) return;const table = this.contextMenu.targetCell.closest("table");const rowIndex = this.contextMenu.targetCell.parentElement.rowIndex;const textAlign = this.contextMenu.targetCell.style.textAlign; // 获取当前单元格的对齐方式// 插入新行const newRow = table.insertRow(position === "above" ? rowIndex : rowIndex + 1);// 为新行添加单元格并应用对齐方式for (let i = 0; i < table.rows[0].cells.length; i++) {const newCell = newRow.insertCell(i);newCell.style.textAlign = textAlign; // 应用对齐方式到新单元格}this.closeContextMenu();},deleteColumn() {const table = this.contextMenu.targetCell.closest("table");const cellIndex = this.contextMenu.targetCell.cellIndex;// 遍历每一行,删除指定列for (let i = 0; i < table.rows.length; i++) {table.rows[i].deleteCell(cellIndex);}this.closeContextMenu();},deleteRow() {// 如果当前选项被禁用,直接返回if (this.isHeader) return;const table = this.contextMenu.targetCell.closest("table");const rowIndex = this.contextMenu.targetCell.parentElement.rowIndex;// 删除指定行table.deleteRow(rowIndex);this.closeContextMenu();},closeContextMenu() {this.contextMenu.visible = false;this.subMenu = "";},},mounted() {console.log("初始化 this.markdownText=>", this.markdownText);this.MarkdownToHtml(this.markdownText);// 监听窗口大小变化window.addEventListener("resize", () => {this.windowWidth = window.innerWidth;});// 点击页面其他区域时隐藏右键菜单document.addEventListener("click", () => {this.closeContextMenu();});},
};
</script>
CSS
<style lang="scss">
.editable-area {margin-bottom: 30px;outline: none; /* 去除文本框的轮廓 */
}
/* 针对特定类名添加表格边框 */
table {width: 100%;border-collapse: collapse; /* 确保边框折叠 */
}th,
td {border: 1px solid #000; /* 添加边框 */padding: 8px; /* 可选:增加一些内边距 */text-align: inherit; /* 确保文本对齐方式继承自原始样式 */height: 21.49px;
}
p {white-space: pre-wrap;line-height: 26px;
}/* 右键菜单样式 */
.context-menu {position: fixed;background: white;border: 1px solid #ddd;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);z-index: 1000;padding: 8px 0;border-radius: 4px;width: 150px; /* 主菜单宽度 */
}.menu-item {padding: 8px 16px;cursor: pointer;position: relative;display: flex;align-items: center;justify-content: space-between;&:hover {background: #f5f5f5;}i {color: #989898;}
}.sub-menu {position: absolute;background: white;border: 1px solid #ddd;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);border-radius: 4px;padding: 8px 0;
}.sub-menu-item {padding: 8px 16px;white-space: nowrap;cursor: pointer;&:hover {background: #f5f5f5;}&.disabled {color: #ccc;// pointer-events: none; /* 禁用点击事件 */cursor: not-allowed;}
}
</style>
相关文章:
vue2中,在table单元格上右键,对行、列的增删操作(模拟wps里的表格交互)
HTML <template><div><divclass"editable-area"v-html"htmlContent"contenteditableblur"handleBlur"contextmenu.prevent"showContextMenu"></div><button click"transformToMd">点击转成M…...
使用DeepSeek+蓝耘快速设计网页简易版《我的世界》小游戏
前言:如今,借助先进的人工智能模型与便捷的云平台,即便是新手开发者,也能开启创意游戏的设计之旅。DeepSeek 作为前沿的人工智能模型,具备强大的功能与潜力,而蓝耘智算云平台则为其提供了稳定高效的运行环境…...
解决微信小程序中调用流式接口,处理二进制数据时 TextDecoder 不兼容的问题
问题复现 最近在开发一个 AI 问答小程序时,由于接口返回的是流式二进制数据,因此我使用了 TextDecoder 的 decode 方法将二进制数据转换为文本。在开发环境中,数据处理一直没有问题,但在真机测试及上线后,发现调用接口…...
DeepSeek与QWQ大模型对比
题目为《deepseek和qwq大模型对比》1000字 DeepSeek与QWQ大模型对比 引言 在人工智能领域,大模型的发展日新月异。DeepSeek和QWQ作为两种具有代表性的大模型,各自在技术架构、应用场景和性能表现上展现出独特优势。本文将从多个维度对这两种模型进行详细…...
Java 大视界 -- Java 大数据在智慧农业农产品质量追溯与品牌建设中的应用(124)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
c++介绍信号六
信号量是c中实现对有限资源访问控制,现成通过信号量获得对资源访问的许可。可用资源大于0,线程可以对资源进行访问,此时计数器减1。当计数器为0时,不可访问资源,线程进入等待。当资源释放时,线程结束等待&a…...
DeepSeek 本地部署全流程指南:畅享专属AI体验
DeepSeek本地部署全流程指南:畅享专属AI体验 一、部署优势剖析 在本地部署DeepSeek大模型,能带来诸多好处。一方面,数据隐私更有保障,所有运算都在本地独立完成,无需联网,有效避免了数据泄露的风险。另一…...
GStreamer —— 2.18、Windows下Qt加载GStreamer库后运行 - “播放教程 6:音频可视化“(附:完整源码)
运行效果 介绍 GStreamer 带有一组将音频转换为视频的元素。他们 可用于科学可视化或为您的音乐增添趣味 player 的本教程展示了: • 如何启用音频可视化 • 如何选择可视化元素 启用音频可视化实际上非常简单。设置相应的标志,当纯音频流为 found&#…...
IP 地址与端口号:网络通信的双重坐标解析
IP 地址与端口号:网络通信的双重坐标解析 在互联网广袤无垠的世界里,数据恰似无数灵动的信息精灵,在复杂的网络脉络中穿梭往来。而确保这些数据能够精准无误地抵达目的地的关键,便是两个至关重要的核心标识符:IP 地址…...
用Deepseek写一个 HTML 和 JavaScript 实现一个简单的飞机游戏
大家好!今天我将分享如何使用 HTML 和 JavaScript 编写一个简单的飞机游戏。这个游戏的核心功能包括:控制飞机移动、发射子弹、敌机生成、碰撞检测和得分统计。代码简洁易懂,适合初学者学习和实践。 游戏功能概述 玩家控制:使用键…...
【从零开始学习计算机科学】操作系统(三)进程的互斥、同步与通信
【从零开始学习计算机科学】操作系统(三)进程的互斥、同步与通信 进程的互斥、同步与通信进程的互斥进程的同步进程的通信进程的互斥、同步与通信 进程的互斥 两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称…...
Android Compose MutableInteractionSource介绍
在 Android 开发中,Compose 是 Google 推出的现代化 UI 工具包,它让开发者能够更简洁高效地构建应用界面。而 MutableInteractionSource 是 Compose 中一个重要的组件,它可以帮助你处理用户与界面交互时的状态变化,尤其在处理交互…...
[杂学笔记] TCP和UDP的区别,对http接口解释 , Cookie和Session的区别 ,http和https的区别 , 智能指针 ,断点续传
文章目录 1. TCP和UDP的区别2. 对http接口解释3. Cookie和Session的区别4. http和https的区别5. 智能指针6.断点续传 1. TCP和UDP的区别 tcp的特点: 面向连接,可靠性高,全双工,面向字节流udp特点:无连接,不…...
Ubuntu 源码安装 Qt5
1.开发背景 Ubuntu 下安装指定版本的 Qt,最新的Qt官方已经不支持 Qt5.15.2 版本以下版本,所以有必要用旧的源码编译 Qt 库。 2.开发需求 源码安装 Qt5.12.2 3.开发环境 开发环境:Ubuntu18.04 目标版本:Qt5.12.2 4.实现步骤 4…...
【NLP 29、项目 Ⅰ:电商评论分类(好评 / 差评) 】
目录 项目介绍 一、训练及测试数据 二、代码实现 1.配置文件 config.py 2.分割训练集和验证集 split_train_valid.py 3.数据加载文件 loader.py Ⅰ、 加载和处理数据 DataGenerator ① 初始化 ② 数据加载 ③ 文本编码 ④ 补齐 / 截断 ⑤ 获取数据集长度和指定索引的数据 Ⅱ、加…...
Linux进程基础知识
1. 什么是进程? 进程就是运行中的程序,是系统资源分配的基本单位 每个进程都有唯一的PID(进程ID) 进程有父子关系,通过ps -ef可以查看 2. 进程的创建 - fork() pid_t pid fork(); - 简单理解:fork()…...
halcon deeplearn 语义分割经验分享 1
本人因为公司遗留问题,为了解决识别错误的问题。尝试过yolo12进行目标检测。初步测试良好但是是halcon的socket通信不行。故而去测试halcon 的deeplearn。自己标注数据。 注: 这个软件使用非常无脑。推荐没有基础的人去用 语义分割 以下是halcon的调用模型 *读取模型 read_dl_…...
从零开始的python学习(五)P75+P76+P77+P78+P79+P80
本文章记录观看B站python教程学习笔记和实践感悟,视频链接:【花了2万多买的Python教程全套,现在分享给大家,入门到精通(Python全栈开发教程)】 https://www.bilibili.com/video/BV1wD4y1o7AS/?p6&share_sourcecopy_web&v…...
Kylin麒麟操作系统服务部署 | ISCSI存储服务
以下所使用的环境为: 虚拟化软件:VMware Workstation 17 Pro 麒麟系统版本:Kylin-Server-V10-SP3-2403-Release-20240426-x86_64 一、网络存储结构 网络存储技术(Network Storage Technologies)是基于数据存储的一种通…...
数据结构_单链表
今天我们要开启链表的学习 🖋️🖋️🖋️ 学了顺序表我们可以知道: 🎈链表其实就是争对顺序表的缺点来设计的,补足的就是顺序表的缺点 🎈链表在物理上是上一个节点存放的下一个节点的地址 链表 …...
深陷帕金森困境,怎样重燃生活信心?
帕金森,这个悄然影响无数中老年人生活的神经系统疾病,正逐渐走进大众视野。患病后,患者常出现静止性震颤,安静时手部、下肢不自主抖动,如同在默默诉说着身体的异常。肢体变得僵硬,行动迟缓,起步…...
C语言(23)
字符串函数 11.strstr函数 1.1函数介绍: 头文件:string.h char *strstr ( const char * str1,const char *str2); 作用:在一个字符串(str1)中寻找另外一个字符串(str2)是否出现过 如果找到…...
Docker运行hello-world镜像失败或超时:Unable to find image ‘hello-world:latest‘ locally Trying to pull reposi
Docker运行hello-world镜像失败或超时,报错:Unable to find image ‘hello-world:latest’ locally Trying to pull repository docker.io/library/hello-world … /usr/bin/docker-current: missing signature key. See ‘/usr/bin/docker-current run …...
Linux内核如何和设备树协同工作的?
1.编写设备树 cd arch/riscv/boot/dts/ 再cd到厂商,例如下述内容。 2.编译设备树(dts->dtb)通过dtc命令来转换 3.解析设备树 例如上述内容,都是对设备树的解析。 这里重点说一下内核对设备树的处理吧,因为这个内…...
electron的通信方式(三种)
文章目录 一、渲染进程向主进程发送消息二、渲染进程向主进程发送消息并异步获取结果三、主进程向渲染进程发送消息 electron的主要是主线程和渲染线程之间的通信,简单记录一下三种通信方式 一、渲染进程向主进程发送消息 利用ipcRenderer.send()和ipcMain.on()方法…...
LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)
文章目录 LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)Self-Attention (自注意力机制)结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMas…...
高效编程指南:PyCharm与DeepSeek的完美结合
DeepSeek接入Pycharm 前几天DeepSeek的充值窗口又悄悄的开放了,这也就意味着我们又可以丝滑的使用DeepSeek的API进行各种辅助性工作了。本文我们来聊聊如何在代码编辑器中使用DeepSeek自动生成代码。 注:本文适用于所有的JetBrains开发工具,…...
嵌入式c学习四
c语言的输入输出:ANSI组织发布的标准c库,使用函数需要包含对应头文件,使用输入输出函数时需要包含stdio.h (stdio -> standard input output)标准输入输出 格式化输出:int printf(const char * restrict format, ...)…...
爱可以传递,幸福可以具象化
遇到什么:晚上上课学生吵吵吵,把学生手机全部收了,放讲台上。 感受到的情绪:很烦躁。 反思:收手机也不是长久之计,可是物理有什么翻转课堂呢? 明天的待办事项:早上高数选修课&#x…...
力扣-数组-367 有效的完全平方数
思路和时间复杂度 思路:利用二分,确定区间是左闭右闭,然后根据大小进行二分时间复杂度: 代码 class Solution { public:bool isPerfectSquare(int num) {bool flag false;if(num 0 || num 1) return true;long long …...
