Files
PC-Light-Forum/js/edit.js
DESKTOP-RQ919RC\Pc 40d83d5374 feat: 优化响应式布局并添加签到组件
refactor: 重构CSS和LESS文件以支持响应式设计
fix: 修复图片上传和编辑器解析问题
style: 调整搜索框和日历组件的样式
docs: 更新HTML模板中的广告和操作链接
2025-12-04 19:15:08 +08:00

1017 lines
44 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 简单版本的论坛编辑器,确保图片插入功能正常
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();
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 || {};
// 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;
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 `<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 || {};
const editorConfig = {
placeholder: "输入正文",
enabledMenus: [],
MENU_CONF: {
["emotion"]: {
emotions: optionEmoji.value,
},
["insertImage"]: {
onInsertedImage(imageNode) {
const { src, alt, url, href } = imageNode;
},
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 || "操作失败");
return "";
}
} catch (e) {
console.error("Transform network image failed", e);
return "";
}
},
checkImage: (src, alt, url) => {
if (src.indexOf("http") !== 0) {
return "图片网址必须以 http/https 开头";
}
return true;
}, // 也支持 async 函数
},
["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");
console.log("img", img);
insertFn(`${img.url}?aid=${img.aid}`);
} catch (err) {
console.error("上传出错:", err);
}
},
},
["insertVideo"]: {
onInsertedVideo(videoNode) {
if (videoNode == null) return;
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,
// html: "",
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 = `<img class="icon" src="${valueUrl}/img/t-icon.png" alt="段落标题" /> <span>段落标题</span>`;
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="${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="${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="${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="${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 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="${valueUrl}/img/between -icon.png" alt="居中" /> <span>居中</span>`;
});
};
const restoreHtml = (formattedText, attachments) => {
const imageList = attachments?.images || [];
const filesList = attachments?.files || [];
const videosList = attachments?.videos || [];
let html = formattedText || "";
// 0.5 [p] -> <p>
// html = html.replace(/\[p\]([\s\S]*?)\[\/p\]/gi, "<p>$1</p>");
// 0. 基础清理:换行符转 <br>div 转 p (保持原有逻辑)
html = html.replace(/(\r\n|\n|\r)/g, "<br>");
html = html.replace(/<div(\s|>)/gi, "<p$1");
html = html.replace(/<\/div>/g, "</p>");
html = html.replace(/<br><div>/g, "<div>");
html = html.replace(/<\/div><br>/g, "</div>");
html = html.replace(/<br><p>/g, "<p>");
html = html.replace(/<\/p><br>/g, "</p>");
// 1. [section] -> h1
html = html.replace(/\[section\]([\s\S]*?)\[\/section\]/gi, "<h1>$1</h1>");
// 2. [b] -> strong
html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "<strong>$1</strong>");
// 3. [align=center]
// 特殊处理嵌套在 h1 中的居中
html = html.replace(/<h1>\s*\[align=center\]([\s\S]*?)\[\/align\]\s*<\/h1>/gi, '<h1 style="text-align: center;">$1</h1>');
// 普通居中
html = html.replace(/\[align=center\]([\s\S]*?)\[\/align\]/gi, '<p style="text-align: center;">$1</p>');
// 移除 align 后紧跟的换行符(因为 align 转换为了块级元素 p/h1其后的换行符通常是多余的
html = html.replace(/(<p style="text-align: center;">.*?<\/p>)\s*<br>/gi, "$1");
html = html.replace(/(<h1 style="text-align: center;">.*?<\/h1>)\s*<br>/gi, "$1");
// 定义图片处理函数
const processImg = (aid, width, height, href) => {
console.log("processImg", aid, width, height, href);
const image = imageList.find((img) => String(img.aid) === String(aid));
if (!image) return "";
// 移除已使用的图片
const index = imageList.findIndex((img) => String(img.aid) === String(aid));
if (index > -1) imageList.splice(index, 1);
let style = "";
const formatStyleVal = (val) => {
if (!val) return null;
if (String(val).endsWith("%")) return val;
const num = Number(val);
return num > 0 ? `${num}px` : null;
};
const wStyle = formatStyleVal(width);
const hStyle = formatStyleVal(height);
let styleParts = [];
if (wStyle) styleParts.push(`width: ${wStyle}`);
if (hStyle) styleParts.push(`height: ${hStyle}`);
if (styleParts.length > 0) style = `style="${styleParts.join("; ")};"`;
let dataHref = href ? ` data-href="${href}"` : "";
return `<img src="${image.url}?aid=${aid}" data-aid="${aid}"${dataHref} ${style}>`;
};
// 4. [url] + [img] 组合 (带链接的图片)
// 匹配 [url=...][img...]...[/img][/url]
html = html.replace(/\[url=([^\]]+)\]\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\](\d+)\[\/img\]\[\/url\]/gi, (match, href, w, h, aid) => {
const imgTag = processImg(aid, w, h, href);
return imgTag || match;
});
// 5. 单独 [img]
html = html.replace(/\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\](\d+)\[\/img\]/gi, (match, w, h, aid) => {
const imgTag = processImg(aid, w, h, null);
return imgTag || match;
});
html = html.replace(/\[url=([^\]]+)\]\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\]([^\]]+)\[\/img\]\[\/url\]/gi, (match, href, w, h, inner) => {
if (/^\d+$/.test(inner)) return match;
const formatStyleVal = (val) => {
if (!val) return null;
if (String(val).endsWith("%")) return val;
const num = Number(val);
return num > 0 ? `${num}px` : null;
};
const wStyle = formatStyleVal(w);
const hStyle = formatStyleVal(h);
let styleParts = [];
if (wStyle) styleParts.push(`width: ${wStyle}`);
if (hStyle) styleParts.push(`height: ${hStyle}`);
const styleAttr = styleParts.length > 0 ? ` style="${styleParts.join("; ")};"` : "";
const dataHref = href ? ` data-href="${href}"` : "";
return `<img src="${inner}"${dataHref}${styleAttr}>`;
});
html = html.replace(/\[img(?:=([0-9.%]+)(?:,([0-9.%]+))?)?\]([^\]]+)\[\/img\]/gi, (match, w, h, inner) => {
if (/^\d+$/.test(inner)) return match;
const formatStyleVal = (val) => {
if (!val) return null;
if (String(val).endsWith("%")) return val;
const num = Number(val);
return num > 0 ? `${num}px` : null;
};
const wStyle = formatStyleVal(w);
const hStyle = formatStyleVal(h);
let styleParts = [];
if (wStyle) styleParts.push(`width: ${wStyle}`);
if (hStyle) styleParts.push(`height: ${hStyle}`);
const styleAttr = styleParts.length > 0 ? ` style="${styleParts.join("; ")};"` : "";
return `<img src="${inner}"${styleAttr}>`;
});
// 6. [attachimg] (兼容旧格式)
html = html.replace(/\[attachimg\](\d+)\[\/attachimg\]/gi, (match, aid) => {
const imgTag = processImg(aid, 0, 0, null);
return imgTag || match;
});
// 7. [attach] (图片、视频或附件)
html = html.replace(/\[attach\](\d+)\[\/attach\]/gi, (match, aid) => {
// 尝试匹配视频
const video = videosList.find((v) => String(v.aid) === String(aid));
if (video) {
const index = videosList.findIndex((v) => String(v.aid) === String(aid));
if (index > -1) videosList.splice(index, 1);
return `<p><video src="${video.url}?aid=${video.aid}" preload="none" poster="${video.posterurl}?aid=${video.posterid}" controls></video></p>`;
}
// 尝试匹配图片
const image = imageList.find((img) => String(img.aid) === String(aid));
if (image) {
const index = imageList.findIndex((img) => String(img.aid) === String(aid));
if (index > -1) imageList.splice(index, 1);
return `<img src="${image.url}?aid=${image.aid}"><br/>`;
}
// 尝试匹配文件
const file = filesList.find((f) => String(f.aid) === String(aid));
if (file) {
return `<a href="${file.url}?aid=${aid}" target="_blank">附件: ${file.name || "Download"}</a>`;
}
return match;
});
// 8. [url] (普通链接)
html = html.replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, '<a href="$1" target="_blank">$2</a>');
// 9. 剩余资源追加
imageList.forEach((element) => (html += `<img src="${element.url}?aid=${element.aid}"><br/>`));
videosList.forEach((element) => (html += `<video src="${element.url}?aid=${element.aid}" preload="none" poster="${element.posterurl}?aid=${element.posterid}" controls></video><br/>`));
// 10. 最终 HTML 结构包裹
const __c = document.createElement("div");
__c.innerHTML = html;
// 确保所有顶层元素都是块级元素 (Slate/WangEditor 要求)
const blockTags = ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "UL", "OL", "PRE", "TABLE", "FIGURE", "HR"];
const newContainer = document.createElement("div");
let currentInlineNodes = [];
let lastWasBlock = false;
const flushInlines = () => {
if (currentInlineNodes.length > 0) {
if (currentInlineNodes.length > 0) {
const p = document.createElement("p");
currentInlineNodes.forEach((node) => p.appendChild(node));
newContainer.appendChild(p);
}
currentInlineNodes = [];
}
};
Array.from(__c.childNodes).forEach((node) => {
// 判断是否为块级元素
const isBlock = node.nodeType === 1 && blockTags.includes(node.tagName);
if (isBlock) {
flushInlines();
newContainer.appendChild(node);
// 记录最后添加的是块级元素
lastWasBlock = true;
} else if (node.nodeName === "BR") {
if (currentInlineNodes.length > 0) {
flushInlines();
lastWasBlock = false; // 刚刚结束了一个段落,不算紧挨着块级
} else {
// 如果前面紧挨着块级元素,忽略这个 BR避免块级元素后的换行产生空行
if (lastWasBlock) {
// 忽略
lastWasBlock = false; // 消耗掉块级后的换行状态,避免连续 BR 被吞
} else {
const p = document.createElement("p");
p.innerHTML = "<br>";
newContainer.appendChild(p);
lastWasBlock = false;
}
}
} else {
// 过滤掉块级元素之间或开头的纯空白文本节点,避免产生空的 P 标签
if (node.nodeType === 3 && !node.textContent.trim()) {
if (currentInlineNodes.length > 0) {
currentInlineNodes.push(node);
}
} else {
currentInlineNodes.push(node);
}
lastWasBlock = false;
}
});
flushInlines();
html = newContainer.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 (location.hostname == "127.0.0.1") status = 0;
if (realname.value == 0 && userInfoWin.value?.uin > 0) {
openAttest();
return;
}
if (!isLogin.value) {
goLogin();
return;
}
const infoTarget = { ...info.value } || {};
let content = editor.getHtml();
const obj = formatContent(content);
content = obj.content;
const images = extractImages(editorRef.value);
// const images = obj.images;
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;
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) => {
let images = [];
if (!html) return "";
// <p><br></p> 转换为单个换行符
html = html.replace(/<p><br><\/p>/gi, "\n");
// 1. 处理居中 [align=center] (p, div, h1-h6)
html = html.replace(/<(p|div|h[1-6])[^>]*style="[^"]*text-align:\s*center;[^"]*"[^>]*>([\s\S]*?)<\/\1>/gi, (match, tag, content) => {
if (tag.toLowerCase() === "h1") {
// h1 特殊处理:保留标签,内容居中,供后续转 [section]
return `<h1>[align=center]${content}[/align]</h1>`;
}
return `[align=center]${content}[/align]`;
});
// 2. 处理章节 [section] (对应 h1)
html = html.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "[section]$1[/section]");
// 2.5 处理段落 [p]
// 优先处理空段落 <p><br></p>,将其替换为单个换行符,避免后续双重换行
html = html.replace(/<p[^>]*>\s*<br\s*\/?>\s*<\/p>/gi, "\n");
// 处理普通段落结束符
html = html.replace(/<\/p>/gi, "\n");
// 3. 处理加粗 [b] (对应 strong, b)
html = html.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "[b]$2[/b]");
// 4. 处理图片 [img]
html = html.replace(/<img[^>]*>/gi, (imgTag) => {
let aid = "";
const srcMatch = imgTag.match(/src="([^"]+)"/i);
if (!srcMatch) return "";
const srcUrl = srcMatch[1];
const cleanUrl = srcUrl.split("?")[0];
const aidMatch = srcUrl.match(/[?&]aid=(\d+)/);
if (aidMatch) aid = aidMatch[1];
let w = 0,
h = 0;
const styleMatch = imgTag.match(/style="([^"]+)"/i);
if (styleMatch) {
const wMatch = styleMatch[1].match(/width:\s*([\d.]+(?:px|%)?)/i);
const hMatch = styleMatch[1].match(/height:\s*([\d.]+(?:px|%)?)/i);
if (wMatch) {
let val = wMatch[1];
if (val.endsWith("%")) w = val;
else w = parseFloat(val);
}
if (hMatch) {
let val = hMatch[1];
if (val.endsWith("%")) h = val;
else h = parseFloat(val);
}
}
if (!w) {
const wAttr = imgTag.match(/\swidth="(\d+)"/i);
if (wAttr) w = Number(wAttr[1]);
}
if (!h) {
const hAttr = imgTag.match(/\sheight="(\d+)"/i);
if (hAttr) h = Number(hAttr[1]);
}
const inner = aid ? aid : cleanUrl;
let result = "";
if (w || h) {
const formatVal = (val) => {
if (typeof val === "string" && val.endsWith("%")) return val;
return val ? parseFloat(Number(val).toFixed(2)) : 0;
};
result = `[img=${formatVal(w)},${formatVal(h)}]${inner}[/img]`;
if (aid) {
images.push({ url: cleanUrl, aid: Number(aid) });
} else {
images.push({ url: cleanUrl });
}
} else {
result = `[img]${inner}[/img]`;
if (aid) {
images.push({ url: cleanUrl, aid: Number(aid) });
} else {
images.push({ url: cleanUrl });
}
}
const hrefMatch = imgTag.match(/data-href="([^"]+)"/i);
if (hrefMatch && hrefMatch[1]) result = `[url=${hrefMatch[1]}]${result}[/url]`;
return result;
});
// 5. 处理视频 [attach]
html = html.replace(/<div[^>]*data-w-e-type="video"[^>]*>([\s\S]*?)<\/div>/gi, (match, content) => {
return content.trim(); // 去掉包裹视频的 div并去除首尾空白防止产生额外换行
});
html = html.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, (videoTag) => {
let aid = "";
const dataAid = videoTag.match(/data-aid="(\d+)"/i);
if (dataAid) aid = dataAid[1];
if (!aid) {
const srcMatch = videoTag.match(/src="([^"]+)"/i);
if (srcMatch) {
const aidMatch = srcMatch[1].match(/[?&]aid=(\d+)/);
if (aidMatch) aid = aidMatch[1];
}
}
return aid ? `[attach]${aid}[/attach]` : "";
});
// 6. 处理链接 [url] 和 附件下载 [attach]
html = html.replace(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (match, href, content) => {
// 尝试提取 aid
const aidMatch = href.match(/[?&]aid=(\d+)/);
// 如果是下载链接(包含 download 属性 或 明确是附件)且有 aid转为 [attach]
if (match.includes("download=") && aidMatch) {
return `[attach]${aidMatch[1]}[/attach]`;
}
// 普通链接
return `[url=${href}]${content}[/url]`;
});
// 7. 换行处理
html = html.replace(/<br\s*\/?>/gi, "\n");
html = html.replace(/<\/(div)>/gi, "\n"); // 块级元素结束视为换行
// 8. 清理剩余标签和解码
html = html.replace(/<[^>]+>/g, ""); // 移除所有剩余标签
// 简单的 HTML 实体解码
const entities = {
"&nbsp;": " ",
"&lt;": "<",
"&gt;": ">",
"&amp;": "&",
"&quot;": '"',
"&apos;": "'",
};
html = html.replace(/&[a-z]+;/gi, (match) => entities[match] || match);
return {
content: html.trim(),
images,
};
};
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;
if (Number(aid)) {
images.push({
url: cleanUrl,
aid: Number(aid),
});
}
});
return images;
};
const extractVideos = (dom) => {
// 1. 查找页面中所有 <video> 节点(返回 NodeList 集合)
const videoElements = dom.querySelectorAll("video");
const result = [];
// 2. 遍历每个 video 节点,直接获取属性
videoElements.forEach((videoEl) => {
const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 视频地址
// 1. 用 URL 构造函数解析链接(自动处理查询参数)
const urlObj = new URL(posterurl);
// 2. 获取 aid 参数get 方法找不到时返回 null
const posterid = urlObj.searchParams.get("aid");
const sourceEl = videoEl.querySelector("source");
const url = sourceEl.getAttribute("src") || null;
const obj = new URL(url);
// 2. 获取 aid 参数get 方法找不到时返回 null
const aid = obj.searchParams.get("aid");
const queryIndex = url.indexOf("?");
const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
const queryIndex2 = posterurl.indexOf("?");
const cleanPosterurl = queryIndex2 !== -1 ? posterurl.substring(0, queryIndex2) : posterurl;
result.push({
aid: Number(aid),
posterid: Number(posterid),
url: cleanUrl,
posterurl: cleanPosterurl,
});
});
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);
if (config.params && config.params.data) {
formData.append("data", config.params.data);
}
axios
.post(config.url, formData, {
headers: {
...config.headers,
"Content-Type": "multipart/form-data",
},
onUploadProgress: (e) => {
progress.value = Math.round((e.loaded / e.total) * 100);
},
withCredentials: true,
})
.then((response) => {
const res = response.data;
if (res.code == 200) {
resolve(res.data);
} else {
creationAlertBox("error", res.message || "上传失败");
reject(res);
}
})
.catch((error) => {
if (error.response) {
creationAlertBox("error", `HTTP错误: ${error.response.status}`);
} else if (error.request) {
creationAlertBox("error", "网络错误");
} else {
creationAlertBox("error", "请求设置错误");
}
reject(error);
});
});
};
// 封装:提取视频第一帧(返回 Promiseresolve 第一帧 Blob
const getVideoFirstFrame = (videoFile) => {
return new Promise((resolve, reject) => {
if (!(videoFile instanceof File) || !videoFile.type.startsWith("video/")) {
reject(new Error("请传入合法的视频文件"));
return;
}
const objectUrl = URL.createObjectURL(videoFile);
const video = document.createElement("video");
video.src = objectUrl;
video.preload = "auto";
video.muted = true;
video.playsInline = true;
const cleanup = () => {
URL.revokeObjectURL(objectUrl);
video.src = "";
};
video.addEventListener(
"error",
() => {
cleanup();
reject(new Error("视频加载失败,请检查文件完整性"));
},
{ once: true }
);
video.addEventListener(
"loadeddata",
() => {
const canvas = document.createElement("canvas");
const w = video.videoWidth || 320;
const h = video.videoHeight || 240;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
try {
ctx.drawImage(video, 0, 0, w, h);
} catch (e) {
cleanup();
reject(e);
return;
}
canvas.toBlob(
(blob) => {
cleanup();
if (!blob) {
reject(new Error("第一帧提取失败Blob 生成异常"));
return;
}
const frameFile = new File([blob], `video_cover_${Date.now()}.png`, { type: "image/png" });
resolve(frameFile);
},
"image/png",
0.9
);
},
{ once: true }
);
});
};
return { progress, valueA, toolbarRef, uniqid, userInfoWin, titleLength, submit, info, tagList, token, cutAnonymity, editorRef };
},
});
editApp.component("headTop", headTop);
editApp.mount("#edit");