修复个人主页的响应式布局问题,优化移动端显示效果 调整分类和排序区域的样式,移除不必要的margin-left 更新投票组件的内容显示类名为one-line-display-v2 修复主页加载逻辑,优化数据获取和分页处理 移除未使用的代码和注释,清理CSS样式
788 lines
41 KiB
JavaScript
788 lines
41 KiB
JavaScript
// 简单版本的论坛编辑器,确保图片插入功能正常
|
|
const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue;
|
|
|
|
const editApp = createApp({
|
|
setup() {
|
|
const LANG = location.href.indexOf("lang=en") > 0 ? "en" : "zh_CN";
|
|
|
|
const title = ref("");
|
|
const saveStatus = ref("");
|
|
const uniqid = ref("");
|
|
const info = ref({});
|
|
const token = ref("");
|
|
let uConfigData = {};
|
|
const maxSize = 20 * 1024 * 1024; // 1M
|
|
|
|
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() || "";
|
|
try {
|
|
const urlObj = new URL(url, location.origin);
|
|
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),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error("Error parsing image URL:", url, e);
|
|
}
|
|
});
|
|
return images;
|
|
};
|
|
|
|
const extractVideos = (dom) => {
|
|
const videoElements = dom.querySelectorAll("video");
|
|
const result = [];
|
|
|
|
videoElements.forEach((videoEl) => {
|
|
const posterurl = videoEl.getAttribute("poster")?.trim() || "";
|
|
let posterid = null;
|
|
let cleanPosterurl = posterurl;
|
|
|
|
try {
|
|
const urlObj = new URL(posterurl, location.origin);
|
|
posterid = urlObj.searchParams.get("aid");
|
|
const queryIndex2 = posterurl.indexOf("?");
|
|
cleanPosterurl = queryIndex2 !== -1 ? posterurl.substring(0, queryIndex2) : posterurl;
|
|
} catch (e) {}
|
|
|
|
const sourceEl = videoEl.querySelector("source");
|
|
if (!sourceEl) return;
|
|
|
|
const url = sourceEl.getAttribute("src") || "";
|
|
let aid = null;
|
|
let cleanUrl = url;
|
|
|
|
try {
|
|
const obj = new URL(url, location.origin);
|
|
aid = obj.searchParams.get("aid");
|
|
const queryIndex = url.indexOf("?");
|
|
cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
|
|
} catch (e) {}
|
|
|
|
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 = "";
|
|
if (window.tinymce && window.tinymce.activeEditor) {
|
|
content = window.tinymce.activeEditor.getContent();
|
|
}
|
|
|
|
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(() => {
|
|
// Pass content directly to init
|
|
initTinyMCE(infoTarget.content || "");
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
console.log("err", err);
|
|
});
|
|
};
|
|
|
|
const uploading = (file, name, type) => {
|
|
return new Promise((resolve, reject) => {
|
|
const upload = () => {
|
|
let config = uConfigData;
|
|
|
|
const formData = new FormData();
|
|
formData.append(config.requestName || "file", 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;
|
|
|
|
xhr.onload = function () {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
const res = JSON.parse(xhr.responseText);
|
|
if (res.code == 200) {
|
|
const data = res.data;
|
|
resolve(data);
|
|
} else {
|
|
creationAlertBox("error", res.message || "上传失败");
|
|
reject(res);
|
|
}
|
|
} catch (e) {
|
|
creationAlertBox("error", "解析响应失败");
|
|
reject(e);
|
|
}
|
|
} 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();
|
|
}
|
|
});
|
|
};
|
|
|
|
// 提取视频第一帧作为封面 (支持 File 或 URL)
|
|
const getVideoFirstFrame = (source) => {
|
|
return new Promise((resolve, reject) => {
|
|
const video = document.createElement("video");
|
|
video.setAttribute("crossOrigin", "anonymous"); // Allow cross-origin for cover generation
|
|
|
|
if (source instanceof File) {
|
|
video.src = URL.createObjectURL(source);
|
|
} else if (typeof source === "string") {
|
|
video.src = source;
|
|
} else {
|
|
reject(new Error("Invalid source for video cover generation"));
|
|
return;
|
|
}
|
|
|
|
video.currentTime = 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");
|
|
};
|
|
video.onerror = (e) => {
|
|
reject(e);
|
|
};
|
|
});
|
|
};
|
|
|
|
const initTinyMCE = (initialContent) => {
|
|
if (window.tinymce.get("editor-text-area")) {
|
|
window.tinymce.get("editor-text-area").remove();
|
|
}
|
|
|
|
// Calculate height based on window size to match original CSS calc(100vh - 370px)
|
|
// const editorHeight = window.innerHeight - 330;
|
|
|
|
const editorConfig = {
|
|
selector: "#editor-text-area",
|
|
language: LANG === "en" ? "en" : "zh_CN",
|
|
language_url: LANG === "en" ? undefined : "/js/tinymce/langs/zh_CN.js",
|
|
plugins: "image media table link lists code charmap emoticons wordcount fullscreen preview searchreplace autolink directionality visualblocks visualchars template codesample",
|
|
paste_preprocess: (plugin, args) => {
|
|
if (!args.content.match(/<\/?(p|div|h[1-6]|ul|ol|table|blockquote|pre)[^>]*>/i)) {
|
|
const fragments = args.content.split(/<br\s*\/?>/gi);
|
|
const newContent = fragments
|
|
.map((frag) => {
|
|
// 移除 和空白后检查是否有内容
|
|
// 保留图片等 HTML 标签,只过滤纯空白行
|
|
if (frag.replace(/ /gi, "").trim().length > 0) {
|
|
return "<p>" + frag + "</p>";
|
|
}
|
|
return null;
|
|
})
|
|
.filter(Boolean)
|
|
.join("");
|
|
|
|
if (newContent) {
|
|
args.content = newContent;
|
|
}
|
|
}
|
|
},
|
|
toolbar: "undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | removeformat | emoticons | fullscreen",
|
|
menubar: false,
|
|
fixed_toolbar_container: "#editor-toolbar",
|
|
// height: editorHeight > 300 ? editorHeight : 300, // Ensure minimum height
|
|
// resize: false,
|
|
height: "100%", // Use CSS height
|
|
resize: true, // Allow user to resize if needed, or rely on container
|
|
branding: false,
|
|
promotion: false,
|
|
convert_urls: false,
|
|
media_live_embeds: true, // Enable live video previews
|
|
initialValue: initialContent, // Set initial content here
|
|
|
|
// Add urlconverter_callback to handle network resources
|
|
urlconverter_callback: (url, node, on_save, name) => {
|
|
// Return URL as is, let image upload handler process it if needed
|
|
return url;
|
|
},
|
|
|
|
// Intercept Media Dialog URL input
|
|
media_url_resolver: function (data, resolve /*, reject*/) {
|
|
const url = data.url;
|
|
|
|
// Only intercept http/https URLs that are NOT from our domain (already uploaded)
|
|
if (url && (url.startsWith("http://") || url.startsWith("https://")) && !url.includes("?aid=")) {
|
|
// Check if it's a video file type we care about
|
|
const isVideo = /\.(mp4|webm|ogg|mov|mkv|avi|flv|wmv)$/i.test(url);
|
|
|
|
if (isVideo && uConfigData && uConfigData.url) {
|
|
creationAlertBox("info", "Uploading network video...");
|
|
|
|
const formData = new FormData();
|
|
formData.append("uploadType", "url");
|
|
formData.append("url", url);
|
|
if (uConfigData.params && uConfigData.params.data) {
|
|
formData.append("data", uConfigData.params.data);
|
|
}
|
|
|
|
ajax(uConfigData.url, formData)
|
|
.then(async (res) => {
|
|
if (res.code == 200 && res.data) {
|
|
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
|
|
|
|
try {
|
|
// Generate cover
|
|
const coverFile = await getVideoFirstFrame(newUrl);
|
|
const coverRes = await uploading(coverFile, "cover.jpg", "image");
|
|
const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
|
|
|
|
// Return the full HTML embed code
|
|
// TinyMCE expects HTML when using resolve() for embeds
|
|
const videoHtml = `<video src="${newUrl}" poster="${coverUrl}" controls="controls" width="300" height="150"></video>`;
|
|
resolve({ html: videoHtml });
|
|
|
|
// Mark as processed (need to wait for insertion)
|
|
setTimeout(() => {
|
|
const editor = window.tinymce.activeEditor;
|
|
if (editor) {
|
|
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
|
|
if (insertedVideo) {
|
|
// We need to access processedNodes from the outer scope if possible,
|
|
// but this config object is defined inside initTinyMCE.
|
|
// However, processedNodes is defined in setup().
|
|
// We can't access it here easily unless we move processedNodes to higher scope or use a global/editor property.
|
|
// Let's attach it to the editor instance in setup().
|
|
if (editor.processedNodes) {
|
|
editor.processedNodes.add(insertedVideo);
|
|
}
|
|
}
|
|
}
|
|
}, 500);
|
|
|
|
creationAlertBox("success", "Video uploaded successfully");
|
|
} catch (e) {
|
|
console.error("Cover generation failed", e);
|
|
const videoHtml = `<video src="${newUrl}" controls="controls" width="300" height="150"></video>`;
|
|
resolve({ html: videoHtml });
|
|
creationAlertBox("success", "Video uploaded (cover failed)");
|
|
}
|
|
} else {
|
|
creationAlertBox("error", "Video upload failed");
|
|
resolve({ html: `<span class="error">Upload failed: ${url}</span>` }); // Or just resolve empty to cancel?
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Media resolver upload failed", err);
|
|
resolve({ html: "" }); // Or fallback to original url? resolve({ html: `<video src="${url}" controls></video>` })
|
|
});
|
|
|
|
// Return early, we will resolve asynchronously
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Default behavior for other URLs or if logic skipped
|
|
resolve({ html: "" }); // Letting it empty might trigger default embed logic?
|
|
// Actually, if we return empty string, TinyMCE might just insert nothing or error.
|
|
// The default behavior is to rely on promises.
|
|
// If we want default behavior, we should probably NOT define this option or call a fallback?
|
|
// Wait, media_url_resolver replaces the default logic.
|
|
// If we don't handle it, we must return a promise that resolves to HTML.
|
|
// If we want the default behavior (creating a video tag for the URL), we have to do it ourselves.
|
|
|
|
const defaultHtml = `<video src="${url}" width="300" height="150" controls="controls"></video>`;
|
|
resolve({ html: defaultHtml });
|
|
},
|
|
|
|
// Handle pasted/dropped images
|
|
images_upload_handler: (blobInfo, progress) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (blobInfo.blob().size > maxSize) {
|
|
reject({ message: "图片大小不能超过 20MB", remove: true });
|
|
return;
|
|
}
|
|
uploading(blobInfo.blob(), blobInfo.filename(), "image")
|
|
.then((res) => {
|
|
resolve(res.url + "?aid=" + res.aid);
|
|
})
|
|
.catch((err) => {
|
|
reject({ message: err.message || "Image upload failed", remove: true });
|
|
});
|
|
});
|
|
},
|
|
|
|
file_picker_callback: (callback, value, meta) => {
|
|
const input = document.createElement("input");
|
|
input.setAttribute("type", "file");
|
|
|
|
if (meta.filetype === "image") {
|
|
input.setAttribute("accept", "image/png, image/jpeg, image/jpg");
|
|
} else if (meta.filetype === "media") {
|
|
input.setAttribute("accept", "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");
|
|
}
|
|
|
|
input.addEventListener("change", (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
if (file.size > maxSize) {
|
|
creationAlertBox("error", "文件大小不能超过 20MB");
|
|
return;
|
|
}
|
|
|
|
if (meta.filetype === "image") {
|
|
uploading(file, file.name, "image")
|
|
.then((res) => {
|
|
callback(res.url + "?aid=" + res.aid, { alt: file.name });
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
creationAlertBox("error", "Image upload failed");
|
|
});
|
|
} else if (meta.filetype === "media") {
|
|
creationAlertBox("info", "Uploading video, please wait...");
|
|
uploading(file, file.name, "video")
|
|
.then(async (videoRes) => {
|
|
try {
|
|
const coverFile = await getVideoFirstFrame(file);
|
|
const coverRes = await uploading(coverFile, coverFile.name, "image");
|
|
callback(videoRes.url + "?aid=" + videoRes.aid, { poster: coverRes.url + "?aid=" + coverRes.aid });
|
|
creationAlertBox("success", "Video uploaded successfully");
|
|
} catch (e) {
|
|
console.error(e);
|
|
callback(videoRes.url + "?aid=" + videoRes.aid);
|
|
creationAlertBox("success", "Video uploaded (cover generation failed)");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
creationAlertBox("error", "Video upload failed");
|
|
});
|
|
}
|
|
});
|
|
|
|
input.click();
|
|
},
|
|
|
|
setup: (editor) => {
|
|
const processedNodes = new WeakSet();
|
|
editor.processedNodes = processedNodes; // Expose to editor instance for media resolver access
|
|
|
|
editor.on("change keyup", () => {
|
|
saveStatus.value = "有未保存的更改";
|
|
});
|
|
|
|
// Also try to set content on init as fallback/confirmation
|
|
editor.on("init", () => {
|
|
// Only if empty (though initialValue should handle it)
|
|
if (!editor.getContent() && initialContent) {
|
|
editor.setContent(initialContent);
|
|
}
|
|
|
|
// Mark all existing images/videos as processed to prevent auto-upload on click
|
|
// This handles the case where initial content contains external images that are already "uploaded"
|
|
// or should be treated as such (not auto-uploaded again).
|
|
const body = editor.getBody();
|
|
const imgs = body.querySelectorAll("img");
|
|
const videos = body.querySelectorAll("video");
|
|
|
|
imgs.forEach((node) => processedNodes.add(node));
|
|
videos.forEach((node) => processedNodes.add(node));
|
|
});
|
|
|
|
// Handle network images/videos paste or drop
|
|
editor.on("Paste", async (e) => {
|
|
const clipboardData = e.clipboardData || window.clipboardData;
|
|
const html = clipboardData.getData("text/html");
|
|
|
|
if (html) {
|
|
const div = document.createElement("div");
|
|
div.innerHTML = html;
|
|
|
|
const images = div.querySelectorAll("img");
|
|
const videos = div.querySelectorAll("video");
|
|
|
|
if (images.length > 0) {
|
|
for (let img of images) {
|
|
const src = img.getAttribute("src");
|
|
if (src && !src.includes("?aid=") && src.startsWith("http") && uConfigData && uConfigData.url) {
|
|
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) {
|
|
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
|
|
setTimeout(() => {
|
|
const editorContent = editor.getContent();
|
|
if (editorContent.includes(src)) {
|
|
const newContent = editorContent.replace(src, newUrl);
|
|
editor.setContent(newContent);
|
|
}
|
|
}, 100);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to upload network image:", src, err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (videos.length > 0) {
|
|
for (let video of videos) {
|
|
const src = video.getAttribute("src");
|
|
if (src && !src.includes("?aid=") && src.startsWith("http") && uConfigData && uConfigData.url) {
|
|
try {
|
|
creationAlertBox("info", "Uploading network video...");
|
|
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) {
|
|
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
|
|
|
|
// Generate cover from the new local URL
|
|
try {
|
|
const coverFile = await getVideoFirstFrame(newUrl);
|
|
const coverRes = await uploading(coverFile, "cover.jpg", "image");
|
|
const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
|
|
|
|
setTimeout(() => {
|
|
const editorContent = editor.getContent();
|
|
// Replace src and add poster
|
|
// This regex might need to be more robust
|
|
if (editorContent.includes(src)) {
|
|
let newContent = editorContent.replace(src, newUrl);
|
|
// Find the video tag and inject poster if not present, or replace
|
|
// Simple string replace for src is safe, but for poster we need DOM manipulation or regex
|
|
// Let's use DOM for precision
|
|
const tempDiv = document.createElement("div");
|
|
tempDiv.innerHTML = newContent;
|
|
const tempVideo = tempDiv.querySelector(`video[src*="${res.data.url}"]`); // Use partial match or ID
|
|
if (tempVideo) {
|
|
tempVideo.setAttribute("poster", coverUrl);
|
|
editor.setContent(tempDiv.innerHTML);
|
|
} else {
|
|
// Fallback: regex replace the tag
|
|
// This is tricky without unique ID.
|
|
// Let's just setContent with simple replace first, then update poster via NodeChange logic or DOM
|
|
editor.setContent(newContent);
|
|
// We can trigger a NodeChange or find it again
|
|
}
|
|
}
|
|
}, 100);
|
|
} catch (e) {
|
|
console.error("Cover generation failed", e);
|
|
// Update video without cover
|
|
setTimeout(() => {
|
|
const editorContent = editor.getContent();
|
|
if (editorContent.includes(src)) {
|
|
const newContent = editorContent.replace(src, newUrl);
|
|
editor.setContent(newContent);
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to upload network video:", src, err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Listen for NodeChange to catch inserted images/videos that might be external
|
|
editor.on("NodeChange", async (e) => {
|
|
const node = e.element;
|
|
if (!uConfigData || !uConfigData.url) return;
|
|
|
|
// Handle Image
|
|
if (node.tagName === "IMG" && !node.getAttribute("data-mce-object")) {
|
|
// Check if already processed
|
|
if (processedNodes.has(node)) return;
|
|
|
|
const src = node.getAttribute("src");
|
|
|
|
// Check local or invalid
|
|
if (!src || src.startsWith("data:") || src.includes("?aid=") || src.startsWith(location.origin) || src.startsWith("/")) {
|
|
processedNodes.add(node);
|
|
return;
|
|
}
|
|
|
|
if (src.startsWith("http")) {
|
|
// Mark as processed
|
|
processedNodes.add(node);
|
|
|
|
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) {
|
|
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
|
|
editor.dom.setAttrib(node, "src", newUrl);
|
|
editor.dom.setAttrib(node, "data-mce-src", newUrl);
|
|
// Remove from processedNodes? No, because now src has ?aid= so it will be ignored anyway.
|
|
// And if we undo, node is new.
|
|
} else {
|
|
console.warn("Upload failed for", src);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to upload network image on NodeChange:", src, err);
|
|
// Do NOT remove from processedNodes to avoid retry loop on persistent error
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle Video
|
|
// TinyMCE 6 with media_live_embeds: true uses <video> or <span class="mce-object-video">
|
|
if (node.tagName === "VIDEO" || (node.tagName === "IMG" && node.getAttribute("data-mce-object") === "video")) {
|
|
// Check if already processed
|
|
if (processedNodes.has(node)) return;
|
|
|
|
let src = node.getAttribute("src");
|
|
// If it's a placeholder img, the src usually points to a transparent pixel, real src is in data-mce-p-src
|
|
if (node.tagName === "IMG") {
|
|
src = node.getAttribute("data-mce-p-src") || src;
|
|
}
|
|
|
|
// Check local or invalid
|
|
if (!src || src.startsWith("data:") || src.includes("?aid=") || src.startsWith(location.origin) || src.startsWith("/")) {
|
|
processedNodes.add(node);
|
|
return;
|
|
}
|
|
|
|
if (src.startsWith("http")) {
|
|
// Mark as processed
|
|
processedNodes.add(node);
|
|
|
|
try {
|
|
creationAlertBox("info", "Uploading network video...");
|
|
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) {
|
|
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
|
|
|
|
// Update URL first
|
|
if (node.tagName === "VIDEO") {
|
|
editor.dom.setAttrib(node, "src", newUrl);
|
|
editor.dom.setAttrib(node, "data-mce-src", newUrl);
|
|
} else {
|
|
editor.dom.setAttrib(node, "data-mce-p-src", newUrl);
|
|
}
|
|
|
|
// Generate and set poster
|
|
try {
|
|
const coverFile = await getVideoFirstFrame(newUrl);
|
|
const coverRes = await uploading(coverFile, "cover.jpg", "image");
|
|
const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
|
|
|
|
// Instead of just setting attributes, replace the entire node with a proper video tag
|
|
// This ensures consistency and proper rendering in TinyMCE
|
|
const videoHtml = `<video src="${newUrl}" poster="${coverUrl}" controls="controls" width="300" height="150"></video>`;
|
|
editor.selection.select(node);
|
|
editor.insertContent(videoHtml);
|
|
|
|
// Mark the new video node as processed
|
|
// We need to find the newly inserted video node
|
|
// insertContent might leave the cursor after the video.
|
|
// A simple way is to query by src again, but that might be ambiguous.
|
|
// However, since we just inserted it, it should be safe.
|
|
setTimeout(() => {
|
|
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
|
|
if (insertedVideo) {
|
|
processedNodes.add(insertedVideo);
|
|
}
|
|
}, 0);
|
|
|
|
creationAlertBox("success", "Video auto-uploaded with cover");
|
|
} catch (e) {
|
|
console.error("Auto cover generation failed", e);
|
|
// Even if cover fails, we should update the src properly if it was a placeholder
|
|
if (node.tagName === "IMG") {
|
|
const videoHtml = `<video src="${newUrl}" controls="controls" width="300" height="150"></video>`;
|
|
editor.selection.select(node);
|
|
editor.insertContent(videoHtml);
|
|
setTimeout(() => {
|
|
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
|
|
if (insertedVideo) processedNodes.add(insertedVideo);
|
|
}, 0);
|
|
}
|
|
creationAlertBox("success", "Video auto-uploaded (cover failed)");
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to upload network video on NodeChange:", src, err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
window.tinymce.init(editorConfig);
|
|
};
|
|
|
|
const handleTitleInput = () => {
|
|
saveStatus.value = "有未保存的更改";
|
|
};
|
|
|
|
onMounted(() => {
|
|
const params = getUrlParams();
|
|
uniqid.value = params.uniqid || "";
|
|
cUpload();
|
|
|
|
nextTick(() => {
|
|
init();
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (window.tinymce && window.tinymce.activeEditor) {
|
|
window.tinymce.activeEditor.destroy();
|
|
}
|
|
});
|
|
|
|
return {
|
|
title,
|
|
saveStatus,
|
|
submit,
|
|
handleTitleInput,
|
|
cutAnonymity,
|
|
info,
|
|
};
|
|
},
|
|
});
|
|
editApp.mount("#edit");
|