// 简单版本的论坛编辑器,确保图片插入功能正常 const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; const { headTop } = await import(withVer("../component/head-top/head-top.js")); const { createEditor, createToolbar, SlateTransforms, Boot, SlateEditor } = window.wangEditor; class MyButtonMenu { // JS 语法 constructor() { this.title = "居中"; // 自定义菜单标题 this.tag = "button"; } // 获取菜单执行时的 value ,用不到则返回空 字符串或 false getValue(editor) { return " hello "; } // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false isActive(editor) { if (editor.getFragment()?.[0]?.textAlign == "center") return true; return false; } // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false isDisabled(editor) { return false; } // 点击菜单时触发的函数 exec(editor, value) { let align = this.isActive(editor) ? "left" : "center"; SlateTransforms.setNodes(editor, { textAlign: align, }); } } const editApp = createApp({ setup() { let titleLength = ref(200); let uniqid = ref(""); const valueA = ref(null); let valueUrl = ""; onMounted(() => { const params = getUrlParams(); uniqid.value = params.uniqid || ""; getUserInfoWin(); checkWConfig(); cUpload(); valueUrl = valueA.value.innerText; if (location.hostname == "127.0.0.1") { realname.value = 1; userInfoWin.value = { uin: 1234567890, uid: 1234567890, realname: "测试用户", }; isLogin.value = true; } }); let imageLength = 10; let videoLength = 5; const checkWConfig = () => { const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {}; if (wConfig.time) { const time = new Date(wConfig.time); const now = new Date(); if (now - time > 24 * 60 * 60 * 1000) getWConfig(); else { const config = wConfig.config || {}; titleLength.value = config.max_topic_title_length; imageLength = config.topic_image_count || 0; videoLength = config.topic_video_count || 0; } } else getWConfig(); }; const getWConfig = () => { ajaxGet("/v2/api/config/website").then((res) => { if (res.code == 200) { let data = res["data"] || {}; const config = data.config || {}; titleLength.value = config.max_topic_title_length; imageLength = config.topic_image_count || 0; videoLength = config.topic_video_count || 0; data.time = new Date().toISOString(); localStorage.setItem("wConfig", JSON.stringify(data)); } }); }; let isLogin = ref(false); let realname = ref(0); // 是否已经实名 let userInfoWin = ref({}); let permissions = ref([]); const getUserInfoWin = () => { const checkUser = () => { const user = window.userInfoWin; if (!user) return; document.removeEventListener("getUser", checkUser); realname.value = user.realname; userInfoWin.value = user; if (user?.uin > 0 || user?.uid > 0) isLogin.value = true; permissions.value = user?.authority || []; }; document.addEventListener("getUser", checkUser); }; const openAttest = () => { const handleAttestClose = () => { document.removeEventListener("closeAttest", handleAttestClose); realname.value = window.userInfoWin?.realname || 0; }; // 启动认证流程时添加监听 document.addEventListener("closeAttest", handleAttestClose); loadAttest(2); }; // 跳转登录 const goLogin = () => { if (typeof window === "undefined") return; if (window["userInfoWin"] && Object.keys(window["userInfoWin"]).length !== 0) { if (window["userInfoWin"]["uid"]) isLogin.value = true; else ajax_login(); } else ajax_login(); }; let uConfigData = {}; const cUpload = () => { ajaxGet(`/v2/api/config/upload?type=topic`).then((res) => { const data = res.data; uConfigData = data; init(); }); }; let info = ref({}); let tagList = ref([]); let token = ref(""); const init = () => { ajax("/v2/api/forum/postPublishInit", { uniqid: uniqid.value, }) .then((res) => { const data = res.data; if (res.code != 200) { creationAlertBox("error", res.message || "操作失败"); return; } const infoTarget = data.info || {}; // console.log("content", infoTarget.content); if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoTarget.attachments); // console.log("content", infoTarget.content); info.value = infoTarget; token.value = data.token; initEditor(); }) .catch((err) => { console.log("err", err); }); }; let editor = null; let toolbarRef = ref(null); // 自定义转换视频 function customParseVideoSrc(src) { // console.log("customParseVideoSrc", "src:", src); // if (src.includes(".bilibili.com")) { // // 转换 bilibili url 为 iframe (仅作为示例,不保证代码正确和完整) // const arr = location.pathname.split("/"); // const vid = arr[arr.length - 1]; // return ``; // } return src; } const initEditor = () => { let infoTarget = info.value || {}; const editorConfig = { placeholder: "输入正文", enabledMenus: [], MENU_CONF: { ["emotion"]: { emotions: optionEmoji.value, }, ["insertImage"]: { onInsertedImage(imageNode) { const { src, alt, url, href } = imageNode; }, async parseImageSrc(src) { // 如果图片链接中已经包含了 ?aid= ,则说明是本站图片,直接返回,无需处理 if (src.includes("?aid=")) return src; // 对于不含 ?aid= 的外部图片,执行上传转换 if (!uConfigData || !uConfigData.url) return src; try { const formData = new FormData(); formData.append("uploadType", "url"); formData.append("url", src); if (uConfigData.params && uConfigData.params.data) { formData.append("data", uConfigData.params.data); } const res = await ajax(uConfigData.url, formData); if (res.code == 200 && res.data) return `${res.data.url}?aid=${res.data.aid}`; else { creationAlertBox("error", res.message || "操作失败"); return ""; } } catch (e) { console.error("Transform network image failed", e); return ""; } }, checkImage: (src, alt, url) => { if (src.indexOf("http") !== 0) { return "图片网址必须以 http/https 开头"; } return true; }, // 也支持 async 函数 }, ["uploadImage"]: { server: uConfigData.url, fieldName: uConfigData.requestName, maxFileSize: maxSize, // 1M maxNumberOfFiles: imageLength, allowedFileTypes: ["image/png", "image/jpeg", "image/jpg"], // .png, .jpg, .jpeg meta: { ...uConfigData.params }, metaWithUrl: false, headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers }, withCredentials: true, timeout: 60 * 1000, // 15 秒 async customUpload(file, insertFn) { try { const img = await uploading(file, file.name, "image"); console.log("img", img); insertFn(`${img.url}?aid=${img.aid}`); } catch (err) { console.error("上传出错:", err); } }, }, ["insertVideo"]: { onInsertedVideo(videoNode) { if (videoNode == null) return; const { src } = videoNode; // console.log("inserted video", src); }, parseVideoSrc: customParseVideoSrc, // 也支持 async 函数 }, ["uploadVideo"]: { server: uConfigData.url, fieldName: uConfigData.requestName, maxFileSize: maxSize, // 1M maxNumberOfFiles: videoLength, allowedFileTypes: ["video/flv", "video/mkv", "video/avi", "video/rm", "video/rmvb", "video/mpeg", "video/mpg", "video/ogg", "video/ogv", "video/mov", "video/wmv", "video/mp4", "video/webm", "video/m4v"], meta: { ...uConfigData.params }, metaWithUrl: false, headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers }, withCredentials: true, timeout: 60 * 1000, // 15 秒 async customUpload(file, insertFn) { try { const videoUploadRes = await uploading(file, file.name, "video"); progress.value = 0; const coverFile = await getVideoFirstFrame(file); // console.log("第一帧提取成功", coverFile); const coverUploadRes = await uploading(coverFile, coverFile.name, "image"); // console.log("封面上传成功", coverUploadRes); insertFn(`${videoUploadRes.url}?aid=${videoUploadRes.aid}`, `${coverUploadRes.url}?aid=${coverUploadRes.aid}`); } catch (err) { console.error("上传出错:", err); progress.value = 0; } }, }, }, onChange(editor) { const html = editor.getHtml(); console.log("edior content", html); }, hoverbarKeys: { text: { menuKeys: [] }, video: { menuKeys: [] } }, }; editor = createEditor({ selector: "#editor-container", html: infoTarget.content, // html: "", config: editorConfig, mode: "default", }); const toolbarConfig = { toolbarKeys: [ "header1", { key: "group-image", title: "图片", menuKeys: ["insertImage", "uploadImage"], }, "uploadVideo", "emotion", "insertLink", "bold", ], }; const menu1Conf = { key: "customCenter", // 定义 menu key :要保证唯一、不重复(重要) factory() { return new MyButtonMenu(); // 把 `YourMenuClass` 替换为你菜单的 class }, }; Boot.registerMenu(menu1Conf); toolbarConfig.insertKeys = { index: 7, // 插入的位置,基于当前的 toolbarKeys keys: ["customCenter"], }; const toolbar = createToolbar({ editor, selector: "#toolbar-container", config: toolbarConfig, mode: "default", }); nextTick(() => { const h1 = toolbarRef.value.querySelector('[data-menu-key="header1"]'); const h1Item = h1.parentElement; h1Item.classList.add("toolbar-item", "flexacenter"); h1.innerHTML = `段落标题 段落标题`; const image = toolbarRef.value.querySelector('[data-menu-key="group-image"]'); const imageItem = image.parentElement; imageItem.classList.add("toolbar-item", "flexacenter"); image.innerHTML = `图片 图片`; const video = toolbarRef.value.querySelector('[data-menu-key="uploadVideo"]'); const videoItem = video.parentElement; videoItem.classList.add("toolbar-item", "flexacenter"); video.innerHTML = `视频 视频`; const emotion = toolbarRef.value.querySelector('[data-menu-key="emotion"]'); const emotionItem = emotion.parentElement; emotionItem.classList.add("toolbar-item", "flexacenter"); emotion.innerHTML = `表情 表情`; const link = toolbarRef.value.querySelector('[data-menu-key="insertLink"]'); const linkItem = link.parentElement; linkItem.classList.add("toolbar-item", "flexacenter"); link.innerHTML = `链接 链接`; const bold = toolbarRef.value.querySelector('[data-menu-key="bold"]'); const boldItem = bold.parentElement; boldItem.classList.add("toolbar-item", "flexacenter"); bold.innerHTML = `加粗 加粗`; const customCenter = toolbarRef.value.querySelector('[data-menu-key="customCenter"]'); const customCenterItem = customCenter.parentElement; customCenterItem.classList.add("toolbar-item", "flexacenter"); customCenter.innerHTML = `居中 居中`; }); }; const restoreHtml = (formattedText, attachments) => { const imageList = attachments?.images || []; const filesList = attachments?.files || []; const videosList = attachments?.videos || []; let html = formattedText || ""; // 0.5 [p] ->

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

$1

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

"); html = html.replace(/
/g, "
"); html = html.replace(/<\/div>
/g, "
"); html = html.replace(/

/g, "

"); html = html.replace(/<\/p>
/g, "

"); // 1. [section] -> h1 html = html.replace(/\[section\]([\s\S]*?)\[\/section\]/gi, "

$1

"); // 2. [b] -> strong html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "$1"); // 3. [align=center] // 特殊处理嵌套在 h1 中的居中 html = html.replace(/

\s*\[align=center\]([\s\S]*?)\[\/align\]\s*<\/h1>/gi, '

$1

'); // 普通居中 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) => { console.log("processImg", aid, width, height, href); const image = imageList.find((img) => String(img.aid) === String(aid)); if (!image) return ""; // 移除已使用的图片 const index = imageList.findIndex((img) => String(img.aid) === String(aid)); if (index > -1) imageList.splice(index, 1); let style = ""; const formatStyleVal = (val) => { if (!val) return null; if (String(val).endsWith("%")) return val; const num = Number(val); return num > 0 ? `${num}px` : null; }; 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}"` : ""; return ``; }; // 4. [url] + [img] 组合 (带链接的图片) // 匹配 [url=...][img...]...[/img][/url] html = html.replace(/\[url=([^\]]+)\]\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\](\d+)\[\/img\]\[\/url\]/gi, (match, href, w, h, aid) => { const imgTag = processImg(aid, w, h, href); return imgTag || match; }); // 5. 单独 [img] html = html.replace(/\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\](\d+)\[\/img\]/gi, (match, w, h, aid) => { const imgTag = processImg(aid, w, h, null); return imgTag || match; }); html = html.replace(/\[url=([^\]]+)\]\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\]([^\]]+)\[\/img\]\[\/url\]/gi, (match, href, w, h, inner) => { if (/^\d+$/.test(inner)) return match; const formatStyleVal = (val) => { if (!val) return null; if (String(val).endsWith("%")) return val; const num = Number(val); return num > 0 ? `${num}px` : null; }; const wStyle = formatStyleVal(w); const hStyle = formatStyleVal(h); let styleParts = []; if (wStyle) styleParts.push(`width: ${wStyle}`); if (hStyle) styleParts.push(`height: ${hStyle}`); const styleAttr = styleParts.length > 0 ? ` style="${styleParts.join("; ")};"` : ""; const dataHref = href ? ` data-href="${href}"` : ""; return ``; }); html = html.replace(/\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\]([^\]]+)\[\/img\]/gi, (match, w, h, inner) => { if (/^\d+$/.test(inner)) return match; const formatStyleVal = (val) => { if (!val) return null; if (String(val).endsWith("%")) return val; const num = Number(val); return num > 0 ? `${num}px` : null; }; const wStyle = formatStyleVal(w); const hStyle = formatStyleVal(h); let styleParts = []; if (wStyle) styleParts.push(`width: ${wStyle}`); if (hStyle) styleParts.push(`height: ${hStyle}`); const styleAttr = styleParts.length > 0 ? ` style="${styleParts.join("; ")};"` : ""; return ``; }); // 6. [attachimg] (兼容旧格式) html = html.replace(/\[attachimg\](\d+)\[\/attachimg\]/gi, (match, aid) => { const imgTag = processImg(aid, 0, 0, null); return imgTag || match; }); // 7. [attach] (图片、视频或附件) html = html.replace(/\[attach\](\d+)\[\/attach\]/gi, (match, aid) => { // 尝试匹配视频 const video = videosList.find((v) => String(v.aid) === String(aid)); if (video) { const index = videosList.findIndex((v) => String(v.aid) === String(aid)); if (index > -1) videosList.splice(index, 1); return `

`; } // 尝试匹配图片 const image = imageList.find((img) => String(img.aid) === String(aid)); if (image) { const index = imageList.findIndex((img) => String(img.aid) === String(aid)); if (index > -1) imageList.splice(index, 1); return `
`; } // 尝试匹配文件 const file = filesList.find((f) => String(f.aid) === String(aid)); if (file) { return `附件: ${file.name || "Download"}`; } return match; }); // 8. [url] (普通链接) html = html.replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, '$2'); // 9. 剩余资源追加 imageList.forEach((element) => (html += `
`)); videosList.forEach((element) => (html += `
`)); // 10. 最终 HTML 结构包裹 const __c = document.createElement("div"); __c.innerHTML = html; // 确保所有顶层元素都是块级元素 (Slate/WangEditor 要求) const blockTags = ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "UL", "OL", "PRE", "TABLE", "FIGURE", "HR"]; const newContainer = document.createElement("div"); let currentInlineNodes = []; let lastWasBlock = false; 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) { 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; } } } else { // 过滤掉块级元素之间或开头的纯空白文本节点,避免产生空的 P 标签 if (node.nodeType === 3 && !node.textContent.trim()) { if (currentInlineNodes.length > 0) { currentInlineNodes.push(node); } } else { currentInlineNodes.push(node); } lastWasBlock = false; } }); flushInlines(); html = newContainer.innerHTML; console.log("初始化显示的html", html); return html; }; onMounted(() => { // setTimeout(() => focusLastNode(), 1000); // document.addEventListener("keydown", handleUndoKeydown); }); const editorRef = ref(null); let loading = ref(false); const maxSize = 20 * 1024 * 1024; // 20MB const cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1); const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵‍💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]); // 提交 const submit = (status) => { if (location.hostname == "127.0.0.1") status = 0; if (realname.value == 0 && userInfoWin.value?.uin > 0) { openAttest(); return; } if (!isLogin.value) { goLogin(); return; } const infoTarget = { ...info.value } || {}; let content = editor.getHtml(); const obj = formatContent(content); content = obj.content; const images = extractImages(editorRef.value); // const images = obj.images; const videos = extractVideos(editorRef.value); infoTarget.attachments = infoTarget.attachments || {}; infoTarget.attachments.images = images; infoTarget.attachments.videos = videos; info.value["attachments"] = info.value["attachments"] || {}; info.value["attachments"]["images"] = images; info.value["attachments"]["videos"] = videos; const data = { ...infoTarget, content, }; ajax("/v2/api/forum/postPublishTopic", { info: data, token: token.value, status, }).then((res) => { const data = res.data; if (res.code != 200) { creationAlertBox("error", res.message); return; } creationAlertBox("success", res.message || "操作成功"); const back = () => { if (status == 1) redirectToExternalWebsite("/details/" + data.uniqid, "_self"); else redirectToExternalWebsite("/", "_self"); }; setTimeout(() => back(), 1500); }); }; const formatContent = (html) => { let images = []; 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) => { if (tag.toLowerCase() === "h1") { // h1 特殊处理:保留标签,内容居中,供后续转 [section] return `

[align=center]${content}[/align]

`; } return `[align=center]${content}[/align]`; }); // 2. 处理章节 [section] (对应 h1) html = html.replace(/]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]"); // 2.5 处理段落 [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]"); // 4. 处理图片 [img] html = html.replace(/]*>/gi, (imgTag) => { let aid = ""; const srcMatch = imgTag.match(/src="([^"]+)"/i); if (!srcMatch) return ""; const srcUrl = srcMatch[1]; const cleanUrl = srcUrl.split("?")[0]; const aidMatch = srcUrl.match(/[?&]aid=(\d+)/); if (aidMatch) aid = aidMatch[1]; let w = 0, h = 0; const styleMatch = imgTag.match(/style="([^"]+)"/i); if (styleMatch) { const wMatch = styleMatch[1].match(/width:\s*([\d.]+(?:px|%)?)/i); const hMatch = styleMatch[1].match(/height:\s*([\d.]+(?:px|%)?)/i); if (wMatch) { let val = wMatch[1]; if (val.endsWith("%")) w = val; else w = parseFloat(val); } if (hMatch) { let val = hMatch[1]; if (val.endsWith("%")) h = val; else h = parseFloat(val); } } if (!w) { const wAttr = imgTag.match(/\swidth="(\d+)"/i); if (wAttr) w = Number(wAttr[1]); } if (!h) { const hAttr = imgTag.match(/\sheight="(\d+)"/i); if (hAttr) h = Number(hAttr[1]); } const inner = aid ? aid : cleanUrl; let result = ""; if (w || h) { const formatVal = (val) => { if (typeof val === "string" && val.endsWith("%")) return val; return val ? parseFloat(Number(val).toFixed(2)) : 0; }; result = `[img=${formatVal(w)},${formatVal(h)}]${inner}[/img]`; if (aid) { images.push({ url: cleanUrl, aid: Number(aid) }); } else { images.push({ url: cleanUrl }); } } else { result = `[img]${inner}[/img]`; if (aid) { images.push({ url: cleanUrl, aid: Number(aid) }); } else { images.push({ url: cleanUrl }); } } const hrefMatch = imgTag.match(/data-href="([^"]+)"/i); 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); if (dataAid) aid = dataAid[1]; if (!aid) { const srcMatch = videoTag.match(/src="([^"]+)"/i); if (srcMatch) { const aidMatch = srcMatch[1].match(/[?&]aid=(\d+)/); if (aidMatch) aid = aidMatch[1]; } } return aid ? `[attach]${aid}[/attach]` : ""; }); // 6. 处理链接 [url] 和 附件下载 [attach] html = html.replace(/]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (match, href, content) => { // 尝试提取 aid const aidMatch = href.match(/[?&]aid=(\d+)/); // 如果是下载链接(包含 download 属性 或 明确是附件)且有 aid,转为 [attach] if (match.includes("download=") && aidMatch) { return `[attach]${aidMatch[1]}[/attach]`; } // 普通链接 return `[url=${href}]${content}[/url]`; }); // 7. 换行处理 html = html.replace(//gi, "\n"); html = html.replace(/<\/(div)>/gi, "\n"); // 块级元素结束视为换行 // 8. 清理剩余标签和解码 html = html.replace(/<[^>]+>/g, ""); // 移除所有剩余标签 // 简单的 HTML 实体解码 const entities = { " ": " ", "<": "<", ">": ">", "&": "&", """: '"', "'": "'", }; html = html.replace(/&[a-z]+;/gi, (match) => entities[match] || match); return { content: html.trim(), images, }; }; const extractImages = (dom) => { const images = []; // 直接查找页面中所有带 data-aid 的 img 标签 const imgElements = dom.querySelectorAll("img"); imgElements.forEach((imgEl) => { let url = imgEl.getAttribute("src")?.trim() || ""; const urlObj = new URL(url); const aid = urlObj.searchParams.get("aid"); const queryIndex = url.indexOf("?"); const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url; if (Number(aid)) { images.push({ url: cleanUrl, aid: Number(aid), }); } }); return images; }; const extractVideos = (dom) => { // 1. 查找页面中所有