feat(editor): 添加预加载动画和编辑器功能优化

- 新增预加载动画组件及样式
- 优化编辑器图片和视频上传处理逻辑
- 修复编辑器内容转换和格式处理问题
- 添加上传进度显示功能
- 改进编辑器工具栏图标和布局
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-11-28 18:06:40 +08:00
parent 2a227a806d
commit 0960a310aa
8 changed files with 366 additions and 192 deletions

View File

@@ -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 #editorwrapper .editor-toolbar .toolbar-item > button.active {
background-color: #f6f6bd;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item > button.disabled {
color: #999;
cursor: not-allowed;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .file {
opacity: 0;
/* 隐藏输入框 */
@@ -277,19 +291,20 @@
line-height: 26px;
color: #333333;
}
#edit .edit-container #editorwrapper #editor-container a {
#edit .edit-container #editorwrapper #editor-container h1,
#edit .edit-container #editorwrapper #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 #editorwrapper #editor-container a,
#edit .edit-container #editorwrapper #editor-container a span {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
text-decoration: underline;
color: #04b0d5;
}
#edit .edit-container #editorwrapper #editor-container h1,
#edit .edit-container #editorwrapper #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 #editorwrapper #editor-container video {
max-width: 100%;
}

View File

@@ -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%;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -16,9 +16,16 @@
</head>
<body>
<div id="pre-loader">
<div class="three-bounce" p-id="11">
<div class="one" p-id="12"></div>
<div class="two" p-id="13"></div>
<div class="three" p-id="14"></div>
</div>
</div>
<sign-in-box></sign-in-box>
<div class="container" id="details" v-cloak>
<div class="templateValue" ref="uniqidRef">qXi0yrL189WW</div>
<div class="templateValue" ref="uniqidRef">4uPq5uKzTPTP</div>
<div class="head-top flexacenter">
<img class="logo" src="https://oss.gter.net/logo" alt="" />

View File

@@ -21,21 +21,25 @@
<body>
<div class="container" id="edit" v-cloak>
<div class="valueA" ref="valueA" style="display: none;">{@}</div>
<div class="edit-head flexacenter">
<div class="edit-head-container flexacenter">
<a class="" href="/" target="_blank"><img class="icon" src="{@/img/edit-logo-icon.png}" /></a>
<a class="" href="/" target="_blank">
<img class="icon" src="{@/img/edit-logo-icon.png}" />
</a>
<div class="dot"></div>
<div class="title">发帖</div>
<div class="hint">发帖奖励 3 个寄托币/篇每天最高奖励3篇</div>
<div class="flex1"></div>
<img v-if="userInfoWin.avatar" class="avatar" :src="userInfoWin.avatar" />
</div>
<!-- <div class="progress-box" v-if="progress != 0 || progress != 100" :style="{ width: progress + '%' }"></div> -->
</div>
<div class="edit-container">
<!-- 标题输入 -->
<div class="title-box">
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title"
:maxlength="titleLength" />
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title" :maxlength="titleLength" />
<div class="sum">{{ info?.title?.length ? titleLength - info?.title?.length : titleLength }}</div>
</div>
@@ -89,13 +93,14 @@
<div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji" @click.stop="selectEmoji(emoji)">{{ emoji }}</div>
</div>
</div> -->
</div>
<div id="editor-container" ref="editorRef"><!-- 编辑器 --></div>
</div>
<!-- 内容编辑区 -->
<!-- <div class="content-input" id="editor" contenteditable="true" :class="{ 'empty': isEmpty }" placeholder="输入正文" ref="editorRef" @input="onEditorInput" @focus="onEditorFocus" @blur="onEditorBlur" v-html="info.content" @click="handleClick"></div> -->
<!-- 操作按钮 -->
<div class="action-buttons flexacenter">
<div class="left-section flexacenter" @click="cutAnonymity">

View File

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

View File

@@ -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]魂牵梦萦&nbsp;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 = `<p><img src="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polfHeD1NFX9ddrB_WbUGy8P79gQxccQOeR45kV7NkzNDQyOQ~~?aid=1009985" alt="图片描述" data-href="https://i-operation.csdnimg.cn/ad/ad_pic/a0beaaca1e2047e0ae5c0783e02b3c0a.png" style=""/></p><div data-w-e-type="video" data-w-e-is-void>
// <video poster="https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polcniH1NFX9ddrB_WbUGy8P79gQxcSQbvLtMsV7NkzNDQyOQ~~?aid=1009771" controls="true" width="auto" height="auto"><source src="https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_polcniG1NFX9ddrB_WbUGy8P79gQxcSFbqQ78MV7NkzNDQyOQ~~?aid=1009770" type="video/mp4"/></video>
// </div><p><br></p>`;
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 `<iframe src="//player.bilibili.com/player.html?bvid=${vid}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`;
// }
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 = '<img class="icon" src="{@/img/t-icon.png}" alt="段落标题" /> <span>段落标题</span>';
h1.innerHTML = `<img class="icon" src="${valueUrl}/img/t-icon.png" alt="段落标题" /> <span>段落标题</span>`;
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 = '<img class="icon" src="{@/img/img-icon.png}" alt="图片" /> <span>图片</span>';
image.innerHTML = `<img class="icon" src="${valueUrl}/img/img-icon.png" alt="图片" /> <span>图片</span>`;
const video = toolbarRef.value.querySelector('[data-menu-key="uploadVideo"]');
const videoItem = video.parentElement;
videoItem.classList.add("toolbar-item", "flexacenter");
video.innerHTML = '<img class="icon" src="{@/img/video-icon.png}" alt="视频" /> <span>视频</span>';
video.innerHTML = `<img class="icon" src="${valueUrl}/img/video-icon.png" alt="视频" /> <span>视频</span>`;
const emotion = toolbarRef.value.querySelector('[data-menu-key="emotion"]');
const emotionItem = emotion.parentElement;
emotionItem.classList.add("toolbar-item", "flexacenter");
emotion.innerHTML = '<img class="icon" src="{@/img/emotion-icon.png}" alt="表情" /> <span>表情</span>';
emotion.innerHTML = `<img class="icon" src="${valueUrl}/img/smiling-face-round-black.png" alt="表情" /> <span>表情</span>`;
const link = toolbarRef.value.querySelector('[data-menu-key="insertLink"]');
const linkItem = link.parentElement;
linkItem.classList.add("toolbar-item", "flexacenter");
link.innerHTML = '<img class="icon" src="{@/img/link-icon.png}" alt="链接" /> <span>链接</span>';
link.innerHTML = `<img class="icon" src="${valueUrl}/img/link-icon.png" alt="链接" /> <span>链接</span>`;
const bold = toolbarRef.value.querySelector('[data-menu-key="bold"]');
const boldItem = bold.parentElement;
boldItem.classList.add("toolbar-item", "flexacenter");
bold.innerHTML = '<img class="icon" src="{@/img/bold-icon.png}" alt="加粗" /> <span>加粗</span>';
bold.innerHTML = `<img style="width: 14px;height: 14px;" class="icon" src="${valueUrl}/img/overstriking-icon.png" alt="加粗" /> <span>加粗</span>`;
const customCenter = toolbarRef.value.querySelector('[data-menu-key="customCenter"]');
const customCenterItem = customCenter.parentElement;
customCenterItem.classList.add("toolbar-item", "flexacenter");
customCenter.innerHTML = '<img class="icon" src="{@/img/justify-center-icon.png}" alt="居中" /> <span>居中</span>';
customCenter.innerHTML = `<img class="icon" src="${valueUrl}/img/between -icon.png" alt="居中" /> <span>居中</span>`;
});
};
@@ -405,7 +407,7 @@ const editApp = createApp({
let html = formattedText;
// 0. 将所有 <div> 转为 <p>, </div>转为</p>
html = html.replace(/<div>/g, "<p>");
html = html.replace(/<div(\s|>)/gi, "<p$1");
html = html.replace(/<\/div>/g, "</p>");
// 1. 还原换行符为<br>标签
@@ -424,6 +426,23 @@ const editApp = createApp({
// html = html.replace(/\[center\]([\s\S]*?)\[\/center\]/gi, '<p style="text-align: center;">$1</p>');
// console.log("html1", html);
// 5. 还原 a > [img] 的情况
html = html.replace(/<a href="([^\"]+)"[^>]*>\[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 `<img src="${image.url}?aid=${aid}" data-aid="${aid}" data-href="${href}" ${style}>`;
});
// 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 `<img src="${image.url}" data-aid="${aid}">`;
return `<img src="${image.url}?aid=${aid}" data-aid="${aid}">`;
}
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(/<img[^>]*data-aid="(\d+)"[^>]*>/gi, "[attachimg]$1[/attachimg]");
// 1. 替换图片标签优先解析src中的aid+宽高生成自定义格式再兼容原有data-aid逻辑
html = html.replace(/<img[^>]*>/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 = `<a href="${dataHrefMatch[1]}" target="_blank">${result}</a>`;
return result;
});
// 1.1 替换视频标签
// html = html.replace(/<video[^>]*aid="(\d+)"[^>]*>[\s\S]*?<\/video>/gi, "[attach]$1[/attach]");
html = html.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, (videoTag) => {
// 第一步提取video内source标签的src属性
const sourceSrcMatch = videoTag.match(/<source\s+src="([^"]+)"[^>]*>/i);
@@ -652,25 +673,17 @@ const editApp = createApp({
});
// 2. 替换H2标签
html = html.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "[b]$1[/b]");
html = html.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]");
html = html.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, "[b]$1[/b]");
// html = html.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, "[strong]$1[/strong]");
// html = html.replace(/<p[^>]*style=["'][^"']*text-align\s*:\s*center[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, "[center]$1[/center]");
// 5. 处理块级标签换行(仅<div>等块级标签前后换行,保持行内内容连续)
// 块级标签div、p、h1-h6等这里以div为例
// html = html.replace(/<\/div>\s*/gi, "</div>\n"); // 闭合div后换行
// html = html.replace(/\s*<div[^>]*>/gi, "\n<div>"); // 开启div前换行
// 3.<a href="ghj hgj gh jghj " target="_blank">ghj hgj ghj </a> 替换为 [url=ghj hgj gh jghj ]ghj hgj ghj [/url]
html = html.replace(/<a\s+href="([^"]+)"\s+target="_blank">([\s\S]*?)<\/a>/gi, (match, href, content) => {
return `[url=${href}]${content}[/url]`;
});
// 6. 处理<br>为换行
html = html.replace(/<br\s*\/?>/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 };
},
});