Files
PC-Light-Forum/js/edit.js
DESKTOP-RQ919RC\Pc 1b5cf79300 refactor(editor): 优化富文本内容转换逻辑并修复图片处理问题
- 重构 restoreHtml 和 formatContent 函数,改进 BBCode 标记与 HTML 的相互转换
- 修复 parseImageSrc 函数在失败时未返回空字符串的问题
- 改进图片宽高处理,支持百分比单位和更精确的尺寸控制
- 增强视频和附件处理逻辑,完善资源匹配机制
- 优化 HTML 结构处理,确保符合编辑器的块级元素要求
2025-11-28 19:23:01 +08:00

941 lines
41 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();
// console.log(valueA.value);
valueUrl = valueA.value.innerText;
if (location.hostname == "127.0.0.1") {
realname.value = 1;
userInfoWin.value = {
uin: 1234567890,
uid: 1234567890,
realname: "测试用户",
};
isLogin.value = true;
}
});
let imageLength = 10;
let videoLength = 5;
const checkWConfig = () => {
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
if (wConfig.time) {
const time = new Date(wConfig.time);
const now = new Date();
if (now - time > 24 * 60 * 60 * 1000) getWConfig();
else {
const config = wConfig.config || {};
titleLength.value = config.max_topic_title_length;
imageLength = config.topic_image_count || 0;
videoLength = config.topic_video_count || 0;
}
} else getWConfig();
};
const getWConfig = () => {
ajaxGet("/v2/api/config/website").then((res) => {
if (res.code == 200) {
let data = res["data"] || {};
const config = data.config || {};
titleLength.value = config.max_topic_title_length;
imageLength = config.topic_image_count || 0;
videoLength = config.topic_video_count || 0;
data.time = new Date().toISOString();
localStorage.setItem("wConfig", JSON.stringify(data));
}
});
};
let isLogin = ref(false);
let realname = ref(0); // 是否已经实名
let userInfoWin = ref({});
let permissions = ref([]);
const getUserInfoWin = () => {
const checkUser = () => {
const user = window.userInfoWin;
if (!user) return;
document.removeEventListener("getUser", checkUser);
realname.value = user.realname;
userInfoWin.value = user;
if (user?.uin > 0 || user?.uid > 0) isLogin.value = true;
permissions.value = user?.authority || [];
};
document.addEventListener("getUser", checkUser);
};
const openAttest = () => {
const handleAttestClose = () => {
document.removeEventListener("closeAttest", handleAttestClose);
realname.value = window.userInfoWin?.realname || 0;
};
// 启动认证流程时添加监听
document.addEventListener("closeAttest", handleAttestClose);
loadAttest(2);
};
// 跳转登录
const goLogin = () => {
if (typeof window === "undefined") return;
if (window["userInfoWin"] && Object.keys(window["userInfoWin"]).length !== 0) {
if (window["userInfoWin"]["uid"]) isLogin.value = true;
else ajax_login();
} else ajax_login();
};
let uConfigData = {};
const cUpload = () => {
ajaxGet(`/v2/api/config/upload?type=topic`).then((res) => {
const data = res.data;
uConfigData = data;
init();
});
};
let info = ref({});
let tagList = ref([]);
let token = ref("");
const init = () => {
ajax("/v2/api/forum/postPublishInit", {
uniqid: uniqid.value,
})
.then((res) => {
const data = res.data;
if (res.code != 200) {
creationAlertBox("error", res.message || "操作失败");
return;
}
const infoTarget = data.info || {};
// if (location.hostname == "127.0.0.1")
// infoTarget.content = `<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);
info.value = infoTarget;
token.value = data.token;
console.log("data", data);
initEditor();
})
.catch((err) => {
console.log("err", err);
});
};
let editor = null;
let toolbarRef = ref(null);
// 自定义转换视频
function customParseVideoSrc(src) {
// console.log("customParseVideoSrc", "src:", src);
// if (src.includes(".bilibili.com")) {
// // 转换 bilibili url 为 iframe (仅作为示例,不保证代码正确和完整)
// const arr = location.pathname.split("/");
// const vid = arr[arr.length - 1];
// return `<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) {
console.log("insertImage");
// if (imageNode == null) return;
const { src, alt, url, href } = imageNode;
console.log("src", src);
console.log("alt", alt);
console.log("url", url);
console.log("href", href);
},
async parseImageSrc(src) {
console.log("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);
}
},
},
["uploadImage"]: {
server: uConfigData.url,
fieldName: uConfigData.requestName,
maxFileSize: maxSize, // 1M
maxNumberOfFiles: imageLength,
allowedFileTypes: ["image/png", "image/jpeg", "image/jpg"], // .png, .jpg, .jpeg
meta: { ...uConfigData.params },
metaWithUrl: false,
headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers },
withCredentials: true,
timeout: 60 * 1000, // 15 秒
async customUpload(file, insertFn) {
try {
const img = await uploading(file, file.name, "image");
insertFn(`${img.url}?aid=${img.aid}`);
} catch (err) {
console.error("上传出错:", err);
}
},
},
["insertVideo"]: {
onInsertedVideo(videoNode) {
if (videoNode == null) return;
// console.log(videoNode);
const { src } = videoNode;
// console.log("inserted video", src);
},
parseVideoSrc: customParseVideoSrc, // 也支持 async 函数
},
["uploadVideo"]: {
server: uConfigData.url,
fieldName: uConfigData.requestName,
maxFileSize: maxSize, // 1M
maxNumberOfFiles: videoLength,
allowedFileTypes: ["video/flv", "video/mkv", "video/avi", "video/rm", "video/rmvb", "video/mpeg", "video/mpg", "video/ogg", "video/ogv", "video/mov", "video/wmv", "video/mp4", "video/webm", "video/m4v"],
meta: { ...uConfigData.params },
metaWithUrl: false,
headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers },
withCredentials: true,
timeout: 60 * 1000, // 15 秒
async customUpload(file, insertFn) {
try {
const videoUploadRes = await uploading(file, file.name, "video");
progress.value = 0;
const coverFile = await getVideoFirstFrame(file);
// console.log("第一帧提取成功", coverFile);
const coverUploadRes = await uploading(coverFile, coverFile.name, "image");
// console.log("封面上传成功", coverUploadRes);
insertFn(`${videoUploadRes.url}?aid=${videoUploadRes.aid}`, `${coverUploadRes.url}?aid=${coverUploadRes.aid}`);
} catch (err) {
console.error("上传出错:", err);
progress.value = 0;
}
},
},
},
onChange(editor) {
const html = editor.getHtml();
console.log("edior content", html);
},
hoverbarKeys: { text: { menuKeys: [] }, video: { menuKeys: [] } },
};
editor = createEditor({
selector: "#editor-container",
html: infoTarget.content,
config: editorConfig,
mode: "default",
});
const toolbarConfig = {
toolbarKeys: [
"header1",
{
key: "group-image",
title: "图片",
menuKeys: ["insertImage", "uploadImage"],
},
"uploadVideo",
"emotion",
"insertLink",
"bold",
],
};
const menu1Conf = {
key: "customCenter", // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new MyButtonMenu(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
Boot.registerMenu(menu1Conf);
toolbarConfig.insertKeys = {
index: 7, // 插入的位置,基于当前的 toolbarKeys
keys: ["customCenter"],
};
const toolbar = createToolbar({
editor,
selector: "#toolbar-container",
config: toolbarConfig,
mode: "default",
});
nextTick(() => {
const h1 = toolbarRef.value.querySelector('[data-menu-key="header1"]');
const h1Item = h1.parentElement;
h1Item.classList.add("toolbar-item", "flexacenter");
h1.innerHTML = `<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(/\n/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>');
// 定义图片处理函数
const 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;
});
// 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 children = Array.from(__c.childNodes);
let wrapper = null;
children.forEach((node) => {
// 判断是否为块级元素
const isBlock = node.nodeType === 1 && blockTags.includes(node.tagName);
if (isBlock) {
wrapper = null; // 遇到块级元素,重置包裹容器
} else {
// 如果是行内元素文本、图片、span、strong等放入 p 标签
// 忽略纯空白文本节点(如果它们在块级元素之间),避免产生过多的空段落
// 但为了保险起见(避免吞掉有意义的空格),这里暂时全部包裹
if (!wrapper) {
wrapper = document.createElement('p');
__c.insertBefore(wrapper, node);
}
wrapper.appendChild(node);
}
});
html = __c.innerHTML;
console.log("初始化显示的html", html);
return html;
};
onMounted(() => {
// setTimeout(() => focusLastNode(), 1000);
// document.addEventListener("keydown", handleUndoKeydown);
});
const editorRef = ref(null);
let loading = ref(false);
const maxSize = 20 * 1024 * 1024; // 20MB
const cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1);
const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵‍💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]);
// 提交
const submit = (status) => {
if (realname.value == 0 && userInfoWin.value?.uin > 0) {
openAttest();
return;
}
if (!isLogin.value) {
goLogin();
return;
}
const infoTarget = { ...info.value } || {};
let content = editor.getHtml();
const images = extractImages(editorRef.value);
const videos = extractVideos(editorRef.value);
infoTarget.attachments = infoTarget.attachments || {};
infoTarget.attachments.images = images;
infoTarget.attachments.videos = videos;
info.value["attachments"] = info.value["attachments"] || {};
info.value["attachments"]["images"] = images;
info.value["attachments"]["videos"] = videos;
console.log("原始html", content);
content = formatContent(content);
console.log("最终html", content);
const data = {
...infoTarget,
content,
};
console.log("data", data);
if (location.hostname == "127.0.0.1") {
status = 0;
}
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) => {
if (!html) return "";
// 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]
html = html.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "[p]$1[/p]");
// 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 = "";
// 尝试从 src 中获取 aid
const srcMatch = imgTag.match(/src="([^"]+)"/i);
if (srcMatch) {
const aidMatch = srcMatch[1].match(/[?&]aid=(\d+)/);
if (aidMatch) aid = aidMatch[1];
}
// 尝试从 data-aid 中获取 aid
if (!aid) {
const dataAid = imgTag.match(/data-aid="(\d+)"/i);
if (dataAid) aid = dataAid[1];
}
if (!aid) return ""; // 无法获取 aid跳过
// 获取宽高 (支持 px 和 %)
let w = 0,
h = 0;
const styleMatch = imgTag.match(/style="([^"]+)"/i);
if (styleMatch) {
// 匹配数字+单位 (px或%)
const wMatch = styleMatch[1].match(/width:\s*([\d.]+(?:px|%)?)/i);
const hMatch = styleMatch[1].match(/height:\s*([\d.]+(?:px|%)?)/i);
if (wMatch) {
// 如果是百分比,直接保留字符串;如果是纯数字默认视为 px如果是 px 去掉单位
let val = wMatch[1];
if (val.endsWith('%')) w = val; // 保留百分比字符串
else w = parseFloat(val); // 转为数字 (px)
}
if (hMatch) {
let val = hMatch[1];
if (val.endsWith('%')) h = val;
else h = parseFloat(val);
}
}
// 兼容 width/height 属性 (通常只有数字)
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]);
}
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)}]${aid}[/img]`;
} else {
result = `[img]${aid}[/img]`;
}
// 处理 data-href包裹在 [url] 中
const hrefMatch = imgTag.match(/data-href="([^"]+)"/i);
if (hrefMatch && hrefMatch[1]) {
result = `[url=${hrefMatch[1]}]${result}[/url]`;
}
return result;
});
// 5. 处理视频 [attach]
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(/<\/(p|div)>/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 html.trim();
};
const extractImages = (dom) => {
const images = [];
// 直接查找页面中所有带 data-aid 的 img 标签
const imgElements = dom.querySelectorAll("img");
imgElements.forEach((imgEl) => {
let url = imgEl.getAttribute("src")?.trim() || "";
const urlObj = new URL(url);
const aid = urlObj.searchParams.get("aid");
const queryIndex = url.indexOf("?");
const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
images.push({
url: cleanUrl,
aid: Number(aid),
});
});
console.log("提取完成的图片列表:", images);
return images;
};
const extractVideos = (dom) => {
// 1. 查找页面中所有 <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,
});
});
console.log("提取完成的视频列表:", result);
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);
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);
}
})
.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");