// 简单版本的论坛编辑器,确保图片插入功能正常 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(); // console.log(valueA.value); 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 || {}; // if (location.hostname == "127.0.0.1") // infoTarget.content = `

图片描述

// //


`; 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; console.log("data", data); 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) { 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) { // 如果图片链接中已经包含了 ?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 || "操作失败"); } catch (e) { console.error("Transform network image failed", e); } return src; }, }, ["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"); insertFn(`${img.url}?aid=${img.aid}`); } catch (err) { console.error("上传出错:", err); } }, }, ["insertVideo"]: { onInsertedVideo(videoNode) { if (videoNode == null) return; // console.log(videoNode); 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, 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. 将所有
转为

,

转为

html = html.replace(/)/gi, "/g, "

"); // 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

"); // html = html.replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, "$1"); // html = html.replace(/\[center\]([\s\S]*?)\[\/center\]/gi, '

$1

'); // console.log("html1", html); // 5. 还原 a > [img] 的情况 html = html.replace(/]*>\[img(?:=([0-9]+(?:\.[0-9]+)?)(?:,([0-9]+(?:\.[0-9]+)?))?)?\](\d+)\[\/img\]<\/a>/gi, (match, href, width, height, aid) => { const image = imageList.find((img) => String(img.aid) === String(aid)); if (!image) return match; const index = imageList.findIndex((img) => String(img.aid) === String(aid)); if (index > -1) imageList.splice(index, 1); let style = ""; const w = width ? Number(width) : 0; const h = height ? Number(height) : 0; if (w > 0 && h > 0) style = `style="width: ${w}px; height: ${h}px;"`; else if (w > 0) style = `style="width: ${w}px;"`; return ``; }); // 5. 还原【新增图片格式】[img=width,height]aid[/img] 或 [img]aid[/img] html = html.replace(/\[img(?:=([0-9]+(?:\.[0-9]+)?)(?:,([0-9]+(?:\.[0-9]+)?))?)?\](\d+)\[\/img\]/gi, (match, width, height, aid) => { const image = imageList.find((img) => String(img.aid) === String(aid)); // 统一字符串比较,避免类型问题 if (!image) return match; // 从列表中移除已匹配的图片(避免重复使用) const index = imageList.findIndex((img) => String(img.aid) === String(aid)); if (index > -1) imageList.splice(index, 1); // 拼接img标签(带宽高样式,宽高为0则不设置) let style = ""; const w = width ? Number(width) : 0; const h = height ? Number(height) : 0; if (w > 0 && h > 0) style = `style="width: ${w}px; height: ${h}px;"`; else if (w > 0) style = `style="width: ${w}px;"`; return ``; }); // console.log("html2", html); // 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); 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 += `
`)); videosList.forEach((element) => (html += `
`)); const __c = document.createElement("div"); __c.innerHTML = html; Array.from(__c.querySelectorAll("img")).forEach((im) => { const p = im.parentElement; if (!p || p.tagName !== "P") { const wrap = document.createElement("p"); p ? p.insertBefore(wrap, im) : __c.appendChild(wrap); wrap.appendChild(im); } }); Array.from(__c.querySelectorAll("video")).forEach((vd) => { const p = vd.parentElement; if (!p || p.tagName !== "P") { const wrap = document.createElement("p"); p ? p.insertBefore(wrap, vd) : __c.appendChild(wrap); wrap.appendChild(vd); } }); html = __c.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 (realname.value == 0 && userInfoWin.value?.uin > 0) { openAttest(); return; } if (!isLogin.value) { goLogin(); return; } const infoTarget = { ...info.value } || {}; let content = editor.getHtml(); 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("原始html", content); content = formatContent(content); console.log("最终html", content); const data = { ...infoTarget, content, }; console.log("data", data); if (location.hostname == "127.0.0.1") return; 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. 替换图片标签(优先解析src中的aid+宽高生成自定义格式,再兼容原有data-aid逻辑) html = html.replace(/]*>/gi, (imgTag) => { const srcMatch = imgTag.match(/src="([^"]+)"/i); let aid = ""; if (srcMatch && srcMatch[1]) { const aidMatch = srcMatch[1].match(/aid=(\d+)/); if (aidMatch) aid = aidMatch[1]; } if (!aid) { const dataAidMatch = imgTag.match(/data-aid="(\d+)"/i); if (dataAidMatch) aid = dataAidMatch[1]; } if (!aid) return imgTag; const styleMatch = imgTag.match(/style="([^"]+)"/i); let width = 0, height = 0; if (styleMatch && styleMatch[1]) { const widthMatch = styleMatch[1].match(/width:\s*(\d+(?:\.\d+)?)px/i); const heightMatch = styleMatch[1].match(/height:\s*(\d+(?:\.\d+)?)px/i); width = widthMatch ? Number(widthMatch[1]) : 0; height = heightMatch ? Number(heightMatch[1]) : 0; if (!width) { const widthPctMatch = styleMatch[1].match(/width:\s*(\d+(?:\.\d+)?)%/i); if (widthPctMatch) { const el = (editorRef && editorRef.value) || document.querySelector("#editor-container"); const boxW = el ? el.getBoundingClientRect().width || el.clientWidth || 0 : 0; const pct = Number(widthPctMatch[1]); if (boxW && pct > 0) width = Math.round((pct / 100) * boxW); } } } console.log("width", width, "height", height); // 第四步:按规则生成格式 let result; if (width == 0 && height == 0) result = `[img]${aid}[/img]`; else result = `[img=${width}${height ? "," + height : ""}]${aid}[/img]`; // 提取 data-href 并添加 a 标签 const dataHrefMatch = imgTag.match(/data-href="([^"]+)"/i); if (dataHrefMatch && dataHrefMatch[1]) result = `
${result}`; return result; }); // 1.1 替换视频标签 html = html.replace(/]*>[\s\S]*?<\/video>/gi, (videoTag) => { // 第一步:提取video内source标签的src属性 const sourceSrcMatch = videoTag.match(/]*>/i); let aid = ""; if (sourceSrcMatch && sourceSrcMatch[1]) { // 从source的src中提取aid const aidMatch = sourceSrcMatch[1].match(/aid=(\d+)/); if (aidMatch) aid = aidMatch[1]; } // 第二步:兼容原有video标签的aid属性(如果source中没有,取video的aid) if (!aid) { const videoAidMatch = videoTag.match(/aid="(\d+)"/i); if (videoAidMatch) aid = videoAidMatch[1]; } // 无aid则返回原标签,有则生成指定格式 return aid ? `[attach]${aid}[/attach]` : ""; }); // 2. 替换H2标签 html = html.replace(/]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]"); html = html.replace(/]*>([\s\S]*?)<\/strong>/gi, "[b]$1[/b]"); // 3.ghj hgj ghj 替换为 [url=ghj hgj gh jghj ]ghj hgj ghj [/url] html = html.replace(/([\s\S]*?)<\/a>/gi, (match, href, content) => { return `[url=${href}]${content}[/url]`; }); // 6. 处理
为换行 html = html.replace(//gi, "\n"); // 去除首尾空白 html = html.trim(); return html; }; 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; images.push({ url: cleanUrl, aid: Number(aid), }); }); console.log("提取完成的图片列表:", images); return images; }; const extractVideos = (dom) => { // 1. 查找页面中所有