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>