// 简单版本的论坛编辑器,确保图片插入功能正常 const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; const { createEditor, createToolbar, SlateTransforms, Boot, SlateEditor } = window.wangEditor; const editApp = createApp({ setup() { const E = window.wangEditor; const LANG = location.href.indexOf("lang=en") > 0 ? "en" : "zh-CN"; E.i18nChangeLanguage(LANG); const title = ref(""); const saveStatus = ref(""); const uniqid = ref(""); const info = ref({}); const token = ref(""); let editor = null; let toolbar = null; const draftKey = "publish_admin_draft"; let uConfigData = {}; let imageLength = 10; let videoLength = 5; const formatTime = (d) => { const pad = (n) => String(n).padStart(2, "0"); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const extractImages = (dom) => { const images = []; 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) => { const videoElements = dom.querySelectorAll("video"); const result = []; videoElements.forEach((videoEl) => { const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 视频地址 const urlObj = new URL(posterurl); const posterid = urlObj.searchParams.get("aid"); const sourceEl = videoEl.querySelector("source"); const url = sourceEl.getAttribute("src") || null; const obj = new URL(url); 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 cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1); // 提交 const submit = (status) => { const infoTarget = { ...info.value } || {}; let content = editor.getHtml(); const editorDom = document.getElementById("editor-text-area"); const images = extractImages(editorDom); const videos = extractVideos(editorDom); 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; infoTarget.title = title.value; const data = { ...infoTarget, content, }; ajax("/v2/api/forum/postPublishTopic", { info: data, token: token.value, status, htmledit: 1, }).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 cUpload = () => { ajaxGet(`/v2/api/config/upload?type=topic`).then((res) => { const data = res.data; uConfigData = data; }); }; const init = () => { ajax("/v2/api/forum/postPublishInit", { uniqid: uniqid.value, htmledit: 1, }) .then((res) => { const data = res.data; if (res.code != 200) { creationAlertBox("error", res.message || "操作失败"); return; } const infoTarget = data.info || {}; // if (infoTarget.content) infoTarget.content = `
2026年度研究生课程火热招生中!
\n2026年度研究生课程火热招生中!
` info.value = infoTarget; token.value = data.token; if (infoTarget.title) title.value = infoTarget.title; nextTick(() => { initEditor(); }); }) .catch((err) => { console.log("err", err); }); }; // 上传图片/视频 获取url const uploading = (file, name, type) => { return new Promise((resolve, reject) => { const upload = () => { let config = uConfigData; const formData = new FormData(); formData.append(config.requestName, file); // 文件数据 formData.append("name", name); // 文件名 formData.append("type", type); // 文件名 if (config.params && config.params.data) { formData.append("data", config.params.data); } const xhr = new XMLHttpRequest(); xhr.open("POST", config.url, true); xhr.withCredentials = true; // 允许携带 Cookie // 监听上传进度 xhr.upload.onprogress = function (event) { if (event.lengthComputable) { // const percentComplete = (event.loaded / event.total) * 100; // progress.value = Math.round(percentComplete); } }; xhr.onload = function () { if (xhr.status === 200) { const res = JSON.parse(xhr.responseText); if (res.code == 200) { const data = res.data; resolve(data); } else { creationAlertBox("error", res.message || "上传失败"); reject(res); } } else { creationAlertBox("error", "上传失败"); reject(new Error("Upload failed")); } }; xhr.onerror = function () { creationAlertBox("error", "网络错误,上传失败"); reject(new Error("Network error")); }; xhr.send(formData); }; if (!uConfigData || !uConfigData.url) { ajaxGet(`/v2/api/config/upload?type=topic`).then((res) => { const data = res.data; uConfigData = data; upload(); }); } else { upload(); } }); }; const initEditor = () => { const maxSize = 1 * 1024 * 1024; // 1M const editorConfig = { // 自定义 HTML 处理规则 htmlFilter: { // 保留 img 标签(核心) tags: { img: true, // true 表示不过滤该标签,也可配置具体属性白名单 }, // 更精细控制:指定 img 允许的属性(避免过滤关键属性) attrs: { img: ["src", "alt", "width", "height", "data-id", "class"], // 按需添加自定义属性 }, }, // 关闭「粘贴/插入 HTML 时自动清理空标签」(若 img 暂未赋值 src 可能触发) autoFormatAfterInit: false, autoFormatOnEditable: false, placeholder: "输入正文", // scroll: true, // 禁止编辑器滚动 MENU_CONF: { ["emotion"]: { emotions: ["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵‍💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"], }, ["insertImage"]: { onInsertedImage(imageNode) { const { src, alt, url, href } = imageNode; }, async parseImageSrc(src) { console.log("parseImageSrc"); // 如果图片链接中已经包含了 ?aid= ,则说明是本站图片,直接返回,无需处理 if (src.includes("?aid=")) return src; console.log("parseImageSrc"); // 对于不含 ?aid= 的外部图片,执行上传转换 console.log("uConfigData", uConfigData); if (!uConfigData || !uConfigData.url) return src; console.log("parseImageSrc"); 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); } }, }, ["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"); const coverFile = await getVideoFirstFrame(file); const coverUploadRes = await uploading(coverFile, coverFile.name, "image"); insertFn(`${videoUploadRes.url}?aid=${videoUploadRes.aid}`, `${coverUploadRes.url}?aid=${coverUploadRes.aid}`); } catch (err) { console.error("上传出错:", err); } }, }, }, onChange(editor) { // console.log(editor.getHtml()); saveStatus.value = "有未保存的更改"; }, hoverbarKeys: { text: { menuKeys: [] }, video: { menuKeys: [] } }, }; try { editor = E.createEditor({ selector: "#editor-text-area", // content: [], html: info.value?.content || "", // html: `
`, config: editorConfig, }); } catch (error) { console.log("error", error); } // 如果有远程数据,使用远程数据 // if (info.value && info.value.content) { // editor.setHtml(info.value.content); // } else { // // 恢复草稿 (仅在没有远程数据时) // const cache = localStorage.getItem(draftKey); // if (cache) { // try { // const data = JSON.parse(cache); // if (data && (data.title || data.content)) { // if (data.title) title.value = data.title; // if (data.content) editor.setHtml(data.content); // if (data.updatedAt) { // saveStatus.value = `已保存 ${formatTime(new Date(data.updatedAt))}`; // } // } // } catch (_) {} // } // } const toolbarConfig = { excludeKeys: ["insertVideo", "fullScreen"], }; toolbar = E.createToolbar({ editor, selector: "#editor-toolbar", config: toolbarConfig, }); // 点击空白处 focus 编辑器 document.getElementById("editor-text-area").addEventListener("click", (e) => { if (e.target.id === "editor-text-area") { editor.blur(); editor.focus(true); // focus 到末尾 } }); }; // 提取视频第一帧作为封面 const getVideoFirstFrame = (file) => { return new Promise((resolve) => { const video = document.createElement("video"); video.src = URL.createObjectURL(file); video.currentTime = 1; // 截取第 1 秒 video.onloadeddata = () => { video.currentTime = 1; }; video.onseeked = () => { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob((blob) => { const coverFile = new File([blob], "cover.jpg", { type: "image/jpeg" }); resolve(coverFile); }, "image/jpeg"); }; }); }; const handleTitleInput = () => { saveStatus.value = "有未保存的更改"; }; onMounted(() => { const params = getUrlParams(); uniqid.value = params.uniqid || ""; cUpload(); nextTick(() => { init(); }); }); onUnmounted(() => { if (editor == null) return; editor.destroy(); editor = null; }); return { title, saveStatus, submit, handleTitleInput, cutAnonymity, info, }; }, }); editApp.mount("#edit");