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

news2025/3/12 9:38:02

 

 

HTML

<template>
  <div>
    <div
      class="editable-area"
      v-html="htmlContent"
      contenteditable
      @blur="handleBlur"
      @contextmenu.prevent="showContextMenu"
    ></div>
    <button @click="transformToMd">点击转成MD</button>

    <!-- 右键菜单 -->
    <div
      v-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>
          <div
            v-if="isRightmostCell"
            class="sub-menu-item"
            @click="insertColumn('right')"
          >
            在右侧插入表列
          </div>
          <div
            class="sub-menu-item"
            @click="insertRow('above')"
            :class="{ disabled: isHeader }"
          >
            在上方插入表行
          </div>
          <div
            v-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>
          <div
            class="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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用DeepSeek+蓝耘快速设计网页简易版《我的世界》小游戏

前言&#xff1a;如今&#xff0c;借助先进的人工智能模型与便捷的云平台&#xff0c;即便是新手开发者&#xff0c;也能开启创意游戏的设计之旅。DeepSeek 作为前沿的人工智能模型&#xff0c;具备强大的功能与潜力&#xff0c;而蓝耘智算云平台则为其提供了稳定高效的运行环境…

解决微信小程序中调用流式接口,处理二进制数据时 TextDecoder 不兼容的问题

问题复现 最近在开发一个 AI 问答小程序时&#xff0c;由于接口返回的是流式二进制数据&#xff0c;因此我使用了 TextDecoder 的 decode 方法将二进制数据转换为文本。在开发环境中&#xff0c;数据处理一直没有问题&#xff0c;但在真机测试及上线后&#xff0c;发现调用接口…

Java 大视界 -- Java 大数据在智慧农业农产品质量追溯与品牌建设中的应用(124)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

c++介绍信号六

信号量是c中实现对有限资源访问控制&#xff0c;现成通过信号量获得对资源访问的许可。可用资源大于0&#xff0c;线程可以对资源进行访问&#xff0c;此时计数器减1。当计数器为0时&#xff0c;不可访问资源&#xff0c;线程进入等待。当资源释放时&#xff0c;线程结束等待&a…

GStreamer —— 2.18、Windows下Qt加载GStreamer库后运行 - “播放教程 6:音频可视化“(附:完整源码)

运行效果 介绍 GStreamer 带有一组将音频转换为视频的元素。他们 可用于科学可视化或为您的音乐增添趣味 player 的本教程展示了&#xff1a; • 如何启用音频可视化 • 如何选择可视化元素 启用音频可视化实际上非常简单。设置相应的标志&#xff0c;当纯音频流为 found&#…

用Deepseek写一个 HTML 和 JavaScript 实现一个简单的飞机游戏

大家好&#xff01;今天我将分享如何使用 HTML 和 JavaScript 编写一个简单的飞机游戏。这个游戏的核心功能包括&#xff1a;控制飞机移动、发射子弹、敌机生成、碰撞检测和得分统计。代码简洁易懂&#xff0c;适合初学者学习和实践。 游戏功能概述 玩家控制&#xff1a;使用键…

Android Compose MutableInteractionSource介绍

在 Android 开发中&#xff0c;Compose 是 Google 推出的现代化 UI 工具包&#xff0c;它让开发者能够更简洁高效地构建应用界面。而 MutableInteractionSource 是 Compose 中一个重要的组件&#xff0c;它可以帮助你处理用户与界面交互时的状态变化&#xff0c;尤其在处理交互…

[杂学笔记] TCP和UDP的区别,对http接口解释 , Cookie和Session的区别 ,http和https的区别 , 智能指针 ,断点续传

文章目录 1. TCP和UDP的区别2. 对http接口解释3. Cookie和Session的区别4. http和https的区别5. 智能指针6.断点续传 1. TCP和UDP的区别 tcp的特点&#xff1a; 面向连接&#xff0c;可靠性高&#xff0c;全双工&#xff0c;面向字节流udp特点&#xff1a;无连接&#xff0c;不…

【NLP 29、项目 Ⅰ:电商评论分类(好评 / 差评) 】

目录 项目介绍 一、训练及测试数据 二、代码实现 1.配置文件 config.py 2.分割训练集和验证集 split_train_valid.py 3.数据加载文件 loader.py Ⅰ、 加载和处理数据 DataGenerator ① 初始化 ② 数据加载 ③ 文本编码 ④ 补齐 / 截断 ⑤ 获取数据集长度和指定索引的数据 Ⅱ、加…

halcon deeplearn 语义分割经验分享 1

本人因为公司遗留问题,为了解决识别错误的问题。尝试过yolo12进行目标检测。初步测试良好但是是halcon的socket通信不行。故而去测试halcon 的deeplearn。自己标注数据。 注: 这个软件使用非常无脑。推荐没有基础的人去用 语义分割 以下是halcon的调用模型 *读取模型 read_dl_…

从零开始的python学习(五)P75+P76+P77+P78+P79+P80

本文章记录观看B站python教程学习笔记和实践感悟&#xff0c;视频链接&#xff1a;【花了2万多买的Python教程全套&#xff0c;现在分享给大家&#xff0c;入门到精通(Python全栈开发教程)】 https://www.bilibili.com/video/BV1wD4y1o7AS/?p6&share_sourcecopy_web&v…

Kylin麒麟操作系统服务部署 | ISCSI存储服务

以下所使用的环境为&#xff1a; 虚拟化软件&#xff1a;VMware Workstation 17 Pro 麒麟系统版本&#xff1a;Kylin-Server-V10-SP3-2403-Release-20240426-x86_64 一、网络存储结构 网络存储技术&#xff08;Network Storage Technologies&#xff09;是基于数据存储的一种通…

数据结构_单链表

今天我们要开启链表的学习 &#x1f58b;️&#x1f58b;️&#x1f58b;️ 学了顺序表我们可以知道&#xff1a; &#x1f388;链表其实就是争对顺序表的缺点来设计的&#xff0c;补足的就是顺序表的缺点 &#x1f388;链表在物理上是上一个节点存放的下一个节点的地址 链表 …

深陷帕金森困境,怎样重燃生活信心?

帕金森&#xff0c;这个悄然影响无数中老年人生活的神经系统疾病&#xff0c;正逐渐走进大众视野。患病后&#xff0c;患者常出现静止性震颤&#xff0c;安静时手部、下肢不自主抖动&#xff0c;如同在默默诉说着身体的异常。肢体变得僵硬&#xff0c;行动迟缓&#xff0c;起步…

C语言(23)

字符串函数 11.strstr函数 1.1函数介绍&#xff1a; 头文件&#xff1a;string.h char *strstr ( const char * str1,const char *str2); 作用&#xff1a;在一个字符串&#xff08;str1&#xff09;中寻找另外一个字符串&#xff08;str2&#xff09;是否出现过 如果找到…

Docker运行hello-world镜像失败或超时:Unable to find image ‘hello-world:latest‘ locally Trying to pull reposi

Docker运行hello-world镜像失败或超时&#xff0c;报错&#xff1a;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到厂商&#xff0c;例如下述内容。 2.编译设备树&#xff08;dts->dtb&#xff09;通过dtc命令来转换 3.解析设备树 例如上述内容&#xff0c;都是对设备树的解析。 这里重点说一下内核对设备树的处理吧&#xff0c;因为这个内…

LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)

文章目录 LLM中的transformer结构学习&#xff08;二 完结 Multi-Head Attention、Encoder、Decoder&#xff09;Self-Attention &#xff08;自注意力机制&#xff09;结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMas…

高效编程指南:PyCharm与DeepSeek的完美结合

DeepSeek接入Pycharm 前几天DeepSeek的充值窗口又悄悄的开放了&#xff0c;这也就意味着我们又可以丝滑的使用DeepSeek的API进行各种辅助性工作了。本文我们来聊聊如何在代码编辑器中使用DeepSeek自动生成代码。 注&#xff1a;本文适用于所有的JetBrains开发工具&#xff0c…

爱可以传递,幸福可以具象化

遇到什么&#xff1a;晚上上课学生吵吵吵&#xff0c;把学生手机全部收了&#xff0c;放讲台上。 感受到的情绪&#xff1a;很烦躁。 反思&#xff1a;收手机也不是长久之计&#xff0c;可是物理有什么翻转课堂呢&#xff1f; 明天的待办事项&#xff1a;早上高数选修课&#x…