From 2a227a806d9598c53e6676791c10ec177d44c9e0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RQ919RC\\Pc" <1300399510@qq.com> Date: Thu, 27 Nov 2025 19:23:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E6=A0=B7=E5=BC=8F=E5=92=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复编辑器容器高度设置问题,统一h2标签为h1 调整图片和视频的显示样式,修复表格背景色 优化编辑器工具栏功能,修复链接插入逻辑 --- css/details.css | 8 +- css/details.less | 9 +- css/edit.css | 27 +- css/edit.less | 37 ++- edit.html | 4 - js/details.js | 38 +++ js/edit copy.js | 761 +++++++++++++++++++++++++++++++++++++++++++ js/edit.js | 816 ++++++++++++++--------------------------------- 8 files changed, 1100 insertions(+), 600 deletions(-) create mode 100644 js/edit copy.js diff --git a/css/details.css b/css/details.css index 96abd2d..df13da2 100644 --- a/css/details.css +++ b/css/details.css @@ -173,18 +173,22 @@ } #details .matter .matter-left .html img { max-width: 100%; - display: block; + display: inline-block; } #details .matter .matter-left .html video { margin: 0 auto; } -#details .matter .matter-left .html h2 { +#details .matter .matter-left .html h1 { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; color: #000000; 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 b634c27..d00e8cb 100644 --- a/css/details.less +++ b/css/details.less @@ -200,20 +200,25 @@ img { max-width: 100%; - display: block; + display: inline-block; } video { margin: 0 auto; } - h2 { + h1 { font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; color: #000000; font-size: 18px; line-height: 30px; } + + tr, + td { + background: transparent; + } } .last-time { diff --git a/css/edit.css b/css/edit.css index bfb171a..c92ff83 100644 --- a/css/edit.css +++ b/css/edit.css @@ -76,8 +76,10 @@ #edit .edit-container #editor—wrapper { z-index: 100; } -#edit .edit-container #editor—wrapper .bold { - font-weight: bold; +#edit .edit-container #editor—wrapper .bold, +#edit .edit-container #editor—wrapper .bold span { + font-weight: bolder; + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; } #edit .edit-container #editor—wrapper .editor-toolbar { height: 36px; @@ -262,14 +264,14 @@ #edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item.link .link-box .btn:hover { background-color: #23e0b6; } -#edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item.h2.pitch { +#edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item.h1.pitch { background-color: #f6f6bd; } #edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item.active > button { background-color: #f6f6bd; } #edit .edit-container #editor—wrapper #editor-container { - min-height: 500px; + height: 500px; max-height: 80vh; font-size: 18px; line-height: 26px; @@ -280,6 +282,17 @@ text-decoration: underline; color: #04b0d5; } +#edit .edit-container #editor—wrapper #editor-container h1, +#edit .edit-container #editor—wrapper #editor-container h1 span { + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; + font-weight: 650; + color: #000000; + font-size: 18px; + line-height: 30px; +} +#edit .edit-container #editor—wrapper #editor-container video { + max-width: 100%; +} #edit .edit-container .content-input { min-height: 509px; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; @@ -306,12 +319,12 @@ max-width: 100%; height: 220px; } -#edit .edit-container .content-input h2 { +#edit .edit-container .content-input h1 { + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; + font-weight: 650; color: #000000; font-size: 18px; line-height: 30px; - font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; - font-weight: 650; } #edit .edit-container .content-input .blue { color: #026277; diff --git a/css/edit.less b/css/edit.less index c9e541f..b73f6ec 100644 --- a/css/edit.less +++ b/css/edit.less @@ -85,8 +85,10 @@ #editor—wrapper { z-index: 100; - .bold { - font-weight: bold; + .bold, + .bold span { + font-weight: bolder; + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; } .editor-toolbar { @@ -305,7 +307,7 @@ } } - &.h2 { + &.h1 { &.pitch { background-color: rgba(246, 246, 189, 1); } @@ -320,7 +322,7 @@ } #editor-container { - min-height: 500px; + height: 500px; max-height: 80vh; font-size: 18px; line-height: 26px; @@ -331,6 +333,19 @@ text-decoration: underline; color: #04b0d5; } + + h1, + h1 span { + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; + font-weight: 650; + color: #000000; + font-size: 18px; + line-height: 30px; + } + + video { + max-width: 100%; + } } } @@ -362,13 +377,19 @@ height: 220px; } - h2 { - color: #000000; - font-size: 18px; - line-height: 30px; + h1 { + // color: #000000; + // font-size: 18px; + // line-height: 30px; + + // font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; + // font-weight: 650; font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; + color: #000000; + font-size: 18px; + line-height: 30px; } .blue { diff --git a/edit.html b/edit.html index 0c0e4fb..ca8ba02 100644 --- a/edit.html +++ b/edit.html @@ -40,7 +40,6 @@
-
- - -
diff --git a/js/details.js b/js/details.js index 95fe5e1..ade618e 100644 --- a/js/details.js +++ b/js/details.js @@ -130,6 +130,25 @@ const appSectionIndex = createApp({ if (!targetInfo.hidden) targetInfo.hidden = 0; + targetInfo.attachments = { + images: [ + { + aid: 708161, + url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-S_pItc37qqsgFptxhXa6RWi26P-BuTQYWFOfCsdkb8LQ0NDI5", + }, + ], + files: [], + videos: [ + { + aid: 1009770, + posterid: 1009849, + posterurl: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polfXuP1NFX9ddrB_WbUGy8P79gQxdHR-HKts0V7NkzNDQyOQ~~", + url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polcniG1NFX9ddrB_WbUGy8P79gQxcSFbqQ78MV7NkzNDQyOQ~~", + }, + ], + }; + + // targetInfo.content = '

红红火火恍恍惚惚

[b]红红火火恍恍惚惚有[/b]

\n

\n

[attach]1009770[/attach]

\n

\n

[img=96]708161[/img]

65456456456456465 111 

\n

'; targetInfo.content = '如果你热爱古典文献,又希望在现代职场大展身手——这个项目可能就是你的“本命”!作为香港最正统的中国语言文学项目,它既传承经典,又为你打跨境传播等全新赛道!\n\n🌟 项目核心亮点权威认证:中国语言文学专业认证,考公考编无障碍\n古今结合:深耕古典文献与理论,同时对接AI内容创作等新兴领域\n语言友好:全程中文授课(普通话+粤语),无语言适应压力\n规模可观:每年录取150+,机会相对较多\n\n点击前往 [港校项目库] 查看 \n中国语言文学\n手机扫码查看\n[attachimg]1008942[/attachimg]\n\n🎯 谁最适合申请?中文系、汉语言、古代文学等对口专业背景\n希望在教育、传媒、AI内容或国际中文教育领域发展\n看重学校声誉与专业正统性的同学\n💼 毕业出路超多元除了教师、公务员等传统路径,毕业生还活跃于:\n✔ 跨境文化传播\n✔ AI内容策划与生成\n✔ 国际中文教育\n✔ 出版与编辑行业\n📌 申请指南专业背景:严格限定中文相关专业,暂不接受跨专业申请\n成绩要求:985/211同学建议86+\n语言成绩:雅思7.0(小分5.5)即可\n面试体验:氛围轻松,专业问题较少\n💡 内部消息参考前几轮拿到面试邀请的同学基本都能录取\n985背景优势明显,建议尽早提交申请\n双非同学如背景特别匹配也可尝试\n🤝 欢迎交流你对中国文学在AI时代的发展有什么想法?或者对哪个就业方向,申请问题欢迎在评论区分享交流!\n欢迎加入寄托香港群交流\n\n[attachimg]969489[/attachimg]'; // 替换换行 @@ -194,6 +213,25 @@ const appSectionIndex = createApp({ // 4. 还原粗体标记为h2标签 html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "

$1

"); + // 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(html); // 5. 统一在单次遍历中按出现顺序替换 attach/attachimg diff --git a/js/edit copy.js b/js/edit copy.js new file mode 100644 index 0000000..bdc64c3 --- /dev/null +++ b/js/edit copy.js @@ -0,0 +1,761 @@ +// 简单版本的论坛编辑器,确保图片插入功能正常 +const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; +const { headTop } = await import(withVer("../component/head-top/head-top.js")); + +const editApp = createApp({ + setup() { + let titleLength = ref(200); + + let uniqid = ref(""); + onMounted(() => { + const params = getUrlParams(); + uniqid.value = params.uniqid || ""; + + getUserInfoWin(); + + cUpload(); + init(); + + checkWConfig(); + + // 添加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; + }); + }; + + 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(); + }); + }) + .catch((err) => { + console.log("err", err); + }); + }; + + 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 = () => { + 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 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 = () => { + const selection = window.getSelection(); + // 确保有选中内容且选中区域在编辑器内 + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + // 检查选中区域是否在编辑器内 + const commonAncestor = range.commonAncestorContainer; + if (editorRef.value.contains(commonAncestor)) { + console.log("选中区域在编辑器内", range); + lastSelection = range; + } + } + }; + + const isPTitle = ref(false); + + const paragraphTitle = () => { + 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) => { + if (realname.value == 0 && userInfoWin.value?.uin > 0) { + openAttest(); + return; + } + + if (!isLogin.value) { + goLogin(); + return; + } + + 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; + + content = formatContent(content); + + 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. 查找页面中所有