328 lines
12 KiB
JavaScript
328 lines
12 KiB
JavaScript
// 简单版本的论坛编辑器,确保图片插入功能正常
|
||
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");
|