复制粘贴修改一下直接用,也写了相关的注释。
一、安装相关插件
npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue
二、官方关键文档
- ButtonMenu:https://www.wangeditor.com/v5/development.html#buttonmenu
- 注册菜单到wangEditor:自定义扩展新功能 | wangEditor
- insertKeys自定义功能的keys:https://www.wangeditor.com/v5/toolbar-config.html#insertkeys
- 自定义上传图片视频功能:菜单配置 | wangEditor
- 源码地址:GitHub - wangeditor-team/wangEditor: wangEditor —— 开源 Web 富文本编辑器
三、初始化编辑器(wangEdit.vue)
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="html"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@onChange="onChange"
/>
</div>
</template>
<script>
// import Location from "@/utils/location";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { Boot, IModuleConf, DomEditor } from "@wangeditor/editor";
import { getToken } from "@/utils/auth";
import MyPainter from "./geshi";
const menu1Conf = {
key: "geshi", // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new MyPainter(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
const module = {
// JS 语法
menus: [menu1Conf],
// 其他功能,下文讲解...
};
Boot.registerModule(module);
export default {
components: { Editor, Toolbar },
props: {
relationKey: {
type: String,
default: "",
},
},
created() {
console.log(this.editorConfig.MENU_CONF.uploadImage.meta.activityKey);
},
data() {
return {
// 富文本实例
editor: null,
// 富文本正文内容
html: "",
// 编辑器模式
mode: "default", // or 'simple'
// 工具栏配置
toolbarConfig: {
//新增菜单
insertKeys: {
index: 32,
keys: ["geshi"],
},
//去掉网络上传图片和视频
excludeKeys: ["insertImage", "insertVideo"],
},
// 编辑栏配置
editorConfig: {
placeholder: "请输入相关内容......",
// 菜单配置
MENU_CONF: {
// ===================
// 上传图片配置
// ===================
uploadImage: {
// 文件名称
fieldName: "contentAttachImage",
server: Location.serverPath + "/editor-upload/upload-image",
headers: {
Authorization: "Bearer " + getToken(),
},
meta: {
activityKey: this.relationKey,
},
// 单个文件的最大体积限制,默认为 20M
maxFileSize: 20 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["image/*"],
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000,
// 自定义插入图片操作
customInsert: (res, insertFn) => {
if (res.errno == -1) {
this.$message.error("上传失败!");
return;
}
insertFn(Location.serverPath + res.data.url, "", "");
this.$message.success("上传成功!");
},
},
// =====================
// 上传视频配置
// =====================
uploadVideo: {
// 文件名称
fieldName: "contentAttachVideo",
server: Location.serverPath + "/editor-upload/upload-video",
headers: {
Authorization: "Bearer " + getToken(),
},
meta: {
activityKey: this.relationKey,
},
// 单个文件的最大体积限制,默认为 60M
maxFileSize: 60 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 3,
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["video/*"],
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 15 * 1000,
// 自定义插入图片操作
customInsert: (res, insertFn) => {
if (res.errno == -1) {
this.$message.error("上传失败!");
return;
}
insertFn(Location.serverPath + res.data.url, "", "");
this.$message.success("上传成功!");
},
},
},
},
// ===== data field end =====
};
},
methods: {
// =============== Editor 事件相关 ================
// 1. 创建 Editor 实例对象
onCreated(editor) {
this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
this.$nextTick(() => {
const toolbar = DomEditor.getToolbar(this.editor);
const curToolbarConfig = toolbar.getConfig();
console.log("【 curToolbarConfig 】-39", curToolbarConfig);
});
},
// 2. 失去焦点事件
onChange(editor) {
this.$emit("change", this.html);
},
// =============== Editor操作API相关 ==============
insertText(insertContent) {
const editor = this.editor; // 获取 editor 实例
if (editor == null) {
return;
}
// 执行Editor的API插入
editor.insertText(insertContent);
},
// =============== 组件交互相关 ==================
// closeEditorBeforeComponent() {
// this.$emit("returnEditorContent", this.html);
// },
closeContent(){
this.html=''
},
// ========== methods end ===============
},
mounted() {
// ========== mounted end ===============
},
beforeDestroy() {
const editor = this.editor;
if (editor == null) {
return;
}
editor.destroy();
console.log("销毁编辑器!");
},
};
</script>
<style lang="scss" scoped>
// 对默认的p标签进行穿透
::v-deep .editorStyle .w-e-text-container [data-slate-editor] p {
margin: 0 !important;
}
</style>
<style src="@wangeditor/editor/dist/css/style.css"></style>
自定义上传图片接口
uploadImage: {
// 文件名称
fieldName: "contentAttachImage",
// server: '/api/v1/public/uploadFile',
headers: {
Authorization: "Bearer " + getToken(),
},
meta: {
activityKey: this.relationKey,
},
// 单个文件的最大体积限制,默认为 20M
maxFileSize: 20 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["image/*"],
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000,
这里设置
customUpload: async (file, insertFn) => {
console.log(file, "file");
let formData = new FormData()
const sub = "order";
formData.append('file', file)
formData.append("sub", sub);
formData.append("type", "1");
let res = await getUploadImg(formData)
insertFn(res.data.full_path, '', '');
},
customInsert: (res, insertFn) => {
if (res.errno == -1) {
this.$message.error("上传失败!");
return;
}
// insertFn(res.data.url, "", "");
this.$message.success("上传成功!");
},
},
四、格式刷功能类js文件
import {
SlateEditor,
SlateText,
SlateElement,
SlateTransforms,
DomEditor,
// Boot,
} from "@wangeditor/editor";
// Boot.registerMenu(menu1Conf);
import { Editor } from "slate";
export default class MyPainter {
constructor() {
this.title = "格式刷"; // 自定义菜单标题
// 这里是设置格式刷的样式图片跟svg都可以,但是注意要图片大小要小一点,因为要应用到鼠标手势上
this.iconSvg = ``;
this.tag = "button"; //注入的菜单类型
this.savedMarks = null; //保存的样式
this.domId = null; //这个可要可不要
this.editor = null; //编辑器示例
this.parentStyle = null; //储存父节点样式
this.mark = "";
this.marksNeedToRemove = []; // 增加 mark 的同时,需要移除哪些 mark (互斥,不能共存的)
}
clickHandler(e) {
console.log(e, "e"); //无效
}
//添加或者移除鼠标事件
addorRemove = (type) => {
const dom = document.body;
if (type === "add") {
dom.addEventListener("mousedown", this.changeMouseDown);
dom.addEventListener("mouseup", this.changeMouseup);
} else {
//赋值完需要做的清理工作
this.savedMarks = undefined;
dom.removeEventListener("mousedown", this.changeMouseDown);
dom.removeEventListener("mouseup", this.changeMouseup);
document.querySelector("#w-e-textarea-1").style.cursor = "auto";
}
};
//处理重复键名值不同的情况
handlerRepeatandNotStyle = (styles) => {
const addStyles = styles[0];
const notVal = [];
for (const style of styles) {
for (const key in style) {
const value = style[key];
if (!addStyles.hasOwnProperty(key)) {
addStyles[key] = value;
} else {
if (addStyles[key] !== value) {
notVal.push(key);
}
}
}
}
for (const key of notVal) {
delete addStyles[key];
}
return addStyles;
};
// 获取当前选中范围的父级节点
getSelectionParentEle = (type, func) => {
if (this.editor) {
const parentEntry = SlateEditor.nodes(this.editor, {
match: (node) => SlateElement.isElement(node),
});
let styles = [];
for (const [node] of parentEntry) {
styles.push(this.editor.toDOMNode(node).style); //将node对应的DOM对应的style对象加入到数组
}
styles = styles.map((item) => {
//处理不为空的style
const newItem = {};
for (const key in item) {
const val = item[key];
if (val !== "") {
newItem[key] = val;
}
}
return newItem;
});
type === "get"
? func(type, this.handlerRepeatandNotStyle(styles))
: func(type);
}
};
//获取或者设置父级样式
getorSetparentStyle = (type, style) => {
if (type === "get") {
this.parentStyle = style; //这里是个样式对象 例如{textAlign:'center'}
} else {
SlateTransforms.setNodes(
this.editor,
{ ...this.parentStyle },
{
mode: "highest", // 针对最高层级的节点
}
);
}
};
//这里是将svg转换为Base64格式
addmouseStyle = () => {
const icon = ``; // 这里是给鼠标手势添加图标
// 将字符串编码为Base64格式
const base64String = btoa(icon);
// 生成数据URI
const dataUri = `data:image/svg+xml;base64,${base64String}`;
// 将数据URI应用于鼠标图标
document.querySelector(
"#w-e-textarea-1"
).style.cursor = `url('${dataUri}'), auto`;
};
changeMouseDown = () => {}; //鼠标落下
changeMouseup = () => {
//鼠标抬起
if (this.editor) {
const editor = this.editor;
const selectTxt = editor.getSelectionText(); //获取文本是否为null
if (this.savedMarks && selectTxt) {
//先改变父节点样式
this.getSelectionParentEle("set", this.getorSetparentStyle);
// 获取所有 text node
const nodeEntries = SlateEditor.nodes(editor, {
//nodeEntries返回的是一个迭代器对象
match: (n) => SlateText.isText(n), //这里是筛选一个节点是否是 text
universal: true, //当universal为 true 时,Editor.nodes会遍历整个文档,包括根节点和所有子节点,以匹配满足条件的节点。当universal为 false 时,Editor.nodes只会在当前节点的直接子节点中进行匹配。
});
// 先清除选中节点的样式
for (const node of nodeEntries) {
const n = node[0]; //{text:xxxx}
const keys = Object.keys(n);
keys.forEach((key) => {
if (key === "text") {
// 保留 text 属性
return;
}
// 其他属性,全部清除
SlateEditor.removeMark(editor, key);
});
}
// 再赋值新样式
for (const key in this.savedMarks) {
if (Object.hasOwnProperty.call(this.savedMarks, key)) {
const value = this.savedMarks[key];
editor.addMark(key, value);
}
}
this.addorRemove("remove");
}
}
};
getValue(editor) {
// return "MyPainter"; // 标识格式刷菜单
const mark = this.mark;
console.log(mark, "mark");
const curMarks = Editor.marks(editor);
// 当 curMarks 存在时,说明用户手动设置,以 curMarks 为准
if (curMarks) {
return curMarks[mark];
} else {
const [match] = Editor.nodes(editor, {
// @ts-ignore
match: (n) => n[mark] === true,
});
return !!match;
}
}
isActive(editor, val) {
const isMark = this.getValue(editor);
return !!isMark;
// return !!DomEditor.getSelectedNodeByType(editor, "geshi");
// return false;
}
isDisabled(editor) {
//是否禁用
return false;
}
exec(editor, value) {
//当菜单点击后触发
// console.log(!this.isActive());
console.log(value, "value");
this.editor = editor;
this.domId = editor.id.split("-")[1]
? `w-e-textarea-${editor.id.split("-")[1]}`
: undefined;
if (this.isDisabled(editor)) return;
const { mark, marksNeedToRemove } = this;
if (value) {
// 已,则取消
editor.removeMark(mark);
} else {
// 没有,则执行
editor.addMark(mark, true);
this.savedMarks = SlateEditor.marks(editor); // 获取当前选中文本的样式
this.getSelectionParentEle("get", this.getorSetparentStyle); //获取父节点样式并赋值
// this.addmouseStyle(); //点击之后给鼠标添加样式
this.addorRemove("add"); //处理添加和移除事件函数
// 移除互斥、不能共存的 marks
if (marksNeedToRemove) {
marksNeedToRemove.forEach((m) => editor.removeMark(m));
}
}
if (
editor.isEmpty() ||
editor.getHtml() == "<p><br></p>" ||
editor.getSelectionText() == ""
)
return; //这里是对没有选中或者没内容做的处理
}
}
五、页面应用组件
<el-form-item label="内容">
<WangEdit v-model="form.content" ref="wangEdit" @change="change"></WangEdit>
</el-form-item>
// js
const WangEdit = () => import("@/views/compoments/WangEdit.vue");
export default {
name: "Notice",
components: {
WangEdit,
},
data(){
return{
form:{
}
}
},
methods: {
change(val) {
console.log(val,'aa');
this.form.content=val
},
// 取消按钮
cancel() {
this.open = false;
this.form={};
this.$refs.wangEdit.closeContent();
},
}