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>