// 简单版本的论坛编辑器,确保图片插入功能正常 const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue; const editApp = createApp({ setup() { const { Editor, FileUploader } = window.textbus; const title = ref(""); const saveStatus = ref(""); const uniqid = ref(""); const info = ref({}); const token = ref(""); let editor = 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 } || {}; // 获取 HTML 内容 let content = ""; if (editor && typeof editor.getHTML === 'function') { content = editor.getHTML(); } else if (editor && editor.output) { content = editor.output.content; // Fallback if getHTML isn't direct } // 创建临时 DOM 用于提取图片和视频 const tempDiv = document.createElement("div"); tempDiv.innerHTML = content; const images = extractImages(tempDiv); const videos = extractVideos(tempDiv); 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 || {}; 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(); } }); }; // 自定义上传适配器 class CustomUploader extends FileUploader { uploadFile(type, file) { // type 可能是 'image' 或 'video' 等,取决于调用方 // uploading 函数接受 (file, name, type) return uploading(file, file.name, type).then(res => { // 构造带 aid 的 url return `${res.url}?aid=${res.aid}`; }); } } const initEditor = () => { const editorConfig = { content: info.value?.content || "", providers: [{ provide: FileUploader, useFactory: () => new CustomUploader() }], // 默认情况下,xnote 使用悬浮/气泡菜单 // 我们不配置 toolbar 容器,让其使用默认行为 }; try { editor = new Editor(editorConfig); editor.mount(document.getElementById("editor-text-area")); // 监听内容变化 if (editor.onChange) { editor.onChange.subscribe(() => { saveStatus.value = "有未保存的更改"; }); } } catch (error) { console.log("error", error); } // 点击空白处 focus 编辑器 document.getElementById("editor-text-area").addEventListener("click", (e) => { // 如果点击的是容器本身(空白处),则聚焦 if (e.target.id === "editor-text-area") { // editor.focus() 如果存在 // Textbus editor 实例通常不需要手动 focus,除非是 command } }); }; // 提取视频第一帧作为封面 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; if (editor.destroy) editor.destroy(); editor = null; }); return { title, saveStatus, submit, handleTitleInput, cutAnonymity, info, }; }, }); editApp.mount("#edit");