From 57bf0ed3eb505cb112952975c827f1e0ea6432a5 Mon Sep 17 00:00:00 2001 From: A1300399510 <1300399510@qq.com> Date: Sun, 30 Nov 2025 23:54:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20=E4=BC=98=E5=8C=96=E5=AF=8C?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E7=BC=96=E8=BE=91=E5=99=A8=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E8=BD=AC=E6=8D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进 HTML 到 Markdown 的转换规则,处理块级元素和行内元素的嵌套关系 - 优化图片和视频标签的解析逻辑,确保附件正确提取 - 修复换行符处理问题,避免产生多余空行 - 增强居中文本和标题的格式转换准确性 - 清理调试日志和冗余代码 --- css/details.css | 21 +++++--- css/details.less | 59 ++++++++++++++++---- css/index.css | 8 ++- css/index.less | 38 +++++++++++-- js/edit.js | 136 ++++++++++++++++++++++++++++------------------- 5 files changed, 182 insertions(+), 80 deletions(-) diff --git a/css/details.css b/css/details.css index df13da2..3612697 100644 --- a/css/details.css +++ b/css/details.css @@ -160,14 +160,21 @@ line-height: 26px; margin-bottom: 66px; } +#details .matter .matter-left .html * { + background: transparent !important; + color: #555555 !important; +} #details .matter .matter-left .html a { text-decoration: underline; - color: #04b0d5; + color: #04b0d5 !important; +} +#details .matter .matter-left .html a * { + color: #04b0d5 !important; } #details .matter .matter-left .html .blue { font-size: 15px; line-height: 26px; - color: #026277; + color: #026277 !important; margin: 0 4px; text-decoration: none; } @@ -176,19 +183,17 @@ display: inline-block; } #details .matter .matter-left .html video { - margin: 0 auto; + margin: 0 auto 5px; + height: 300px; + display: block; } #details .matter .matter-left .html h1 { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; - color: #000000; + color: #000000 !important; font-size: 18px; line-height: 30px; } -#details .matter .matter-left .html tr, -#details .matter .matter-left .html td { - background: transparent; -} #details .matter .matter-left .last-time { color: #aaaaaa; font-size: 13px; diff --git a/css/details.less b/css/details.less index d00e8cb..066357d 100644 --- a/css/details.less +++ b/css/details.less @@ -36,6 +36,7 @@ .content { flex-direction: column; + .name { font-size: 14px; color: #333333; @@ -55,6 +56,7 @@ .operate { position: relative; + .view { .icon { width: 13px; @@ -185,15 +187,25 @@ line-height: 26px; margin-bottom: 66px; + * { + background: transparent !important; + color: #555555 !important; + } + a { text-decoration: underline; - color: #04b0d5; + color: #04b0d5 !important; + + * { + color: #04b0d5 !important; + + } } .blue { font-size: 15px; line-height: 26px; - color: #026277; + color: #026277 !important; margin: 0 4px; text-decoration: none; } @@ -204,21 +216,20 @@ } video { - margin: 0 auto; + margin: 0 auto 5px; + height: 300px; + display: block; } h1 { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; - color: #000000; + color: #000000 !important; font-size: 18px; line-height: 30px; } - tr, - td { - background: transparent; - } + } .last-time { @@ -244,6 +255,7 @@ height: 20px; margin-right: 6px; } + font-size: 14px; color: #333333; cursor: pointer; @@ -254,6 +266,7 @@ &.share { position: relative; + &:hover { .share-box { display: flex; @@ -353,6 +366,7 @@ padding-top: 20px; padding-bottom: 12px; border-bottom: 1px solid #ebebeb; + .text { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; @@ -494,6 +508,7 @@ .author-info { color: #7f7f7f; font-size: 13px; + .amount { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; @@ -508,6 +523,7 @@ width: calc(100% - 16px); padding-bottom: 22px; margin-left: 16px; + .medal-title { font-size: 14px; color: #7f7f7f; @@ -551,6 +567,7 @@ &:hover { color: #000000; } + .dot { width: 6px; height: 6px; @@ -608,6 +625,7 @@ font-weight: 400; color: #7f7f7f; font-size: 14px; + .sum { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; @@ -682,6 +700,7 @@ .coins-total { color: #7f7f7f; font-size: 14px; + .sum { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; @@ -713,6 +732,7 @@ .user { color: #555555; font-size: 13px; + .avatar { width: 32px; height: 32px; @@ -724,6 +744,7 @@ .amount { color: #000000; font-size: 16px; + .text { font-size: 13px; margin-left: 2px; @@ -883,8 +904,7 @@ cursor: pointer; } -.answer-discuss .input-box .bottom .operate .item { -} +.answer-discuss .input-box .bottom .operate .item {} .answer-discuss .input-box .bottom .operate .item .emoji-box { width: 582px; @@ -1421,29 +1441,35 @@ background-color: rgba(0, 0, 0, 0.5); z-index: 12; } + .answer-discuss .edit-comment .box { width: 650px; border-radius: 10px; background: #fff; padding: 20px 15px; } + .answer-discuss .edit-comment .box .text { font-size: 18px; font-weight: 650; margin-bottom: 15px; color: #000; } + .answer-discuss .edit-comment .box .input-box { margin-right: 0; padding-top: 10px; } + .answer-discuss .edit-comment .box .input-box .bottom { // border-top: 1px solid #ebebeb; } + .answer-discuss .edit-comment .box .btn-list { padding: 15px 0; justify-content: flex-end; } + .answer-discuss .edit-comment .box .btn-list .btn { font-size: 14px; color: #333; @@ -1457,6 +1483,7 @@ border: 1px solid #ebebeb; margin-left: 20px; } + .answer-discuss .edit-comment .box .btn-list .btn.send { background-color: #fddf6d; border: 1px solid #fddf6d; @@ -1469,6 +1496,7 @@ width: 100vw; height: 100vh; } + .answer-discuss .emoji-box-mask { position: fixed; top: 0; @@ -1483,6 +1511,7 @@ @media screen and (max-width: 850px) { #details { padding: 10px 10px 0; + .head-top { display: none; } @@ -1494,6 +1523,7 @@ .matter { .matter-left { margin-right: 0 !important; + .action-bar { margin-right: 0 !important; justify-content: space-around; @@ -1508,10 +1538,13 @@ .related-head { padding-left: 14px; } + .list { padding: 14px; + .item { width: 100% !important; + &:not(:last-child) { margin-bottom: 7px; } @@ -1523,10 +1556,12 @@ } } } + .sidebar-box { display: none; } } + .answer-discuss { padding: 15px; @@ -1594,6 +1629,7 @@ .comments-item { .comments-header { font-size: 12px; + .comments-title { height: 14px !important; } @@ -1610,6 +1646,7 @@ .comments-box { .comments-item { .comments-header { + .comment-icon, .like-box { margin-left: 15px; @@ -1619,4 +1656,4 @@ } } } -} +} \ No newline at end of file diff --git a/css/index.css b/css/index.css index ceab5ae..06a761d 100644 --- a/css/index.css +++ b/css/index.css @@ -398,12 +398,16 @@ } #appIndex .header-content-box .header-content-right .offer-box { width: 240px; - height: 214px; background-color: #fff; border: 1px solid #e9eef2; border-radius: 10px; - padding: 17px 10px; overflow: hidden; + height: 64px; + padding: 15px 10px; +} +#appIndex .header-content-box .header-content-right .offer-box.big { + height: 214px; + padding: 17px 10px; } #appIndex .header-content-box .header-content-right .offer-box.small { height: 64px; diff --git a/css/index.less b/css/index.less index 195e379..b83e9fc 100644 --- a/css/index.less +++ b/css/index.less @@ -18,9 +18,11 @@ &:not(:last-child) { margin-right: 12px; } + a { display: block; } + img { width: 468px; height: 60px; @@ -37,6 +39,7 @@ border-radius: 20px 20px 20px 0; margin-top: 0; margin-bottom: 10px; + .icon { width: 15px; height: 12px; @@ -91,6 +94,7 @@ .people { position: relative; justify-content: space-between; + &::after { content: ""; position: absolute; @@ -119,6 +123,7 @@ width: 26px; height: 26px; border-radius: 50%; + .img { width: 26px; height: 26px; @@ -128,12 +133,15 @@ &:nth-child(4) { margin-right: -9px; } + &:nth-child(3) { margin-right: -9px; } + &:nth-child(2) { margin-right: -7px; } + &:nth-child(2) { margin-right: -5px; } @@ -145,6 +153,7 @@ .topic-list { .item { cursor: pointer; + &:not(:last-child) { margin-bottom: 1px; } @@ -222,8 +231,10 @@ border-radius: 10px; padding-left: 12px; margin-top: 12px; + .adv { margin-right: 26px; + .adv-icon { width: 295px; height: 118px; @@ -279,6 +290,7 @@ .header-content-right { width: 240px; + .post-entrance { background-color: #fff; border: 1px solid #e9eef2; @@ -357,9 +369,11 @@ height: 140px; margin-bottom: 10px; display: block; + a { display: block; } + img { width: 240px; height: 140px; @@ -394,12 +408,15 @@ &:nth-child(1) { border-top-left-radius: 8px; } + &:nth-child(2) { border-top-right-radius: 8px; } + &:nth-child(3) { border-bottom-left-radius: 8px; } + &:nth-child(4) { border-bottom-right-radius: 8px; } @@ -429,6 +446,7 @@ line-height: 22px; position: relative; + .title { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; @@ -455,6 +473,7 @@ padding: 20px; box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.168627450980392); flex-direction: column; + .QRcode { width: 100px; height: 100px; @@ -472,13 +491,21 @@ .offer-box { width: 240px; - height: 214px; + // height: 214px; background-color: #fff; border: 1px solid #e9eef2; border-radius: 10px; - padding: 17px 10px; + // padding: 17px 10px; overflow: hidden; + height: 64px; + padding: 15px 10px; + + &.big { + height: 214px; + padding: 17px 10px; + } + &.small { height: 64px; padding: 15px 10px; @@ -521,6 +548,7 @@ .admission { margin-bottom: 30px; + .admission-header { margin-bottom: 24px; display: inline-flex; @@ -543,6 +571,7 @@ .admission-list { flex-wrap: wrap; + .admission-item { width: 291px; height: 103px; @@ -602,6 +631,7 @@ &:not(:last-child) { margin-bottom: 10px; } + .item { font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-weight: 400; @@ -622,6 +652,7 @@ } } } + .item-box { margin-bottom: 12px; } @@ -641,6 +672,7 @@ height: 220px; margin-bottom: 12px; cursor: pointer; + img { width: 291px; height: 220px; @@ -669,4 +701,4 @@ } } } -} +} \ No newline at end of file diff --git a/js/edit.js b/js/edit.js index 922a87f..45d11f8 100644 --- a/js/edit.js +++ b/js/edit.js @@ -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 = `

图片描述

- // - //


`; - - 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] ->

- html = html.replace(/\[p\]([\s\S]*?)\[\/p\]/gi, "

$1

"); + // html = html.replace(/\[p\]([\s\S]*?)\[\/p\]/gi, "

$1

"); // 0. 基础清理:换行符转
,div 转 p (保持原有逻辑) - html = html.replace(/\n/g, "
"); + html = html.replace(/(\r\n|\n|\r)/g, "
"); html = html.replace(/)/gi, "/g, "

"); html = html.replace(/
/g, "
"); @@ -432,11 +417,15 @@ const editApp = createApp({ // 普通居中 html = html.replace(/\[align=center\]([\s\S]*?)\[\/align\]/gi, '

$1

'); + // 移除 align 后紧跟的换行符(因为 align 转换为了块级元素 p/h1,其后的换行符通常是多余的) + html = html.replace(/(

.*?<\/p>)\s*
/gi, "$1"); + html = html.replace(/(

.*?<\/h1>)\s*
/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 = '
'; + 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 ""; + //


转换为单个换行符 + html = html.replace(/


<\/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(/]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]"); // 2.5 处理段落 [p] - html = html.replace(/]*>([\s\S]*?)<\/p>/gi, "[p]$1[/p]"); + // 优先处理空段落


,将其替换为单个换行符,避免后续双重换行 + html = html.replace(/]*>\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(/]*data-w-e-type="video"[^>]*>([\s\S]*?)<\/div>/gi, (match, content) => { + return content.trim(); // 去掉包裹视频的 div,并去除首尾空白,防止产生额外换行 + }); + html = html.replace(/]*>[\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(//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, })