Files
PC-Light-Forum/js/edit.js
DESKTOP-RQ919RC\Pc 2a227a806d fix(editor): 修复编辑器样式和功能问题
修复编辑器容器高度设置问题,统一h2标签为h1
调整图片和视频的显示样式,修复表格背景色
优化编辑器工具栏功能,修复链接插入逻辑
2025-11-27 19:23:49 +08:00

833 lines
37 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;
import { headTop } from "../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("");
onMounted(() => {
const params = getUrlParams();
uniqid.value = params.uniqid || "";
getUserInfoWin();
checkWConfig();
cUpload();
});
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) => {
// 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 || {};
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);
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;
},
},
["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 秒
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
});
} catch (err) {
console.error("上传出错:", err);
}
},
},
["insertVideo"]: {
onInsertedVideo(videoNode) {
if (videoNode == null) return;
// console.log(videoNode);
const { src } = videoNode;
// console.log("inserted video", src);
},
},
["uploadVideo"]: {
server: uConfigData.url,
fieldName: uConfigData.requestName,
maxFileSize: maxSize, // 1M
maxNumberOfFiles: videoLength,
allowedFileTypes: ["video/*"],
meta: { ...uConfigData.params },
metaWithUrl: false,
headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers },
withCredentials: true,
timeout: 15 * 1000, // 15 秒
async customUpload(file, insertFn) {
try {
const videoUploadRes = await uploading(file, file.name, "video");
const coverFile = await getVideoFirstFrame(file);
console.log("第一帧提取成功", coverFile);
// 步骤3再上传第一帧封面type 传 'cover',按后端要求调整)
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);
}
},
},
},
onChange(editor) {
const html = editor.getHtml();
console.log("edior content", html);
},
hoverbarKeys: { text: { menuKeys: [] }, video: { menuKeys: [] } },
};
// html: infoTarget.content,
editor = createEditor({
selector: "#editor-container",
html: infoTarget.content,
config: editorConfig,
mode: "default",
});
const toolbarConfig = {
toolbarKeys: ["header1", "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="{@/img/t-icon.png}" alt="段落标题" /> <span>段落标题</span>';
const image = toolbarRef.value.querySelector('[data-menu-key="uploadImage"]');
const imageItem = image.parentElement;
imageItem.classList.add("toolbar-item", "flexacenter");
image.innerHTML = '<img class="icon" src="{@/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>';
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>';
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>';
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>';
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>';
});
};
const restoreHtml = (formattedText, attachments) => {
const imageList = attachments?.images || [];
const filesList = attachments?.files || [];
const videosList = attachments?.videos || [];
let html = formattedText;
// 0. 将所有 <div> 转为 <p>, </div>转为</p>
html = html.replace(/<div>/g, "<p>");
html = html.replace(/<\/div>/g, "</p>");
// 1. 还原换行符为<br>标签
html = html.replace(/\n/g, "<br>");
// 2. 还原块级标签的换行标记
html = html.replace(/<br><div>/g, "<div>");
html = html.replace(/<\/div><br>/g, "</div>");
// 3. 还原标签标记为span.blue
// html = html.replace(/\[tag\]([^[]+)\[\/tag\]/gi, '<span class="blue">#$1</span> <span class="fill"></span> ');
// 4. 还原粗体标记为h2标签
html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "<h1>$1</h1>");
// html = html.replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, "<strong>$1</strong>");
// html = html.replace(/\[center\]([\s\S]*?)\[\/center\]/gi, '<p style="text-align: center;">$1</p>');
// console.log("html1", html);
// 5. 还原【新增图片格式】[img=width,height]aid[/img] 或 [img]aid[/img]
html = html.replace(/\[img(?:=([0-9]+(?:\.[0-9]+)?)(?:,([0-9]+(?:\.[0-9]+)?))?)?\](\d+)\[\/img\]/gi, (match, width, height, aid) => {
const image = imageList.find((img) => String(img.aid) === String(aid)); // 统一字符串比较,避免类型问题
if (!image) return match;
// 从列表中移除已匹配的图片(避免重复使用)
const index = imageList.findIndex((img) => String(img.aid) === String(aid));
if (index > -1) imageList.splice(index, 1);
// 拼接img标签带宽高样式宽高为0则不设置
let style = "";
const w = width ? Number(width) : 0;
const h = height ? Number(height) : 0;
if (w > 0 && h > 0) style = `style="width: ${w}px; height: ${h}px;"`;
else if (w > 0) style = `style="width: ${w}px;"`;
return `<img src="${image.url}?aid=${aid}" data-aid="${aid}" ${style}>`;
});
// console.log("html2", html);
// 5. 还原图片标记为img标签使用提供的imageList
html = html.replace(/\[attachimg\](\d+)\[\/attachimg\]/gi, (match, aid) => {
// 查找对应的图片信息
const image = imageList.find((img) => img.aid == aid);
if (image) {
imageList.splice(imageList.indexOf(image), 1);
return `<img src="${image.url}" data-aid="${aid}">`;
}
return match; // 未找到对应图片时保留原始标记
});
html = html.replace(/\[attach\](\d+)\[\/attach\]/gi, (match, aid) => {
// 查找对应的图片信息
const image = imageList.find((img) => img.aid == aid);
if (image) {
imageList.splice(imageList.indexOf(image), 1);
return `<img src="${image.url}?aid=${aid}"><br/>`;
}
// 查找对应的视频信息
const video = videosList.find((v) => v.aid == aid);
if (video) {
console.log(video);
videosList.splice(videosList.indexOf(video), 1);
return `<p><video src="${video.url}?aid=${video.aid}" preload="none" poster="${video.posterurl}?aid=${video.posterid}" controls></video></p>`;
}
return match; // 未找到对应图片时保留原始标记
});
// 6. 还原填充标签
// html = html.replace(/(<span class="blue">[^<]+<\/span>)\s+/gi, '$1 <span class="fill"></span> ');
// 7. 清理多余的<br>标签
// html = html.replace(/<br><br>/g, "<br>");
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/>`));
const __c = document.createElement("div");
__c.innerHTML = html;
Array.from(__c.querySelectorAll("img")).forEach((im) => {
const p = im.parentElement;
if (!p || p.tagName !== "P") {
const wrap = document.createElement("p");
p ? p.insertBefore(wrap, im) : __c.appendChild(wrap);
wrap.appendChild(im);
}
});
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);
}
});
html = __c.innerHTML;
console.log("html3", 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(content);
content = formatContent(content);
console.log(content);
const data = {
...infoTarget,
content,
};
console.log("data", data);
// return
ajax("/v2/api/forum/postPublishTopic", {
info: data,
token: token.value,
status,
}).then((res) => {
const data = res.data;
if (res.code != 200) {
creationAlertBox("error", res.message);
return;
}
creationAlertBox("success", res.message || "操作成功");
const back = () => {
if (status == 1) redirectToExternalWebsite("/details/" + data.uniqid, "_self");
else redirectToExternalWebsite("/", "_self");
};
setTimeout(() => back(), 1500);
});
};
const formatContent = (html) => {
// 1. 替换图片标签
// html = html.replace(/<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);
let aid = "";
if (srcMatch && srcMatch[1]) {
const aidMatch = srcMatch[1].match(/aid=(\d+)/);
if (aidMatch) aid = aidMatch[1];
}
if (!aid) {
const dataAidMatch = imgTag.match(/data-aid="(\d+)"/i);
if (dataAidMatch) aid = dataAidMatch[1];
}
if (!aid) return imgTag;
const styleMatch = imgTag.match(/style="([^"]+)"/i);
let width = 0,
height = 0;
if (styleMatch && styleMatch[1]) {
const widthMatch = styleMatch[1].match(/width:\s*(\d+(?:\.\d+)?)px/i);
const heightMatch = styleMatch[1].match(/height:\s*(\d+(?:\.\d+)?)px/i);
width = widthMatch ? Number(widthMatch[1]) : 0;
height = heightMatch ? Number(heightMatch[1]) : 0;
if (!width) {
const widthPctMatch = styleMatch[1].match(/width:\s*(\d+(?:\.\d+)?)%/i);
if (widthPctMatch) {
const el = (editorRef && editorRef.value) || document.querySelector("#editor-container");
const boxW = el ? el.getBoundingClientRect().width || el.clientWidth || 0 : 0;
const pct = Number(widthPctMatch[1]);
if (boxW && pct > 0) width = Math.round((pct / 100) * boxW);
}
}
}
console.log("width", width, "height", height);
// 第四步:按规则生成格式
if (width == 0 && height == 0) return `[img]${aid}[/img]`;
else return `[img=${width}${height ? "," + height : ""}]${aid}[/img]`;
});
// 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);
let aid = "";
if (sourceSrcMatch && sourceSrcMatch[1]) {
// 从source的src中提取aid
const aidMatch = sourceSrcMatch[1].match(/aid=(\d+)/);
if (aidMatch) aid = aidMatch[1];
}
// 第二步兼容原有video标签的aid属性如果source中没有取video的aid
if (!aid) {
const videoAidMatch = videoTag.match(/aid="(\d+)"/i);
if (videoAidMatch) aid = videoAidMatch[1];
}
// 无aid则返回原标签有则生成指定格式
return aid ? `[attach]${aid}[/attach]` : "";
});
// 2. 替换H2标签
html = html.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/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前换行
// 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();
return html;
};
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 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); // 文件名
ajax(config.url, formData)
.then((res) => {
const data = res.data;
try {
resolve(data);
} catch (error) {
console.error("插入图片出错:", error);
}
})
.finally(() => {
loading.value = false;
});
});
};
// 封装:提取视频第一帧(返回 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 { toolbarRef, uniqid, userInfoWin, titleLength, submit, info, tagList, token, cutAnonymity, editorRef };
},
});
editApp.component("headTop", headTop);
editApp.mount("#edit");