// 简单版本的论坛编辑器,确保图片插入功能正常 const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; import { headTop } from "../component/head-top/head-top.js"; const { createEditor, createToolbar } = window.wangEditor; console.log("createEditor", createEditor); const editApp = createApp({ setup() { let titleLength = ref(200); let uniqid = ref(""); onMounted(() => { const params = getUrlParams(); uniqid.value = params.uniqid || ""; getUserInfoWin(); checkWConfig(); cUpload(); // 添加selectionchange事件监听,当鼠标选中区域内容时更新lastSelection document.addEventListener("selectionchange", handleSelectionChange); }); // 组件卸载时移除事件监听 onUnmounted(() => { document.removeEventListener("selectionchange", handleSelectionChange); }); let imageLength = 10; let videoLength = 5; const checkWConfig = () => { const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {}; console.log("wConfig", 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; console.log("uConfigData", uConfigData); init(); }); }; let info = ref({}); let tagList = ref([]); let token = ref(""); let infoImages = []; 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 || {}; if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoTarget.attachments); info.value = infoTarget; token.value = data.token; // nextTick(() => { // judgeIsEmpty(); // }); initEditor(); }) .catch((err) => { console.log("err", err); }); }; let editor = null; let toolbarRef = ref(null); const initEditor = () => { let infoTarget = info.value || {}; console.log("infoTarget", infoTarget); // 转换图片链接 async function customCheckImageFn(src, alt, url) { // JS 语法 if (!src) { return; } // let config = uConfigData; // // 1. 构造 FormData(包含你的接口所需字段) // const formData = new FormData(); // formData.append(config.requestName, file); // 文件数据 // formData.append("name", file.name); // 文件名 // formData.append("type", "image"); // 文件名 // formData.append("data", config.params.data); // 文件名 await setTimeout(() => { console.log("1111"); return true; }, 2000); // setTimeout(() => { // return "图片网址必须以 http/https 开头"; // }, 2000); if (src.indexOf("http") !== 0) { // return "图片网址必须以 http/https 开头"; } // return true; // 返回值有三种选择: // 1. 返回 true ,说明检查通过,编辑器将正常插入图片 // 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串) // 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入。但不会提示任何信息 } // 【新增】判断节点的对齐方式 const getNodeAlign = (node) => { if (!node) return "left"; // 默认居左 // 获取节点的text-align样式(优先内联样式,再取CSS计算样式) const inlineAlign = node.style.textAlign; if (inlineAlign) return inlineAlign; const computedStyle = window.getComputedStyle(node); return computedStyle.textAlign || "left"; }; // 【新增】切换对齐方式(居中 ↔ 居左) const toggleAlign = () => { const editorInst = editor.value; if (!editorInst) return; // 禁用编辑器默认的居中命令 editorInst.off("clickToolbar", "justifyCenter"); // 获取当前选中的节点(优先段落/块级节点) const selectedNode = getSelectedNode(editorInst); const blockNode = DomEditor.getClosestBlock(selectedNode); // 获取块级节点(p/div等) if (!blockNode) return; // 判断当前对齐方式 const currentAlign = getNodeAlign(blockNode); // 切换对齐:居中 → 居左;其他 → 居中 const newAlign = currentAlign === "center" ? "left" : "center"; // 设置节点对齐样式 editorInst.restoreSelection(); // 恢复选区 blockNode.style.textAlign = newAlign; // 触发编辑器更新 editorInst.change(); editorInst.focus(); // 保持焦点 }; const editorConfig = { placeholder: "Type here...", enabledMenus: [], MENU_CONF: { ["emotion"]: { emotions: optionEmoji.value, }, ["insertImage"]: { onInsertedImage(imageNode) { console.log("imageNode", imageNode); // TS 语法 // onInsertedImage(imageNode) { // JS 语法 if (imageNode == null) return; const { src, alt, url, href } = imageNode; console.log("inserted image", src, alt, url, href); }, // checkImage: async (src, alt, url) => await customCheckImageFn(src, alt, url), // 也支持 async 函数 }, ["uploadImage"]: { server: uConfigData.url, // form-data fieldName ,默认值 'wangeditor-uploaded-image' fieldName: uConfigData.requestName, // 单个文件的最大体积限制,默认为 2M maxFileSize: maxSize, // 1M // 最多可上传几个文件,默认为 100 maxNumberOfFiles: imageLength, // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] allowedFileTypes: ["image/*", ".png", ".jpg", ".jpeg"], // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 meta: { ...uConfigData.params }, // 将 meta 拼接到 url 参数中,默认 false metaWithUrl: false, // 自定义增加 http header headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers }, // 跨域是否传递 cookie ,默认为 false withCredentials: true, // 超时时间,默认为 10 秒 async customUpload(file, insertFn) { try { let config = uConfigData; // 1. 构造 FormData(包含你的接口所需字段) const formData = new FormData(); formData.append(config.requestName, file); // 文件数据 formData.append("name", file.name); // 文件名 formData.append("type", "image"); // 文件名 formData.append("data", config.params.data); // 文件名 // uploading(file, file.name, "image").then((data) => { // insertFn(data.url); // 传入图片的可访问 URL // }); ajax(config.url, formData).then((res) => { const data = res.data; console.log("上传成功:", data); insertFn(data.url); // 传入图片的可访问 URL }); } catch (err) { console.error("上传出错:", err); } }, }, ["uploadVideo"]: { server: uConfigData.url, // form-data fieldName ,默认值 'wangeditor-uploaded-video' fieldName: uConfigData.requestName, // 单个文件的最大体积限制,默认为 10M maxFileSize: maxSize, // 1M // 最多可上传几个文件,默认为 5 maxNumberOfFiles: videoLength, // 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 [] allowedFileTypes: ["video/*"], // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 meta: { ...uConfigData.params }, // 将 meta 拼接到 url 参数中,默认 false metaWithUrl: false, // 自定义增加 http header headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers }, // 跨域是否传递 cookie ,默认为 false withCredentials: true, // 超时时间,默认为 30 秒 timeout: 15 * 1000, // 15 秒 // 视频不支持 base64 格式插入 async customUpload(file, insertFn) { try { const videoUploadRes = await uploading(file, file.name, "video"); const coverFile = await getVideoFirstFrame(file); console.log("第一帧提取成功", coverFile); // 步骤3:再上传第一帧封面(type 传 'cover',按后端要求调整) const coverUploadRes = await uploading(coverFile, coverFile.name, "image"); console.log("封面上传成功", coverUploadRes); insertFn(videoUploadRes.url, coverUploadRes.url); } catch (err) { console.error("上传出错:", err); } }, }, ["justifyCenter"]: { onClick: (editor) => { console.log("editor", editor); toggleAlign(); // 替换为自定义切换逻辑 }, // 【可选】自定义居中按钮的激活状态(选中时高亮) isActive: (editor) => { const selectedNode = getSelectedNode(editor); const blockNode = DomEditor.getClosestBlock(selectedNode); return blockNode && getNodeAlign(blockNode) === "center"; }, }, }, onChange(editor) { const html = editor.getHtml(); // console.log('"editor', editor); console.log("editor content", html); updateWHeadingStatus(); }, }; editor = createEditor({ selector: "#editor-container", html: "


555

", config: editorConfig, mode: "default", }); // const toolbar = DomEditor.getToolbar(editor) const toolbarConfig = { // toolbarKeys: ["bold", "italic", "list"], toolbarKeys: [ "header2", // 标题 { key: "group-image", title: "图片", menuKeys: ["insertImage", "uploadImage"], }, { key: "group-video", title: "视频", menuKeys: ["insertVideo", "uploadVideo"], }, // "insertVideo", "emotion", // 表情 // "uploadImage", // 插入图片 // "uploadVideo", // 插入视频 "insertLink", // 插入链接 "bold", // 粗体 "justifyCenter", ], }; const toolbar = createToolbar({ editor, selector: "#toolbar-container", config: toolbarConfig, mode: "default", }); console.log("editor.commands", editor); nextTick(() => { const h2 = toolbarRef.value.querySelector('[data-menu-key="header2"]'); const h2Item = h2.parentElement; h2Item.classList.add("toolbar-item", "flexacenter"); h2.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="group-video"]'); 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 justifyCenter = toolbarRef.value.querySelector('[data-menu-key="justifyCenter"]'); const justifyCenterItem = justifyCenter.parentElement; justifyCenterItem.classList.add("toolbar-item", "flexacenter"); justifyCenter.innerHTML = '居中 居中'; }); }; const restoreHtml = (formattedText, attachments) => { const imageList = attachments?.images || []; const filesList = attachments?.files || []; const videosList = attachments?.videos || []; let html = formattedText; // 1. 还原换行符为
标签 html = html.replace(/\n/g, "
"); // 2. 还原块级标签的换行标记 html = html.replace(/
/g, "
"); html = html.replace(/<\/div>
/g, "
"); // 3. 还原标签标记为span.blue html = html.replace(/\[tag\]([^[]+)\[\/tag\]/gi, '#$1 '); // 4. 还原粗体标记为h2标签 html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "

$1

"); // 5. 还原图片标记为img标签(使用提供的imageList) html = html.replace(/\[attachimg\](\d+)\[\/attachimg\]/gi, (match, aid) => { // 查找对应的图片信息 const image = imageList.find((img) => img.aid == aid); if (image) { imageList.splice(imageList.indexOf(image), 1); return `
`; } return match; // 未找到对应图片时保留原始标记 }); html = html.replace(/\[attach\](\d+)\[\/attach\]/gi, (match, aid) => { // 查找对应的图片信息 const image = imageList.find((img) => img.aid == aid); if (image) { imageList.splice(imageList.indexOf(image), 1); return `
`; } // 查找对应的视频信息 const video = videosList.find((v) => v.aid == aid); if (video) { console.log("video", video); videosList.splice(videosList.indexOf(video), 1); return ``; } return match; // 未找到对应图片时保留原始标记 }); // 6. 还原填充标签 html = html.replace(/([^<]+<\/span>)\s+/gi, '$1 '); // 7. 清理多余的
标签 // html = html.replace(/

/g, "
"); imageList.forEach((element) => { html += `
`; }); // video 不要预加载 videosList.forEach((element) => { html += `
`; }); return html; }; onMounted(() => { setTimeout(() => focusLastNode(), 1000); // document.addEventListener("keydown", handleUndoKeydown); }); const editorRef = ref(null); const focusLastNode = () => { return; const newRange = document.createRange(); const textNode = document.createTextNode(""); editorRef.value.appendChild(textNode); newRange.setStartAfter(textNode, 0); newRange.setEndAfter(textNode, 0); lastSelection = newRange; }; let lastSelection = null; let lastSelectionW = null; const isH1 = ref(false); const updateWHeadingStatus = () => { const wRoot = document.querySelector("#editor-container"); let node = null; try { const DomEditor = window.wangEditor && window.wangEditor.DomEditor; if (DomEditor && editor) node = DomEditor.getSelectionNode(editor); } catch (e) {} if (!node) { const sel = window.getSelection(); if (sel && sel.rangeCount) node = sel.getRangeAt(0).commonAncestorContainer; } let el = node && node.nodeType === 3 ? node.parentElement : node; while (el && el !== wRoot && el && el.nodeType === 1) { if (window.getComputedStyle(el).getPropertyValue("text-align") === "center") { console.log("居中"); const justifyCenter = toolbarRef.value.querySelector('[data-menu-key="justifyCenter"]'); if (justifyCenter) { const justifyCenterItem = justifyCenter.parentElement; justifyCenterItem.classList.add("active"); } break; } el = el.parentElement; } }; let loading = ref(false); const maxSize = 20 * 1024 * 1024; // 20MB const insertImage = (event) => { const images = extractImages(editorRef.value); const count = imageLength - images.length || 0; if (count == 0) { creationAlertBox("error", `最多只能上传 ${imageLength} 张图片`); return; } const target = event.target.files[0]; if (!target) return; // 处理未选择文件的情况 if (target.size > maxSize) { creationAlertBox("error", "文件大小不能超过 20MB"); return; } loading.value = true; uploading(target, target.name, "image").then((data) => { const selection = window.getSelection(); editorRef.value.focus(); if (lastSelection) { selection.removeAllRanges(); selection.addRange(lastSelection); } const html = `
`; document.execCommand("insertHTML", false, html); judgeIsEmpty(); }); }; const insertVideo = async (event) => { const videos = extractVideos(editorRef.value); const count = videoLength - videos.length || 0; if (count == 0) { creationAlertBox("error", `最多只能上传 ${videoLength} 个视频`); return; } const videoFile = event.target.files[0]; if (!videoFile) return; // 处理未选择文件的情况 if (videoFile.size > maxSize) { creationAlertBox("error", "文件大小不能超过 20MB"); return; } loading.value = true; console.log("videoFile", videoFile); // 步骤1:提取视频第一帧(等待提取完成) const coverFile = await getVideoFirstFrame(videoFile); console.log("第一帧提取成功", coverFile); // 步骤2:先上传视频文件(type 传 'video',按后端要求调整) const videoUploadRes = await uploading(videoFile, videoFile.name, "video"); console.log("视频上传成功", videoUploadRes); // 步骤3:再上传第一帧封面(type 传 'cover',按后端要求调整) const coverUploadRes = await uploading(coverFile, coverFile.name, "image"); console.log("封面上传成功", coverUploadRes); console.log("最终", videoUploadRes, videoUploadRes); const selection = window.getSelection(); editorRef.value.focus(); if (lastSelection) { selection.removeAllRanges(); selection.addRange(lastSelection); } const html = `
`; document.execCommand("insertHTML", false, html); judgeIsEmpty(); }; let isEmpty = ref(true); const onEditorInput = (event) => { const selection = window.getSelection(); if (selection.rangeCount > 0) { lastSelection = selection.getRangeAt(0); // console.log("更新选区"); updatePTitleStatus(); } judgeIsEmpty(); // debouncedGetTagList(); }; // 防抖函数 const debounce = (fn, delay = 500) => { let timer = null; return function () { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, arguments); timer = null; }, delay); }; }; const getRandomChinese = () => { // 中文 Unicode 范围:\u4e00 - \u9fa5(共约 2 万个汉字) const start = 0x4e00; // 起始编码 const end = 0x9fa5; // 结束编码 // 生成范围内的随机整数,转为字符 return String.fromCodePoint(Math.floor(Math.random() * (end - start + 1) + start)); }; const generateRandomString = (length = 5) => { // 定义字符集:包含大小写字母和数字 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; // 循环生成指定长度的随机字符 for (let i = 0; i < length; i++) { // 从字符集中随机取一个字符 const randomIndex = Math.floor(Math.random() * chars.length); result += chars[randomIndex]; } return result; }; const getTagList = () => { if (!isLogin.value) { goLogin(); return; } const content = editorRef.value.innerText; ajax("/v2/api/forum/postPublishTags", { content, }).then((res) => { res = res.data; if (res.code != 200) return; let data = res.data || []; // 随机生成一下数据 for (let i = 0; i < 5; i++) { data.push({ title: getRandomChinese() + getRandomChinese(), tagId: generateRandomString(), }); } tagList.value = data; }); }; const debouncedGetTagList = debounce(getTagList, 500); let isBottomState = ref(false); // 底部按钮 显示 const onEditorFocus = () => { isBottomState.value = true; }; const onEditorBlur = () => { isBottomState.value = false; }; // 判断是否为空 const judgeIsEmpty = () => { const text = editorRef.value.innerText; isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img") && !editorRef.value.querySelector("video"); }; // 处理选中文本变化的函数 const handleSelectionChange = () => { return; const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const commonAncestor = range.commonAncestorContainer; if (editorRef.value.contains(commonAncestor)) { lastSelection = range; } const wRoot = document.querySelector("#editor-container"); if (wRoot && wRoot.contains(commonAncestor)) { lastSelectionW = range; updateWHeadingStatus(); } } }; let isPTitle = ref(false); const paragraphTitle = () => { console.log("editor", editor.addMark); // editor.addMark("bold", true); // 加粗 // editorRef.value.focus(); // if (!lastSelection) return; // const selection = window.getSelection(); // selection.removeAllRanges(); // selection.addRange(lastSelection); // // 使用try-catch确保即使命令执行失败也能恢复滚动位置 // try { // document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2"); // } catch (error) { // console.error("应用段落格式失败:", error); // } // // 更新状态 // setTimeout(() => updatePTitleStatus(), 100); }; const updatePTitleStatus = () => { if (lastSelection) { let parentElement = lastSelection.commonAncestorContainer; // 死循环,直到遇到终止条件 while (true) { // 如果没有父元素了(到达文档根节点),退出循环返回false if (!parentElement) { isPTitle.value = false; return; } // 遇到id为"editor"的元素,返回false if (parentElement.id === "editor") { isPTitle.value = false; return; } // 遇到nodeName为"H2"的元素,返回true(注意nodeName是大写的) if (parentElement.nodeName === "H2") { isPTitle.value = true; return; } // 继续向上查找父元素 parentElement = parentElement.parentElement; } } }; const cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1); let emojiState = ref(false); const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵‍💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]); const openEmoji = () => (emojiState.value = true); const closeEmoji = () => (emojiState.value = false); const selectEmoji = (emoji) => { const selection = window.getSelection(); editorRef.value.focus(); if (lastSelection) { selection.removeAllRanges(); selection.addRange(lastSelection); } document.execCommand("insertText", false, emoji); closeEmoji(); judgeIsEmpty(); }; let format = ref(""); const submit = (status) => { const infoTarget = { ...info.value } || {}; let content = editorRef.value.innerHTML; const images = extractImages(editorRef.value); 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; console.log(content); content = formatContent(content); console.log(content); return; 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) => { // 1. 替换图片标签 html = html.replace(/]*data-aid="(\d+)"[^>]*>/gi, "[attachimg]$1[/attachimg]"); // 1.1 替换视频标签 html = html.replace(/]*aid="(\d+)"[^>]*>[\s\S]*?<\/video>/gi, "[attach]$1[/attach]"); // 2. 替换H2标签 html = html.replace(/]*>([\s\S]*?)<\/h2>/gi, "[b]$1[/b]"); // 5. 处理块级标签换行(仅
等块级标签前后换行,保持行内内容连续) // 块级标签:div、p、h1-h6等,这里以div为例 html = html.replace(/<\/div>\s*/gi, "
\n"); // 闭合div后换行 html = html.replace(/\s*]*>/gi, "\n
"); // 开启div前换行 // 6. 处理
为换行 html = html.replace(//gi, "\n"); // 7. 移除所有剩余HTML标签 a标签除外 html = html.replace(/<(?!(a\b|\/a\b))[^>]+>/gi, ""); // 8. 清理连续换行(最多保留两个空行,避免过多空行) // html = html.replace(/\n{3,}/g, "\n\n"); // 去除首尾空白 html = html.trim(); return html; }; const extractImages = (dom) => { const images = []; // 直接查找页面中所有带 data-aid 的 img 标签 const imgElements = dom.querySelectorAll("img"); imgElements.forEach((imgEl) => { const url = imgEl.getAttribute("src")?.trim() || ""; const aid = imgEl.dataset.aid?.trim() || ""; // 用 dataset 简化自定义属性读取 images.push({ url, aid: Number(aid), }); }); return images; }; const extractVideos = (dom) => { // 1. 查找页面中所有