diff --git a/css/edit.css b/css/edit.css index c92ff83..1a6f776 100644 --- a/css/edit.css +++ b/css/edit.css @@ -9,6 +9,7 @@ height: 60px; background: linear-gradient(180deg, #ffffff -41%, #eef8f9 96%); margin-bottom: 20px; + position: relative; } #edit .edit-head .edit-head-container { width: 1200px; @@ -43,6 +44,15 @@ height: 32px; border-radius: 50%; } +#edit .edit-head .progress-box { + position: absolute; + bottom: -5px; + left: 0; + height: 5px; + width: 2px; + background: linear-gradient(315deg, #6772ff 0px, #00f9e5 100%) center center / 104% 104% #4a54ff; + border-radius: 0 5px 5px 0; +} #edit .edit-container { width: 890px; background-color: #ffffff; @@ -134,6 +144,10 @@ #edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item > button.active { background-color: #f6f6bd; } +#edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item > button.disabled { + color: #999; + cursor: not-allowed; +} #edit .edit-container #editor—wrapper .editor-toolbar .toolbar-item .file { opacity: 0; /* 隐藏输入框 */ @@ -277,19 +291,20 @@ line-height: 26px; color: #333333; } -#edit .edit-container #editor—wrapper #editor-container a { +#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 !important; + font-weight: 650 !important; + color: #000000; + font-size: 18px !important; + line-height: 30px; +} +#edit .edit-container #editor—wrapper #editor-container a, +#edit .edit-container #editor—wrapper #editor-container a span { font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; 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%; } diff --git a/css/edit.less b/css/edit.less index b73f6ec..a52a4da 100644 --- a/css/edit.less +++ b/css/edit.less @@ -10,6 +10,7 @@ height: 60px; background: linear-gradient(180deg, rgba(255, 255, 255, 1) -41%, rgba(238, 248, 249, 1) 96%); margin-bottom: 20px; + position: relative; .edit-head-container { width: 1200px; @@ -48,6 +49,16 @@ border-radius: 50%; } } + + .progress-box { + position: absolute; + bottom: -5px; + left: 0; + height: 5px; + width: 2px; + background: linear-gradient(315deg, rgb(103, 114, 255) 0px, rgb(0, 249, 229) 100%) center center / 104% 104% rgb(74, 84, 255); + border-radius: 0 5px 5px 0; + } } .edit-container { @@ -152,6 +163,11 @@ &.active { background-color: rgba(246, 246, 189, 1); } + + &.disabled { + color: #999; + cursor: not-allowed; + } } .file { @@ -327,22 +343,22 @@ font-size: 18px; line-height: 26px; color: #333333; + h1, + h1 span { + font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif !important; + font-weight: 650 !important; + color: #000000; + font-size: 18px !important; + line-height: 30px; + } - a { + a, + a span { font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; 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%; } diff --git a/css/public.css b/css/public.css index c444f30..6ad4671 100644 --- a/css/public.css +++ b/css/public.css @@ -1891,3 +1891,38 @@ td { transform: rotate(360deg); } } +#pre-loader { + height: 70vh; + display: flex; + justify-content: center; + align-items: center; +} +#pre-loader .three-bounce > div { + display: inline-block; + width: 18px; + height: 18px; + border-radius: 100%; + top: 50%; + margin-top: -9px; + background: #aeadba; + animation: bouncedelay 1.4s infinite ease-in-out; + animation-fill-mode: both; +} +#pre-loader .three-bounce .one { + animation-delay: -0.32s; +} +#pre-loader .three-bounce .two { + animation-delay: -0.16s; +} +@keyframes bouncedelay { + 0%, + 100%, + 80% { + transform: scale(0); + -webkit-transform: scale(0); + } + 40% { + transform: scale(1); + -webkit-transform: scale(1); + } +} diff --git a/css/public.less b/css/public.less index 1f87ca4..a63f419 100644 --- a/css/public.less +++ b/css/public.less @@ -2273,3 +2273,46 @@ td { transform: rotate(360deg); } } + +#pre-loader { + height: 70vh; + display: flex; + justify-content: center; + align-items: center; + + .three-bounce { + > div { + display: inline-block; + width: 18px; + height: 18px; + border-radius: 100%; + top: 50%; + margin-top: -9px; + background: #aeadba; + animation: bouncedelay 1.4s infinite ease-in-out; + animation-fill-mode: both; + } + + .one { + animation-delay: -0.32s; + } + + .two { + animation-delay: -0.16s; + } + } +} + +@keyframes bouncedelay { + 0%, + 100%, + 80% { + transform: scale(0); + -webkit-transform: scale(0); + } + + 40% { + transform: scale(1); + -webkit-transform: scale(1); + } +} diff --git a/details.html b/details.html index 23b1018..f6600ce 100644 --- a/details.html +++ b/details.html @@ -16,9 +16,16 @@ +
+
+
+
+
+
+
-
qXi0yrL189WW
+
4uPq5uKzTPTP
diff --git a/edit.html b/edit.html index ca8ba02..fb4429a 100644 --- a/edit.html +++ b/edit.html @@ -21,21 +21,25 @@
+
- + + +
发帖
发帖奖励 3 个寄托币/篇,每天最高奖励3篇
+ +
- +
{{ info?.title?.length ? titleLength - info?.title?.length : titleLength }}
@@ -89,13 +93,14 @@
{{ emoji }}
--> - +
+
diff --git a/js/details.js b/js/details.js index ade618e..d57333f 100644 --- a/js/details.js +++ b/js/details.js @@ -1,6 +1,5 @@ const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue; -const ASSET_VERSION = window.__ASSET_VERSION__ || "20251126"; -const withVer = (p) => `${p}?v=${ASSET_VERSION}`; + const { itemForum } = await import(withVer("../component/item-forum/item-forum.js")); const { itemOffer } = await import(withVer("../component/item-offer/item-offer.js")); const { itemSummary } = await import(withVer("../component/item-summary/item-summary.js")); @@ -80,6 +79,9 @@ const appSectionIndex = createApp({ let uniqidRef = ref(null); onMounted(() => { + const preLoader = document.getElementById("pre-loader"); + if (preLoader) preLoader.style.display = "none"; + uniqid = uniqidRef.value.innerText; init(); @@ -121,7 +123,7 @@ const appSectionIndex = createApp({ ajaxGet(`/v2/api/forum/getTopicDetails?uniqid=${uniqid}`).then((res) => { if (res.code != 200) { creationAlertBox("error", res.message || "主题不存在"); - setTimeout(() => redirectToExternalWebsite(`/`), 3000); + // setTimeout(() => redirectToExternalWebsite(`/`), 3000); return; } const data = res.data; @@ -130,27 +132,6 @@ 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]'; - // 替换换行 targetInfo.content = targetInfo.content?.replace(/\n/g, "
") || ""; @@ -330,6 +311,11 @@ const appSectionIndex = createApp({ let isLikeGif = ref(false); const likeClick = () => { + if (realname.value == 0 && userInfoWin.value?.uin > 0) { + openAttest(); + return; + } + if (!isLogin.value) { goLogin(); return; @@ -558,6 +544,15 @@ const appSectionIndex = createApp({ const handleAnswerText = (e) => { if (e.target.tagName === "IMG") { + // 检查点击的图片是否被a标签包裹 + const anchorTag = e.target.closest("a"); + + // 如果被a标签包裹,则不执行图片预览,让链接正常跳转 + if (anchorTag) { + return; + } + + // 否则,执行图片预览 var src = e.target.getAttribute("src"); previewImage.initComponent(src); } @@ -603,7 +598,6 @@ const appSectionIndex = createApp({ let emojiState = ref(false); let emojiMaskState = ref(false); - let emojiBottomDistance = ref(0); let inputTextarea = ref(""); // 打开 Emoji @@ -622,6 +616,28 @@ const appSectionIndex = createApp({ } emojiMaskState.value = true; + + let emojiBottomDistance = 0; + const doc = document.documentElement; + try { + const targetEl = (event && (event.currentTarget || event.target)) || null; + const rect = targetEl && targetEl.getBoundingClientRect ? targetEl.getBoundingClientRect() : null; + if (rect) { + const elementBottomDocY = rect.bottom + window.scrollY; + emojiBottomDistance = Math.max(doc.scrollHeight - elementBottomDocY, 0); + } else { + const pageY = event && (event.pageY != null ? event.pageY : event.clientY != null ? event.clientY + window.scrollY : 0); + emojiBottomDistance = Math.max(doc.scrollHeight - pageY, 0); + } + + const itemEl = targetEl && targetEl.closest ? targetEl.closest(".item") : null; + + const boxEl = itemEl ? itemEl.querySelector(".emoji-box") : null; + if (boxEl) { + if (emojiBottomDistance < 500) boxEl.classList.add("top"); + else boxEl.classList.remove("top"); + } + } catch (e) {} }; // 关闭 Emoji @@ -1063,6 +1079,7 @@ const appSectionIndex = createApp({ element.timestamp = strtimeago(element.created_at, 4); element["isReplyBoxShow"] = 0; element["picture"] = []; + if (element["content"]) element["content"] = restoreHtml(element["content"], element.attachments, "comment"); }); let merged = [...commentList.value[index]["child"], ...data.data.filter((item2) => !commentList.value[index]["child"].find((item1) => item1.id == item2.id))]; @@ -1218,7 +1235,7 @@ const appSectionIndex = createApp({ ajax(`/v2/api/forum/postTopicShare`, { token }); }; - return { emojiBottomDistance, uniqidRef, share, reportToken, isReplyBoxShow, matterHeight, sidebarHeight, deleteItem, maxPicture, sidebarFixed, matterRef, sidebarRef, pitchInputState, ismyself, edit, searchInput, defaultSearchText, goSearch, goPersonalHomepage, QRcode, alsoCommentsData, copyLinkClick, reportState, tokentoken, essence, recommend, hide, report, cutShow, ismanager, show, openDiscuss, commentDelete, handleInputPaste, autoResize, editCommentState, selectEditEmoji, closeEditEmoji, openEditEmoji, closeEdit, openEdit, closeEditFileUpload, postEditComment, submitAnswerComments, closePictureUpload, closeFileUpload, picture, editToken, editPicture, editInput, editEmojiState, handleFileUpload, inputTextarea, judgeLogin, handleEditFile, selectEmoji, emojiData, emojiMaskState, emojiState, closeEmoji, openEmoji, closeAnswerCommentsChild, openAnswerCommentsChild, handleAnswerText, sendMessage, TAHomePage, operateAnswerCommentsLike, closeUserInfo, openUserInfo, permissions, commentList, commentPage, commentTotalCount, picture, userInfoWin, relatedList, relatedTime, coinNubmer, coinList, coinAmount, coinSubmit, strategy, mybalance, coinsState, openCoinBox, closeCoinBox, isLikeGif, likeClick, collectClick, islike, iscollect, recentlyList, medal, count, sectionn, tags, authorInfo, info, timestamp, updatedTime }; + return { uniqidRef, share, reportToken, isReplyBoxShow, matterHeight, sidebarHeight, deleteItem, maxPicture, sidebarFixed, matterRef, sidebarRef, pitchInputState, ismyself, edit, searchInput, defaultSearchText, goSearch, goPersonalHomepage, QRcode, alsoCommentsData, copyLinkClick, reportState, tokentoken, essence, recommend, hide, report, cutShow, ismanager, show, openDiscuss, commentDelete, handleInputPaste, autoResize, editCommentState, selectEditEmoji, closeEditEmoji, openEditEmoji, closeEdit, openEdit, closeEditFileUpload, postEditComment, submitAnswerComments, closePictureUpload, closeFileUpload, picture, editToken, editPicture, editInput, editEmojiState, handleFileUpload, inputTextarea, judgeLogin, handleEditFile, selectEmoji, emojiData, emojiMaskState, emojiState, closeEmoji, openEmoji, closeAnswerCommentsChild, openAnswerCommentsChild, handleAnswerText, sendMessage, TAHomePage, operateAnswerCommentsLike, closeUserInfo, openUserInfo, permissions, commentList, commentPage, commentTotalCount, picture, userInfoWin, relatedList, relatedTime, coinNubmer, coinList, coinAmount, coinSubmit, strategy, mybalance, coinsState, openCoinBox, closeCoinBox, isLikeGif, likeClick, collectClick, islike, iscollect, recentlyList, medal, count, sectionn, tags, authorInfo, info, timestamp, updatedTime }; }, }); diff --git a/js/edit.js b/js/edit.js index 46152a0..7b035d4 100644 --- a/js/edit.js +++ b/js/edit.js @@ -1,6 +1,6 @@ // 简单版本的论坛编辑器,确保图片插入功能正常 const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; -import { headTop } from "../component/head-top/head-top.js"; +const { headTop } = await import(withVer("../component/head-top/head-top.js")); const { createEditor, createToolbar, SlateTransforms, Boot, SlateEditor } = window.wangEditor; class MyButtonMenu { @@ -40,6 +40,10 @@ const editApp = createApp({ let titleLength = ref(200); let uniqid = ref(""); + + const valueA = ref(null); + let valueUrl = ""; + onMounted(() => { const params = getUrlParams(); uniqid.value = params.uniqid || ""; @@ -48,6 +52,21 @@ const editApp = createApp({ 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; @@ -139,54 +158,20 @@ const editApp = createApp({ uniqid: uniqid.value, }) .then((res) => { - // res = { - // code: 200, - // message: "成功", - // data: { - // info: { - // uniqid: "8a0yn9CWGjur", - // tags: [], - // title: "香港🇭🇰梦中情校offer,叉烧包我来了!", - // attachments: { - // images: [ - // { - // aid: 1008535, - // url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokcHyD1NFX9ddrB_WbUGy8P79gQxdHE7aVs5oV7NkzNDQyOQ~~", - // }, - // { - // aid: 1008032, - // url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokdXyE1NFX9ddrB_WbUGy8P79gQxdAFefK5JoV7NkzNDQyOQ~~", - // }, - // ], - // files: [], - // videos: [ - // { - // aid: 1008621, - // posterid: 1008676, - // posterurl: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokc3iA1NFX9ddrB_WbUGy8P79gQxdHFOXC5J0V7NkzNDQyOQ~~", - // url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokc32H1NFX9ddrB_WbUGy8P79gQxcSFeCW5s8V7NkzNDQyOQ~~", - // }, - // ], - // }, - // anonymous: 1, - // created_at: "2025-11-10 15:49:15", - // type: "thread", - // content: "[attach]1008621[/attach]\n[b]哈哈哈fdsafdaf🤫afafafas[/b]\n\n[b]😘[/b]\n🍻\n[b]婆婆[/b]\n一\n[b]样[/b]\n😉\n[b]噜噜噜fdsafdsafdafafsafafdsfdafsafsafsas[/b]\n[attachimg]1008535[/attachimg]\n\nfds\n[b]afsa[/b]\nfsdafafafafdas\n[b]魂牵梦萦 fsdaf[/b]\n[attachimg]1008032[/attachimg]", - // role: { - // type: "public", - // }, - // }, - // token: "zkzAyP2l9uzYy4S63Ew3zJ1N1QkpC0ZJ7BTUBmhaeQsjc_ACxctWNq5ZtxRkFzPoNTM4W2BkojS6qZ14BLHTPRi3ohhoRKpC22Bui4qps4MDDbdu22VQtra72BDqIykNcfkCj2MDyxbHXAlC6VWGmUbA3VQ0NmUz", - // tagList: [], - // }, - // }; 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); @@ -194,6 +179,8 @@ const editApp = createApp({ info.value = infoTarget; token.value = data.token; + console.log("data", data); + initEditor(); }) .catch((err) => { @@ -204,6 +191,18 @@ const editApp = createApp({ 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 || {}; @@ -218,58 +217,54 @@ const editApp = createApp({ ["insertImage"]: { onInsertedImage(imageNode) { console.log("insertImage"); - if (imageNode == null) return; + // 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, - - // form-data fieldName ,默认值 'wangeditor-uploaded-image' fieldName: uConfigData.requestName, - - // 单个文件的最大体积限制,默认为 2M maxFileSize: maxSize, // 1M - - // 最多可上传几个文件,默认为 100 maxNumberOfFiles: imageLength, - - // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] allowedFileTypes: ["image/png", "image/jpeg", "image/jpg"], // .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 秒 - + timeout: 60 * 1000, // 15 秒 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}?aid=${data.aid}`); // 传入图片的可访问 URL - }); + const img = await uploading(file, file.name, "image"); + insertFn(`${img.url}?aid=${img.aid}`); } catch (err) { console.error("上传出错:", err); } @@ -284,6 +279,7 @@ const editApp = createApp({ const { src } = videoNode; // console.log("inserted video", src); }, + parseVideoSrc: customParseVideoSrc, // 也支持 async 函数 }, ["uploadVideo"]: { @@ -291,26 +287,24 @@ const editApp = createApp({ fieldName: uConfigData.requestName, maxFileSize: maxSize, // 1M maxNumberOfFiles: videoLength, - allowedFileTypes: ["video/*"], + 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: 15 * 1000, // 15 秒 + 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); - - // 步骤3:再上传第一帧封面(type 传 'cover',按后端要求调整) + // console.log("第一帧提取成功", coverFile); const coverUploadRes = await uploading(coverFile, coverFile.name, "image"); - console.log("封面上传成功", coverUploadRes); - + // console.log("封面上传成功", coverUploadRes); insertFn(`${videoUploadRes.url}?aid=${videoUploadRes.aid}`, `${coverUploadRes.url}?aid=${coverUploadRes.aid}`); } catch (err) { console.error("上传出错:", err); + progress.value = 0; } }, }, @@ -323,9 +317,6 @@ const editApp = createApp({ hoverbarKeys: { text: { menuKeys: [] }, video: { menuKeys: [] } }, }; - - // html: infoTarget.content, - editor = createEditor({ selector: "#editor-container", html: infoTarget.content, @@ -334,7 +325,18 @@ const editApp = createApp({ }); const toolbarConfig = { - toolbarKeys: ["header1", "uploadImage", "uploadVideo", "emotion", "insertLink", "bold"], + toolbarKeys: [ + "header1", + { + key: "group-image", + title: "图片", + menuKeys: ["insertImage", "uploadImage"], + }, + "uploadVideo", + "emotion", + "insertLink", + "bold", + ], }; const menu1Conf = { @@ -362,37 +364,37 @@ const editApp = createApp({ const h1 = toolbarRef.value.querySelector('[data-menu-key="header1"]'); const h1Item = h1.parentElement; h1Item.classList.add("toolbar-item", "flexacenter"); - h1.innerHTML = '段落标题 段落标题'; + h1.innerHTML = `段落标题 段落标题`; - const image = toolbarRef.value.querySelector('[data-menu-key="uploadImage"]'); + const image = toolbarRef.value.querySelector('[data-menu-key="group-image"]'); const imageItem = image.parentElement; imageItem.classList.add("toolbar-item", "flexacenter"); - image.innerHTML = '图片 图片'; + image.innerHTML = `图片 图片`; const video = toolbarRef.value.querySelector('[data-menu-key="uploadVideo"]'); const videoItem = video.parentElement; videoItem.classList.add("toolbar-item", "flexacenter"); - video.innerHTML = '视频 视频'; + video.innerHTML = `视频 视频`; const emotion = toolbarRef.value.querySelector('[data-menu-key="emotion"]'); const emotionItem = emotion.parentElement; emotionItem.classList.add("toolbar-item", "flexacenter"); - emotion.innerHTML = '表情 表情'; + emotion.innerHTML = `表情 表情`; const link = toolbarRef.value.querySelector('[data-menu-key="insertLink"]'); const linkItem = link.parentElement; linkItem.classList.add("toolbar-item", "flexacenter"); - link.innerHTML = '链接 链接'; + link.innerHTML = `链接 链接`; const bold = toolbarRef.value.querySelector('[data-menu-key="bold"]'); const boldItem = bold.parentElement; boldItem.classList.add("toolbar-item", "flexacenter"); - bold.innerHTML = '加粗 加粗'; + bold.innerHTML = `加粗 加粗`; const customCenter = toolbarRef.value.querySelector('[data-menu-key="customCenter"]'); const customCenterItem = customCenter.parentElement; customCenterItem.classList.add("toolbar-item", "flexacenter"); - customCenter.innerHTML = '居中 居中'; + customCenter.innerHTML = `居中 居中`; }); }; @@ -405,7 +407,7 @@ const editApp = createApp({ let html = formattedText; // 0. 将所有
转为

,

转为

- html = html.replace(/
/g, "

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

"); // 1. 还原换行符为
标签 @@ -424,6 +426,23 @@ const editApp = createApp({ // 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)); // 统一字符串比较,避免类型问题 @@ -451,7 +470,7 @@ const editApp = createApp({ const image = imageList.find((img) => img.aid == aid); if (image) { imageList.splice(imageList.indexOf(image), 1); - return ``; + return ``; } return match; // 未找到对应图片时保留原始标记 }); @@ -498,7 +517,6 @@ const editApp = createApp({ Array.from(__c.querySelectorAll("video")).forEach((vd) => { const p = vd.parentElement; if (!p || p.tagName !== "P") { - console.log(999999999999999999999999999999); const wrap = document.createElement("p"); p ? p.insertBefore(wrap, vd) : __c.appendChild(wrap); wrap.appendChild(vd); @@ -507,7 +525,7 @@ const editApp = createApp({ html = __c.innerHTML; - console.log("html3", html); + console.log("初始化显示的html", html); return html; }; @@ -532,10 +550,10 @@ const editApp = createApp({ return; } - // if (!isLogin.value) { - // goLogin(); - // return; - // } + if (!isLogin.value) { + goLogin(); + return; + } const infoTarget = { ...info.value } || {}; let content = editor.getHtml(); @@ -551,16 +569,16 @@ const editApp = createApp({ info.value["attachments"]["images"] = images; info.value["attachments"]["videos"] = videos; - console.log(content); + console.log("原始html", content); content = formatContent(content); - console.log(content); + console.log("最终html", content); const data = { ...infoTarget, content, }; console.log("data", data); - // return + if (location.hostname == "127.0.0.1") return; ajax("/v2/api/forum/postPublishTopic", { info: data, @@ -584,9 +602,6 @@ const editApp = createApp({ }; const formatContent = (html) => { - // 1. 替换图片标签 - // html = html.replace(/]*data-aid="(\d+)"[^>]*>/gi, "[attachimg]$1[/attachimg]"); - // 1. 替换图片标签(优先解析src中的aid+宽高生成自定义格式,再兼容原有data-aid逻辑) html = html.replace(/]*>/gi, (imgTag) => { const srcMatch = imgTag.match(/src="([^"]+)"/i); @@ -625,12 +640,18 @@ const editApp = createApp({ console.log("width", width, "height", height); // 第四步:按规则生成格式 - if (width == 0 && height == 0) return `[img]${aid}[/img]`; - else return `[img=${width}${height ? "," + height : ""}]${aid}[/img]`; + 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(/]*aid="(\d+)"[^>]*>[\s\S]*?<\/video>/gi, "[attach]$1[/attach]"); html = html.replace(/]*>[\s\S]*?<\/video>/gi, (videoTag) => { // 第一步:提取video内source标签的src属性 const sourceSrcMatch = videoTag.match(/]*>/i); @@ -652,25 +673,17 @@ const editApp = createApp({ }); // 2. 替换H2标签 - html = html.replace(/]*>([\s\S]*?)<\/h1>/gi, "[b]$1[/b]"); + html = html.replace(/]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]"); + html = html.replace(/]*>([\s\S]*?)<\/strong>/gi, "[b]$1[/b]"); - // html = html.replace(/]*>([\s\S]*?)<\/strong>/gi, "[strong]$1[/strong]"); - - // html = html.replace(/]*style=["'][^"']*text-align\s*:\s*center[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, "[center]$1[/center]"); - - // 5. 处理块级标签换行(仅
等块级标签前后换行,保持行内内容连续) - // 块级标签:div、p、h1-h6等,这里以div为例 - // html = html.replace(/<\/div>\s*/gi, "
\n"); // 闭合div后换行 - // html = html.replace(/\s*]*>/gi, "\n
"); // 开启div前换行 + // 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"); - // 7. 移除所有剩余HTML标签 a标签除外 - // html = html.replace(/<(?!(a\b|\/a\b))[^>]+>/gi, ""); - - // 8. 清理连续换行(最多保留两个空行,避免过多空行) - // html = html.replace(/\n{3,}/g, "\n\n"); // 去除首尾空白 html = html.trim(); @@ -736,26 +749,49 @@ const editApp = createApp({ return result; }; + const progress = ref(0); + const uploading = (target, name, type) => { return new Promise((resolve, reject) => { let config = uConfigData; const formData = new FormData(); - formData.append(config.requestName, target); // 文件数据 - formData.append("name", name); // 文件名 - formData.append("type", type); // 文件名 - formData.append("data", config.params.data); // 文件名 + formData.append(config.requestName, target); + formData.append("name", name); + formData.append("type", type); + if (config.params && config.params.data) { + formData.append("data", config.params.data); + } - ajax(config.url, formData) - .then((res) => { - const data = res.data; - try { - resolve(data); - } catch (error) { - console.error("插入图片出错:", error); + axios + .post(config.url, formData, { + headers: { + ...config.headers, + "Content-Type": "multipart/form-data", + }, + onUploadProgress: (e) => { + progress.value = Math.round((e.loaded / e.total) * 100); + console.log("progress.value", progress.value); + }, + withCredentials: true, + }) + .then((response) => { + const res = response.data; + if (res.code == 200) { + resolve(res.data); + } else { + creationAlertBox("error", res.message || "上传失败"); + reject(res); } }) - .finally(() => { - loading.value = false; + .catch((error) => { + if (error.response) { + creationAlertBox("error", `HTTP错误: ${error.response.status}`); + } else if (error.request) { + creationAlertBox("error", "网络错误"); + } else { + creationAlertBox("error", "请求设置错误"); + } + reject(error); }); }); }; @@ -824,7 +860,7 @@ const editApp = createApp({ }); }; - return { toolbarRef, uniqid, userInfoWin, titleLength, submit, info, tagList, token, cutAnonymity, editorRef }; + return { progress, valueA, toolbarRef, uniqid, userInfoWin, titleLength, submit, info, tagList, token, cutAnonymity, editorRef }; }, });