Files
PC-Light-Forum/js/edit.js
DESKTOP-RQ919RC\Pc 460450c339 feat(editor): 集成富文本编辑器并优化响应式布局
1. 添加wangEditor富文本编辑器替换原有简易编辑器
2. 新增编辑器相关CSS样式和功能按钮
3. 优化详情页和编辑页的响应式布局
4. 调整评论区域样式结构
5. 添加移动端适配样式
2025-11-25 19:12:29 +08:00

906 lines
35 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 } = window.wangEditor;
console.log("createEditor", createEditor);
const editApp = createApp({
setup() {
let titleLength = ref(200);
let uniqid = ref("");
onMounted(() => {
const params = getUrlParams();
uniqid.value = params.uniqid || "";
getUserInfoWin();
checkWConfig();
cUpload();
// 添加selectionchange事件监听当鼠标选中区域内容时更新lastSelection
document.addEventListener("selectionchange", handleSelectionChange);
});
// 组件卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener("selectionchange", handleSelectionChange);
});
let imageLength = 10;
let videoLength = 5;
const checkWConfig = () => {
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
console.log("wConfig", 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;
console.log("uConfigData", uConfigData);
init();
});
};
let info = ref({});
let tagList = ref([]);
let token = ref("");
let infoImages = [];
const init = () => {
ajax("/v2/api/forum/postPublishInit", {
uniqid: uniqid.value,
})
.then((res) => {
const data = res.data;
if (res.code != 200) {
creationAlertBox("error", res.message || "操作失败");
return;
}
const infoTarget = data.info || {};
if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoTarget.attachments);
info.value = infoTarget;
token.value = data.token;
// nextTick(() => {
// judgeIsEmpty();
// });
initEditor();
})
.catch((err) => {
console.log("err", err);
});
};
let editor = null;
const initEditor = () => {
let infoTarget = info.value || {};
console.log("infoTarget", infoTarget);
const editorConfig = {
placeholder: "Type here...",
enabledMenus: [],
MENU_CONF: {
["emotion"]: {
emotions: optionEmoji.value,
},
["uploadImage"]: {
server: uConfigData.url,
// form-data fieldName ,默认值 'wangeditor-uploaded-image'
fieldName: uConfigData.requestName,
// 单个文件的最大体积限制,默认为 2M
maxFileSize: maxSize, // 1M
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: imageLength,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["image/*", ".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); // 文件名
ajax(config.url, formData).then((res) => {
const data = res.data;
insertFn(data.url); // 传入图片的可访问 URL
});
} catch (err) {
console.error("上传出错:", err);
}
},
},
},
// 4. 链接菜单:显式启用(默认启用,补充配置防止被过滤)
link: {
disabled: false, // 确保不禁用
showTarget: true, // 显示「是否新窗口打开」选项
showRel: true, // 显示「rel 属性」选项
},
// 5. 对齐菜单:显式启用(默认启用,兜底配置)
justify: {
disabled: false,
},
onChange(editor) {
const html = editor.getHtml();
console.log("editor content", html);
// 也可以同步到 <textarea>
},
};
editor = createEditor({
selector: "#editor-container",
html: "<p><br>555</p>",
config: editorConfig,
mode: "default",
});
setTimeout(() => {
console.log("editor", editor);
editor.addMark("bold", true); // 加粗
}, 1000);
// const toolbar = DomEditor.getToolbar(editor)
const toolbarConfig = {
// toolbarKeys: ["bold", "italic", "list"],
toolbarKeys: [
"headerSelect", // 标题
"bold", // 粗体
"italic", // 斜体
// "justify", // 对齐方式
{
key: "justifyCenter",
title: "对齐",
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
menuKeys: ["justifyLeft", "justifyRight", "justifyCenter", "justifyJustify"],
},
"emotion", // 表情
"insertLink", // 插入链接
"uploadImage", // 插入图片
"uploadVideo", // 插入视频
"undo", // 撤销
"redo", // 重做
"fullScreen", // 全屏
],
};
const toolbar = createToolbar({
editor,
selector: "#toolbar-container",
config: toolbarConfig,
mode: "default",
});
console.log("toolbar", toolbar);
// setTimeout(() => {
// const el = document.querySelector("#toolbar-container");
// if (el && el.children.length === 0) {
// createToolbar({ editor, selector: "#toolbar-container", config: toolbarConfig, mode: "default" });
// }
// }, 0);
};
const restoreHtml = (formattedText, attachments) => {
const imageList = attachments?.images || [];
const filesList = attachments?.files || [];
const videosList = attachments?.videos || [];
let html = formattedText;
// 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, "<h2>$1</h2>");
// 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}"><br/>`;
}
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}" data-aid="${aid}"><br/>`;
}
// 查找对应的视频信息
const video = videosList.find((v) => v.aid == aid);
if (video) {
console.log("video", video);
videosList.splice(videosList.indexOf(video), 1);
return `<video contenteditable="false" src="${video.url}" width="400" height="400" preload="none" poster="${video.posterurl}" aid="${video.aid}" posterid="${video.posterid}" controls></video>`;
}
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}" data-aid="${element.aid}"><br/>`;
});
// video 不要预加载
videosList.forEach((element) => {
html += `<video contenteditable="false" src="${element.url}" width="400" height="400" preload="none" poster="${element.posterurl}" aid="${element.aid}" posterid="${element.posterid}" controls></video><br/>`;
});
return html;
};
onMounted(() => {
setTimeout(() => focusLastNode(), 1000);
// document.addEventListener("keydown", handleUndoKeydown);
});
const editorRef = ref(null);
const focusLastNode = () => {
return;
const newRange = document.createRange();
const textNode = document.createTextNode("");
editorRef.value.appendChild(textNode);
newRange.setStartAfter(textNode, 0);
newRange.setEndAfter(textNode, 0);
lastSelection = newRange;
};
let lastSelection = null;
let loading = ref(false);
const maxSize = 20 * 1024 * 1024; // 20MB
const insertImage = (event) => {
const images = extractImages(editorRef.value);
const count = imageLength - images.length || 0;
if (count == 0) {
creationAlertBox("error", `最多只能上传 ${imageLength} 张图片`);
return;
}
const target = event.target.files[0];
if (!target) return; // 处理未选择文件的情况
if (target.size > maxSize) {
creationAlertBox("error", "文件大小不能超过 20MB");
return;
}
loading.value = true;
uploading(target, target.name, "image").then((data) => {
const selection = window.getSelection();
editorRef.value.focus();
if (lastSelection) {
selection.removeAllRanges();
selection.addRange(lastSelection);
}
const html = `<img src="${data.url}" data-aid="${data.aid}"><br/>`;
document.execCommand("insertHTML", false, html);
judgeIsEmpty();
});
};
const insertVideo = async (event) => {
const videos = extractVideos(editorRef.value);
const count = videoLength - videos.length || 0;
if (count == 0) {
creationAlertBox("error", `最多只能上传 ${videoLength} 个视频`);
return;
}
const videoFile = event.target.files[0];
if (!videoFile) return; // 处理未选择文件的情况
if (videoFile.size > maxSize) {
creationAlertBox("error", "文件大小不能超过 20MB");
return;
}
loading.value = true;
console.log("videoFile", videoFile);
// 步骤1提取视频第一帧等待提取完成
const coverFile = await getVideoFirstFrame(videoFile);
console.log("第一帧提取成功", coverFile);
// 步骤2先上传视频文件type 传 'video',按后端要求调整)
const videoUploadRes = await uploading(videoFile, videoFile.name, "video");
console.log("视频上传成功", videoUploadRes);
// 步骤3再上传第一帧封面type 传 'cover',按后端要求调整)
const coverUploadRes = await uploading(coverFile, coverFile.name, "image");
console.log("封面上传成功", coverUploadRes);
console.log("最终", videoUploadRes, videoUploadRes);
const selection = window.getSelection();
editorRef.value.focus();
if (lastSelection) {
selection.removeAllRanges();
selection.addRange(lastSelection);
}
const html = `<video width="400" height="400" controls preload="none" poster="${coverUploadRes.url}" src="${videoUploadRes.url}" contenteditable="false" posterid="${coverUploadRes.aid}" aid="${videoUploadRes.aid}"></video><br/>`;
document.execCommand("insertHTML", false, html);
judgeIsEmpty();
};
let isEmpty = ref(true);
const onEditorInput = (event) => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
lastSelection = selection.getRangeAt(0);
// console.log("更新选区");
updatePTitleStatus();
}
judgeIsEmpty();
// debouncedGetTagList();
};
// 防抖函数
const debounce = (fn, delay = 500) => {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
};
const getRandomChinese = () => {
// 中文 Unicode 范围:\u4e00 - \u9fa5共约 2 万个汉字)
const start = 0x4e00; // 起始编码
const end = 0x9fa5; // 结束编码
// 生成范围内的随机整数,转为字符
return String.fromCodePoint(Math.floor(Math.random() * (end - start + 1) + start));
};
const generateRandomString = (length = 5) => {
// 定义字符集:包含大小写字母和数字
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
// 循环生成指定长度的随机字符
for (let i = 0; i < length; i++) {
// 从字符集中随机取一个字符
const randomIndex = Math.floor(Math.random() * chars.length);
result += chars[randomIndex];
}
return result;
};
const getTagList = () => {
if (!isLogin.value) {
goLogin();
return;
}
const content = editorRef.value.innerText;
ajax("/v2/api/forum/postPublishTags", {
content,
}).then((res) => {
res = res.data;
if (res.code != 200) return;
let data = res.data || [];
// 随机生成一下数据
for (let i = 0; i < 5; i++) {
data.push({
title: getRandomChinese() + getRandomChinese(),
tagId: generateRandomString(),
});
}
tagList.value = data;
});
};
const debouncedGetTagList = debounce(getTagList, 500);
let isBottomState = ref(false); // 底部按钮 显示
const onEditorFocus = () => {
isBottomState.value = true;
};
const onEditorBlur = () => {
isBottomState.value = false;
};
// 判断是否为空
const judgeIsEmpty = () => {
const text = editorRef.value.innerText;
isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img") && !editorRef.value.querySelector("video");
};
// 处理选中文本变化的函数
const handleSelectionChange = () => {
return;
const selection = window.getSelection();
// 确保有选中内容且选中区域在编辑器内
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// 检查选中区域是否在编辑器内
const commonAncestor = range.commonAncestorContainer;
if (editorRef.value.contains(commonAncestor)) {
console.log("选中区域在编辑器内", range);
lastSelection = range;
}
}
};
const isPTitle = ref(false);
const paragraphTitle = () => {
editorRef.value.focus();
if (!lastSelection) return;
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(lastSelection);
// 使用try-catch确保即使命令执行失败也能恢复滚动位置
try {
document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2");
} catch (error) {
console.error("应用段落格式失败:", error);
}
// 更新状态
setTimeout(() => updatePTitleStatus(), 100);
};
const updatePTitleStatus = () => {
if (lastSelection) {
let parentElement = lastSelection.commonAncestorContainer;
// 死循环,直到遇到终止条件
while (true) {
// 如果没有父元素了到达文档根节点退出循环返回false
if (!parentElement) {
isPTitle.value = false;
return;
}
// 遇到id为"editor"的元素返回false
if (parentElement.id === "editor") {
isPTitle.value = false;
return;
}
// 遇到nodeName为"H2"的元素返回true注意nodeName是大写的
if (parentElement.nodeName === "H2") {
isPTitle.value = true;
return;
}
// 继续向上查找父元素
parentElement = parentElement.parentElement;
}
}
};
const cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1);
let emojiState = ref(false);
const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵‍💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]);
const openEmoji = () => (emojiState.value = true);
const closeEmoji = () => (emojiState.value = false);
const selectEmoji = (emoji) => {
const selection = window.getSelection();
editorRef.value.focus();
if (lastSelection) {
selection.removeAllRanges();
selection.addRange(lastSelection);
}
document.execCommand("insertText", false, emoji);
closeEmoji();
judgeIsEmpty();
};
let format = ref("");
const submit = (status) => {
const infoTarget = { ...info.value } || {};
let content = editorRef.value.innerHTML;
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,
};
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.1 替换视频标签
html = html.replace(/<video[^>]*aid="(\d+)"[^>]*>[\s\S]*?<\/video>/gi, "[attach]$1[/attach]");
// 2. 替换H2标签
html = html.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "[b]$1[/b]");
// 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) => {
const url = imgEl.getAttribute("src")?.trim() || "";
const aid = imgEl.dataset.aid?.trim() || ""; // 用 dataset 简化自定义属性读取
images.push({
url,
aid: Number(aid),
});
});
return images;
};
const extractVideos = (dom) => {
// 1. 查找页面中所有 <video> 节点(返回 NodeList 集合)
const videoElements = dom.querySelectorAll("video");
const result = [];
// 2. 遍历每个 video 节点,直接获取属性
videoElements.forEach((videoEl) => {
const url = videoEl.getAttribute("src")?.trim() || ""; // 视频地址
const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 封面图
const aid = videoEl.getAttribute("aid")?.trim() || ""; // 视频 ID自定义属性
const posterid = videoEl.getAttribute("posterid")?.trim() || ""; // 封面 ID自定义属性
result.push({
aid: Number(aid),
posterid: Number(posterid),
url,
posterurl,
});
});
console.log("提取完成的视频列表:", result);
return result;
};
const handleClick = () => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
lastSelection = selection.getRangeAt(0);
// console.log("更新选区");
updatePTitleStatus();
}
};
let linkUrl = ref("");
let linkText = ref("");
let linkState = ref(false);
const openLink = () => {
console.log("打开链接");
const text = lastSelection ? lastSelection.toString().trim() : "";
console.log("lastSelection", text);
linkText.value = text;
linkState.value = true;
};
const closeLink = () => {
console.log("关闭链接");
linkState.value = false;
linkText.value = "";
linkUrl.value = "";
};
const insertLink = () => {
if (linkText.value == "" || linkUrl.value == "") {
creationAlertBox("error", "请输入链接文字和链接地址");
return;
}
const selection = window.getSelection();
editorRef.value.focus();
if (lastSelection) {
selection.removeAllRanges();
selection.addRange(lastSelection);
}
const html = `<a href="${linkUrl.value}" target="_blank" contenteditable="false">${linkText.value}</a>`;
document.execCommand("insertHTML", false, html);
closeLink();
judgeIsEmpty();
};
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 }
);
});
};
const linkClick = () => {};
const overstriking = () => {
console.log("加粗");
editor.addMark("bold", true); // 加粗
editor.addMark("color", "#999"); // 文本颜色
console.log("editor", editor.addMark);
};
return { overstriking, linkClick, insertVideo, insertLink, linkUrl, linkText, linkState, openLink, closeLink, handleClick, uniqid, userInfoWin, titleLength, submit, emojiState, openEmoji, closeEmoji, selectEmoji, optionEmoji, isPTitle, onEditorInput, onEditorFocus, onEditorBlur, paragraphTitle, info, tagList, token, cutAnonymity, editorRef, insertImage, judgeIsEmpty, isEmpty };
},
});
editApp.component("headTop", headTop);
editApp.mount("#edit");