refactor(editor): 优化富文本编辑器内容格式转换逻辑

- 改进 HTML 到 Markdown 的转换规则,处理块级元素和行内元素的嵌套关系
- 优化图片和视频标签的解析逻辑,确保附件正确提取
- 修复换行符处理问题,避免产生多余空行
- 增强居中文本和标题的格式转换准确性
- 清理调试日志和冗余代码
This commit is contained in:
A1300399510
2025-11-30 23:54:41 +08:00
parent 1b5cf79300
commit 57bf0ed3eb
5 changed files with 182 additions and 80 deletions

View File

@@ -53,8 +53,6 @@ const editApp = createApp({
cUpload();
// console.log(valueA.value);
valueUrl = valueA.value.innerText;
if (location.hostname == "127.0.0.1") {
@@ -67,6 +65,7 @@ const editApp = createApp({
isLogin.value = true;
}
});
let imageLength = 10;
@@ -167,19 +166,13 @@ const editApp = createApp({
const infoTarget = data.info || {};
// if (location.hostname == "127.0.0.1")
// infoTarget.content = `<p><img src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polfHeD1NFX9ddrB_WbUGy8P79gQxccQOeR45kV7NkzNDQyOQ~~?aid=1009985" alt="图片描述" data-href="https://i-operation.csdnimg.cn/ad/ad_pic/a0beaaca1e2047e0ae5c0783e02b3c0a.png" style=""/></p><div data-w-e-type="video" data-w-e-is-void>
// <video poster="https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polcniH1NFX9ddrB_WbUGy8P79gQxcSQbvLtMsV7NkzNDQyOQ~~?aid=1009771" controls="true" width="auto" height="auto"><source src="https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polcniG1NFX9ddrB_WbUGy8P79gQxcSFbqQ78MV7NkzNDQyOQ~~?aid=1009770" type="video/mp4"/></video>
// </div><p><br></p>`;
console.log("content", infoTarget.content);
// console.log("content", infoTarget.content);
if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoTarget.attachments);
console.log("content", infoTarget.content);
// console.log("content", infoTarget.content);
info.value = infoTarget;
token.value = data.token;
console.log("data", data);
initEditor();
})
@@ -216,18 +209,9 @@ const editApp = createApp({
["insertImage"]: {
onInsertedImage(imageNode) {
console.log("insertImage");
// if (imageNode == null) return;
const { src, alt, url, href } = imageNode;
console.log("src", src);
console.log("alt", alt);
console.log("url", url);
console.log("href", href);
},
async parseImageSrc(src) {
console.log("parseImageSrc", src);
// 如果图片链接中已经包含了 ?aid= ,则说明是本站图片,直接返回,无需处理
if (src.includes("?aid=")) return src;
@@ -323,6 +307,7 @@ const editApp = createApp({
editor = createEditor({
selector: "#editor-container",
html: infoTarget.content,
// html: "",
config: editorConfig,
mode: "default",
});
@@ -409,10 +394,10 @@ const editApp = createApp({
let html = formattedText || "";
// 0.5 [p] -> <p>
html = html.replace(/\[p\]([\s\S]*?)\[\/p\]/gi, "<p>$1</p>");
// html = html.replace(/\[p\]([\s\S]*?)\[\/p\]/gi, "<p>$1</p>");
// 0. 基础清理:换行符转 <br>div 转 p (保持原有逻辑)
html = html.replace(/\n/g, "<br>");
html = html.replace(/(\r\n|\n|\r)/g, "<br>");
html = html.replace(/<div(\s|>)/gi, "<p$1");
html = html.replace(/<\/div>/g, "</p>");
html = html.replace(/<br><div>/g, "<div>");
@@ -432,11 +417,15 @@ const editApp = createApp({
// 普通居中
html = html.replace(/\[align=center\]([\s\S]*?)\[\/align\]/gi, '<p style="text-align: center;">$1</p>');
// 移除 align 后紧跟的换行符(因为 align 转换为了块级元素 p/h1其后的换行符通常是多余的
html = html.replace(/(<p style="text-align: center;">.*?<\/p>)\s*<br>/gi, "$1");
html = html.replace(/(<h1 style="text-align: center;">.*?<\/h1>)\s*<br>/gi, "$1");
// 定义图片处理函数
const processImg = (aid, width, height, href) => {
const image = imageList.find((img) => String(img.aid) === String(aid));
if (!image) return "";
if (!image) return "";
// 移除已使用的图片
const index = imageList.findIndex((img) => String(img.aid) === String(aid));
if (index > -1) imageList.splice(index, 1);
@@ -451,11 +440,11 @@ const editApp = createApp({
const wStyle = formatStyleVal(width);
const hStyle = formatStyleVal(height);
let styleParts = [];
if (wStyle) styleParts.push(`width: ${wStyle}`);
if (hStyle) styleParts.push(`height: ${hStyle}`);
if (styleParts.length > 0) style = `style="${styleParts.join('; ')};"`;
let dataHref = href ? ` data-href="${href}"` : "";
@@ -519,29 +508,62 @@ const editApp = createApp({
// 确保所有顶层元素都是块级元素 (Slate/WangEditor 要求)
const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'UL', 'OL', 'PRE', 'TABLE', 'FIGURE', 'HR'];
const children = Array.from(__c.childNodes);
let wrapper = null;
const newContainer = document.createElement('div');
let currentInlineNodes = [];
let lastWasBlock = false;
children.forEach((node) => {
const flushInlines = () => {
if (currentInlineNodes.length > 0) {
if (currentInlineNodes.length > 0) {
const p = document.createElement('p');
currentInlineNodes.forEach(node => p.appendChild(node));
newContainer.appendChild(p);
}
currentInlineNodes = [];
}
};
Array.from(__c.childNodes).forEach((node) => {
// 判断是否为块级元素
const isBlock = node.nodeType === 1 && blockTags.includes(node.tagName);
if (isBlock) {
wrapper = null; // 遇到块级元素,重置包裹容器
} else {
// 如果是行内元素文本、图片、span、strong等放入 p 标签
// 忽略纯空白文本节点(如果它们在块级元素之间),避免产生过多的空段落
// 但为了保险起见(避免吞掉有意义的空格),这里暂时全部包裹
if (!wrapper) {
wrapper = document.createElement('p');
__c.insertBefore(wrapper, node);
flushInlines();
newContainer.appendChild(node);
// 记录最后添加的是块级元素
lastWasBlock = true;
} else if (node.nodeName === 'BR') {
if (currentInlineNodes.length > 0) {
flushInlines();
lastWasBlock = false; // 刚刚结束了一个段落,不算紧挨着块级
} else {
// 如果前面紧挨着块级元素,忽略这个 BR避免块级元素后的换行产生空行
if (lastWasBlock) {
// 忽略
lastWasBlock = false; // 消耗掉块级后的换行状态,避免连续 BR 被吞
} else {
const p = document.createElement('p');
p.innerHTML = '<br>';
newContainer.appendChild(p);
lastWasBlock = false;
}
}
wrapper.appendChild(node);
} else {
// 过滤掉块级元素之间或开头的纯空白文本节点,避免产生空的 P 标签
if (node.nodeType === 3 && !node.textContent.trim()) {
if (currentInlineNodes.length > 0) {
currentInlineNodes.push(node);
}
} else {
currentInlineNodes.push(node);
}
lastWasBlock = false;
}
});
html = __c.innerHTML;
flushInlines();
html = newContainer.innerHTML;
console.log("初始化显示的html", html);
return html;
@@ -563,6 +585,8 @@ const editApp = createApp({
// 提交
const submit = (status) => {
if (location.hostname == "127.0.0.1") status = 0
if (realname.value == 0 && userInfoWin.value?.uin > 0) {
openAttest();
return;
@@ -587,18 +611,15 @@ const editApp = createApp({
info.value["attachments"]["images"] = images;
info.value["attachments"]["videos"] = videos;
console.log("原始html", content);
// console.log("原始html", content);
content = formatContent(content);
console.log("最终html", content);
// console.log("最终html", content);
const data = {
...infoTarget,
content,
};
console.log("data", data);
if (location.hostname == "127.0.0.1") {
status = 0;
}
// console.log("data", data);
ajax("/v2/api/forum/postPublishTopic", {
info: data,
@@ -623,6 +644,8 @@ const editApp = createApp({
const formatContent = (html) => {
if (!html) return "";
// <p><br></p> 转换为单个换行符
html = html.replace(/<p><br><\/p>/gi, "\n");
// 1. 处理居中 [align=center] (p, div, h1-h6)
html = html.replace(/<(p|div|h[1-6])[^>]*style="[^"]*text-align:\s*center;[^"]*"[^>]*>([\s\S]*?)<\/\1>/gi, (match, tag, content) => {
@@ -637,7 +660,10 @@ const editApp = createApp({
html = html.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]");
// 2.5 处理段落 [p]
html = html.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "[p]$1[/p]");
// 优先处理空段落 <p><br></p>,将其替换为单个换行符,避免后续双重换行
html = html.replace(/<p[^>]*>\s*<br\s*\/?>\s*<\/p>/gi, "\n");
// 处理普通段落结束符
html = html.replace(/<\/p>/gi, "\n");
// 3. 处理加粗 [b] (对应 strong, b)
html = html.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "[b]$2[/b]");
@@ -667,7 +693,7 @@ const editApp = createApp({
// 匹配数字+单位 (px或%)
const wMatch = styleMatch[1].match(/width:\s*([\d.]+(?:px|%)?)/i);
const hMatch = styleMatch[1].match(/height:\s*([\d.]+(?:px|%)?)/i);
if (wMatch) {
// 如果是百分比,直接保留字符串;如果是纯数字默认视为 px如果是 px 去掉单位
let val = wMatch[1];
@@ -703,14 +729,16 @@ const editApp = createApp({
// 处理 data-href包裹在 [url] 中
const hrefMatch = imgTag.match(/data-href="([^"]+)"/i);
if (hrefMatch && hrefMatch[1]) {
result = `[url=${hrefMatch[1]}]${result}[/url]`;
}
if (hrefMatch && hrefMatch[1]) result = `[url=${hrefMatch[1]}]${result}[/url]`;
return result;
});
// 5. 处理视频 [attach]
html = html.replace(/<div[^>]*data-w-e-type="video"[^>]*>([\s\S]*?)<\/div>/gi, (match, content) => {
return content.trim(); // 去掉包裹视频的 div并去除首尾空白防止产生额外换行
});
html = html.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, (videoTag) => {
let aid = "";
const dataAid = videoTag.match(/data-aid="(\d+)"/i);
@@ -742,7 +770,7 @@ const editApp = createApp({
// 7. 换行处理
html = html.replace(/<br\s*\/?>/gi, "\n");
// html = html.replace(/<\/(p|div)>/gi, "\n"); // 块级元素结束视为换行
html = html.replace(/<\/(div)>/gi, "\n"); // 块级元素结束视为换行
// 8. 清理剩余标签和解码
@@ -781,8 +809,6 @@ const editApp = createApp({
});
});
console.log("提取完成的图片列表:", images);
return images;
};
@@ -817,7 +843,6 @@ const editApp = createApp({
});
});
console.log("提取完成的视频列表:", result);
return result;
};
@@ -842,7 +867,6 @@ const editApp = createApp({
},
onUploadProgress: (e) => {
progress.value = Math.round((e.loaded / e.total) * 100);
console.log("progress.value", progress.value);
},
withCredentials: true,
})