feat(editor): 实现自定义居中按钮并优化媒体上传处理
- 添加自定义居中按钮功能,替换原生的居中实现 - 修改图片和视频上传后的URL格式,附加aid参数 - 优化媒体资源提取逻辑,处理URL中的查询参数 - 调整编辑器工具栏的z-index样式 - 移除不必要的position属性
This commit is contained in:
@@ -86,7 +86,7 @@
|
||||
padding-left: 25px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
}
|
||||
#edit .edit-container #editor—wrapper .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 #editor—wrapper .editor-toolbar .toolbar-item .icon {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
<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>
|
||||
|
||||
@@ -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> -->
|
||||
|
||||
220
js/edit.js
220
js/edit.js
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user