当前位置: 首页 > article >正文

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

 

 

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.kler.cn/a/581263.html

相关文章:

  • 简要分析NETLINK_KOBJECT_UEVENT参数
  • 【eNSP实战】配置交换机端口安全
  • Linux服务器使用docker离线安装MySQL
  • frameworks 之屏幕旋转
  • 蓝桥杯备考:排队顺序(链表)
  • 中级网络工程师面试题参考示例(3)
  • 17、UDP怎么实现可靠传输【中高频】
  • spring-boot-starter和spring-boot-starter-web的关联
  • 【实战ES】实战 Elasticsearch:快速上手与深度实践-7.2.1Kubernetes Operator部署StatefulSet
  • KNN算法原理及python代码实现
  • 量子之歌2025财年Q2财报:净利润1.3亿元,多元化探索高成长赛道
  • 一键换肤的Qt-Advanced-Stylesheets
  • 从零开始学习PX4源码10(启动过程)
  • 网络信息安全专业(710207)网络安全攻防实训室建设方案
  • Cesium 入门教程(基于 vue3)
  • ubuntu20不同版本的cudnn切换
  • DeepSeek与Excel实现自动化办公:从基础到进阶的全面指南
  • PROC程序报无效的字符串或缓冲区长度问题
  • 初阶数据结构(C语言实现)——4.2队列
  • 支持selenium的chrome driver更新到134.0.6998.88