feat(editor): 实现自定义居中按钮并优化媒体上传处理

- 添加自定义居中按钮功能,替换原生的居中实现
- 修改图片和视频上传后的URL格式,附加aid参数
- 优化媒体资源提取逻辑,处理URL中的查询参数
- 调整编辑器工具栏的z-index样式
- 移除不必要的position属性
This commit is contained in:
A1300399510
2025-11-27 02:50:45 +08:00
parent 275b78b221
commit f7af6d4046
4 changed files with 148 additions and 88 deletions

View File

@@ -86,7 +86,7 @@
padding-left: 25px;
position: sticky;
top: 0;
z-index: 10;
z-index: 1;
}
#edit .edit-container #editorwrapper .editor-toolbar .w-e-panel-content-emotion {
width: 490px;
@@ -118,7 +118,6 @@
color: #000000;
line-height: 23px;
margin-right: 40px;
position: relative;
padding: 0;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .icon {

View File

@@ -96,7 +96,8 @@
padding-left: 25px;
position: sticky;
top: 0;
z-index: 10;
// z-index: 10;
z-index: 1;
.w-e-panel-content-emotion {
width: 490px;
}
@@ -139,7 +140,7 @@
color: #000000;
line-height: 23px;
margin-right: 40px;
position: relative;
// position: relative;
padding: 0;
> button {

View File

@@ -34,10 +34,11 @@
<div class="edit-container">
<!-- 标题输入 -->
<div class="title-box">
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title" :maxlength="titleLength" />
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title"
:maxlength="titleLength" />
<div class="sum">{{ info?.title?.length ? titleLength - info?.title?.length : titleLength }}</div>
</div>
<div id="editor—wrapper" class="editor—wrapper">
<!-- 工具栏 -->
@@ -89,11 +90,12 @@
<div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji" @click.stop="selectEmoji(emoji)">{{ emoji }}</div>
</div>
</div> -->
</div>
<!-- <div id="toolbar-container"></div> -->
<div id="editor-container"><!-- 编辑器 --></div>
<div id="editor-container" ref="editorRef"><!-- 编辑器 --></div>
</div>
<!-- 内容编辑区 -->
<!-- <div class="content-input" id="editor" contenteditable="true" :class="{ 'empty': isEmpty }" placeholder="输入正文" ref="editorRef" @input="onEditorInput" @focus="onEditorFocus" @blur="onEditorBlur" v-html="info.content" @click="handleClick"></div> -->

View File

@@ -1,9 +1,54 @@
// 简单版本的论坛编辑器,确保图片插入功能正常
const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue;
import { headTop } from "../component/head-top/head-top.js";
const { createEditor, createToolbar } = window.wangEditor;
const { createEditor, createToolbar, SlateTransforms, Boot, SlateEditor } = window.wangEditor;
console.log("createEditor", createEditor);
class MyButtonMenu { // JS 语法
constructor() {
this.title = '居中' // 自定义菜单标题
// this.iconSvg = '<svg>...</svg>' // 可选
this.tag = 'button'
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(editor) {
// console.log("getValue", editor);
// TS 语法
// getValue(editor) { // JS 语法
return ' hello '
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor) {
console.log("isActive", editor.getFragment()?.[0]);
if (editor.getFragment()?.[0]?.textAlign == 'center') return true
return false
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor) {
// TS 语法
// isDisabled(editor) { // JS 语法
return false
}
// 点击菜单时触发的函数
exec(editor, value) {
// center
let align = this.isActive(editor) ? 'left' : 'center'
console.log("align", this.isActive(editor));
SlateTransforms.setNodes(editor, {
textAlign: align
},)
}
}
const editApp = createApp({
setup() {
let titleLength = ref(200);
@@ -160,15 +205,6 @@ const editApp = createApp({
if (!src) {
return;
}
// 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); // 文件名
await setTimeout(() => {
console.log("1111");
return true;
@@ -189,47 +225,26 @@ const editApp = createApp({
// 3. 返回 undefined即没有任何返回说明检查未通过编辑器会阻止插入。但不会提示任何信息
}
// 【新增】判断节点的对齐方式
const getNodeAlign = (node) => {
if (!node) return "left"; // 默认居左
// 获取节点的text-align样式优先内联样式再取CSS计算样式
const inlineAlign = node.style.textAlign;
if (inlineAlign) return inlineAlign;
// // 自定义转换视频
// const customParseVideoSrc = (src) => {
// console.log("customParseVideoSrc", this);
const computedStyle = window.getComputedStyle(node);
return computedStyle.textAlign || "left";
};
// // TS 语法
// // function customParseVideoSrc(src) { // JS 语法
// if (src.includes('.bilibili.com')) {
// // 转换 bilibili url 为 iframe (仅作为示例,不保证代码正确和完整)
// const arr = location.pathname.split('/')
// const vid = arr[arr.length - 1]
// return `<iframe src="//player.bilibili.com/player.html?bvid=${vid}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`
// }
// // return src
// return `<iframe src="${src}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`
// 【新增】切换对齐方式(居中 ↔ 居左)
const toggleAlign = () => {
const editorInst = editor.value;
if (!editorInst) return;
// 禁用编辑器默认的居中命令
editorInst.off("clickToolbar", "justifyCenter");
// 获取当前选中的节点(优先段落/块级节点)
const selectedNode = getSelectedNode(editorInst);
const blockNode = DomEditor.getClosestBlock(selectedNode); // 获取块级节点p/div等
if (!blockNode) return;
// 判断当前对齐方式
const currentAlign = getNodeAlign(blockNode);
// 切换对齐:居中 → 居左;其他 → 居中
const newAlign = currentAlign === "center" ? "left" : "center";
// 设置节点对齐样式
editorInst.restoreSelection(); // 恢复选区
blockNode.style.textAlign = newAlign;
// 触发编辑器更新
editorInst.change();
editorInst.focus(); // 保持焦点
};
// // return `<video contenteditable="false" src="${src}" controls></video>`;
// }
const editorConfig = {
placeholder: "Type here...",
placeholder: "输入正文",
enabledMenus: [],
MENU_CONF: {
["emotion"]: {
@@ -293,7 +308,7 @@ const editApp = createApp({
ajax(config.url, formData).then((res) => {
const data = res.data;
console.log("上传成功:", data);
insertFn(data.url); // 传入图片的可访问 URL
insertFn(`${data.url}?aid=${data.aid}`); // 传入图片的可访问 URL
});
} catch (err) {
console.error("上传出错:", err);
@@ -301,6 +316,19 @@ const editApp = createApp({
},
},
['insertVideo']: {
onInsertedVideo(videoNode) {
// TS 语法
// onInsertedVideo(videoNode) { // JS 语法
if (videoNode == null) return
const { src } = videoNode
console.log('inserted video', src)
},
// checkVideo: customCheckVideoFn, // 也支持 async 函数
// parseVideoSrc: (src) => customParseVideoSrc(src), // 也支持 async 函数
},
["uploadVideo"]: {
server: uConfigData.url,
@@ -343,26 +371,14 @@ const editApp = createApp({
// 步骤3再上传第一帧封面type 传 'cover',按后端要求调整)
const coverUploadRes = await uploading(coverFile, coverFile.name, "image");
console.log("封面上传成功", coverUploadRes);
console.log(insertFn);
insertFn(videoUploadRes.url, coverUploadRes.url);
insertFn(`${videoUploadRes.url}?aid=${videoUploadRes.aid}`, `${coverUploadRes.url}?aid=${coverUploadRes.aid}`);
} catch (err) {
console.error("上传出错:", err);
}
},
},
["justifyCenter"]: {
onClick: (editor) => {
console.log("editor", editor);
toggleAlign(); // 替换为自定义切换逻辑
},
// 【可选】自定义居中按钮的激活状态(选中时高亮)
isActive: (editor) => {
const selectedNode = getSelectedNode(editor);
const blockNode = DomEditor.getClosestBlock(selectedNode);
return blockNode && getNodeAlign(blockNode) === "center";
},
},
},
onChange(editor) {
@@ -404,16 +420,34 @@ const editApp = createApp({
// "uploadVideo", // 插入视频
"insertLink", // 插入链接
"bold", // 粗体
"justifyCenter",
// "justifyCenter",
],
};
const menu1Conf = {
key: 'customCenter', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new MyButtonMenu() // 把 `YourMenuClass` 替换为你菜单的 class
},
}
Boot.registerMenu(menu1Conf)
console.log(toolbarConfig, "toolbarConfig");
toolbarConfig.insertKeys = {
index: 7, // 插入的位置,基于当前的 toolbarKeys
keys: ['customCenter'],
}
const toolbar = createToolbar({
editor,
selector: "#toolbar-container",
config: toolbarConfig,
mode: "default",
});
console.log("editor.commands", editor);
nextTick(() => {
@@ -447,10 +481,10 @@ const editApp = createApp({
boldItem.classList.add("toolbar-item", "flexacenter");
bold.innerHTML = '<img class="icon" src="{@/img/bold-icon.png}" alt="粗体" /> <span>粗体</span>';
const justifyCenter = toolbarRef.value.querySelector('[data-menu-key="justifyCenter"]');
const justifyCenterItem = justifyCenter.parentElement;
justifyCenterItem.classList.add("toolbar-item", "flexacenter");
justifyCenter.innerHTML = '<img class="icon" src="{@/img/justify-center-icon.png}" alt="居中" /> <span>居中</span>';
const customCenter = toolbarRef.value.querySelector('[data-menu-key="customCenter"]');
const customCenterItem = customCenter.parentElement;
customCenterItem.classList.add("toolbar-item", "flexacenter");
customCenter.innerHTML = '<img class="icon" src="{@/img/justify-center-icon.png}" alt="居中" /> <span>居中</span>';
});
};
@@ -549,7 +583,7 @@ const editApp = createApp({
try {
const DomEditor = window.wangEditor && window.wangEditor.DomEditor;
if (DomEditor && editor) node = DomEditor.getSelectionNode(editor);
} catch (e) {}
} catch (e) { }
if (!node) {
const sel = window.getSelection();
if (sel && sel.rangeCount) node = sel.getRangeAt(0).commonAncestorContainer;
@@ -844,7 +878,7 @@ const editApp = createApp({
let format = ref("");
const submit = (status) => {
const infoTarget = { ...info.value } || {};
let content = editorRef.value.innerHTML;
let content = editor.getHtml()
const images = extractImages(editorRef.value);
const videos = extractVideos(editorRef.value);
@@ -925,14 +959,20 @@ const editApp = createApp({
const imgElements = dom.querySelectorAll("img");
imgElements.forEach((imgEl) => {
const url = imgEl.getAttribute("src")?.trim() || "";
const aid = imgEl.dataset.aid?.trim() || ""; // 用 dataset 简化自定义属性读取
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;
images.push({
url,
url: cleanUrl,
aid: Number(aid),
});
});
console.log("提取完成的图片列表:", images);
return images;
};
@@ -943,16 +983,27 @@ const editApp = createApp({
// 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自定义属性
const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 视频地址
// 1. 用 URL 构造函数解析链接(自动处理查询参数)
const urlObj = new URL(posterurl);
// 2. 获取 aid 参数get 方法找不到时返回 null
const posterid = urlObj.searchParams.get('aid');
const sourceEl = videoEl.querySelector('source');
const url = sourceEl.getAttribute('src') || null;
const obj = new URL(url);
// 2. 获取 aid 参数get 方法找不到时返回 null
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,
posterurl,
url: cleanUrl,
posterurl: cleanPosterurl,
});
});
@@ -1094,14 +1145,21 @@ const editApp = createApp({
});
};
const linkClick = () => {};
const linkClick = () => { };
const overstriking = () => {
console.log("加粗");
editor.addMark("bold", true); // 加粗
// editor.addMark("bold", true); // 加粗
// editor.addMark("color", "#999"); // 文本颜色
console.log("editor", editor.addMark);
// console.log("editor", editor.addMark);
// editor.addMark('justifyCenter', true) // 加粗
console.log(SlateTransforms.setNodes, editor);
SlateTransforms.setNodes(editor, {
textAlign: 'right'
},)
};
return { toolbarRef, overstriking, isH1, 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 };