1、下载Vditor插件
npm i vditor
我的vditor版本是3.10.2,大家可以自行选择下载最新版本
官网:Vditor 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏 - 链滴
示例:
Vditor 示例 - 链滴
2、引入vue文件
import Vditor from "vditor";
import "vditor/dist/index.css";
3、封装vditor自定义组件
<template>
<div>
<div
:id="vidtorId"
class="vditor"
:class="{ 'vditor-hidden': showPreview }"
></div>
</div>
</template>
<script setup>
import Vditor from "vditor";
import "vditor/dist/index.css";
import { getToken } from "@/utils/auth";
const { proxy } = getCurrentInstance();
const props = defineProps({
height: {
type: [Number, String],
default: "inherit",
},
modelValue: {
type: String,
default: "",
},
knwlgId: {
type: String,
default: "",
},
showPreview: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
// 额外的formData参数
extraData: {
type: Object,
default: () => ({}),
},
uploadURL: {
type: String,
default: "",
},
previewURL: {
type: String,
default: "/ekms/images/v1/preview/",
},
placeholder: {
type: String,
default: "",
},
});
const vidtorId = ref(
"vidtor-" + +new Date() + ((Math.random() * 1000).toFixed(0) + "")
);
// 图片上传地址判断,这里大家可以根据自己需求写死或通过props传入
const imgUploadUrl = computed(() => {
return !props.uploadURL
? `/ekms/images/v1/upload?knwlgId=${props.knwlgId}`
: props.uploadURL;
});
watch(
() => props.showPreview,
(newValue, oldValue) => {
let previewDom =
contentEditor.value.vditor.toolbar.elements.preview.firstChild;
let isPreview = previewDom.className.indexOf("vditor-menu--current") > -1;
emits("update:showPreview", isPreview ? false : true);
previewDom.click();
}
);
const emits = defineEmits([
"update:modelValue",
"update:showPreview",
"setHtml",
]);
const contentEditor = ref(null);
onMounted(() => {
contentEditor.value = new Vditor(vidtorId.value, {
height: props.height,
mode: "wysiwyg", //所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv)
value: props.modelValue,
cdn: import.meta.env.VITE_APP_VDITOR_API,
placeholder: props.placeholder,
toolbarConfig: {
pin: true,
// hide: true,
},
// outline: {
// enable: true, //展示大纲,position默认left
// },
cache: {
enable: false, // 是否使用 localStorage 进行缓存
},
preview: {
mode: "both", //显示模式
delay: 10,
actions: [],
theme: {
path: `${import.meta.env.VITE_APP_VDITOR_API}/dist/css/content-theme`,
},
},
toolbar: [
"emoji",
"headings",
"bold",
"italic",
"strike",
"link",
"|",
"list",
"ordered-list",
"check",
"outdent",
"indent",
"|",
"quote",
"line",
"code",
"inline-code",
"insert-before",
"insert-after",
"|",
{
//自定义上传
hotkey: "",
name: "upload",
tip: "上传图片",
className: "right",
},
"table",
"|",
"undo",
"redo",
"|",
"code-theme",
"content-theme",
"preview",
// 自定义清空内容的菜单
{
hotkey: "⇧⌘S",
name: "clearAll",
tipPosition: "n",
tip: "清空内容",
className: "right",
icon: '<svg t="1696926237451" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2371" width="180" height="180"><path d="M512 838.858c10.89 0 19.732-9.158 19.732-20.43v-490.275c0-11.273-8.842-20.43-19.732-20.43s-19.755 9.157-19.755 20.43v490.275c0 11.272 8.842 20.43 19.755 20.43M629.877 838.813c10.935 0.428 20.138-8.37 20.475-19.688l28.665-489.69c0.427-11.272-8.077-20.745-18.99-21.195-10.935-0.405-20.137 8.415-20.475 19.688l-28.665 489.713c-0.405 11.317 8.1 20.767 18.99 21.172M848.038 185.142h-197.708v-81.653c0-22.545-17.685-40.882-39.51-40.882h-197.64c-21.87 0-39.532 18.338-39.532 40.882v81.653h-197.685c-10.913 0-19.755 9.158-19.755 20.475 0 11.272 8.843 20.407 19.755 20.407h39.577l39.488 653.67c6.367 44.73 35.415 81.72 79.065 81.72h355.793c43.65 0 71.573-37.44 79.088-81.72l39.488-653.67h39.578c10.867 0 19.755-9.135 19.755-20.408 0-11.317-8.888-20.475-19.755-20.475M413.157 103.49h197.64v81.653h-197.64v-81.653zM729.418 879.695c-2.655 21.555-17.73 40.86-39.533 40.86h-355.793c-21.87 0-36.54-19.057-39.532-40.86l-39.532-653.67h513.945l-39.555 653.67zM394.145 838.858c10.89-0.473 19.373-9.9 18.99-21.195l-29.070-489.712c-0.427-11.273-9.585-20.070-20.475-19.665-10.913 0.428-19.463 9.9-19.013 21.173l29.093 489.712c0.36 11.295 9.54 20.070 20.475 19.688z" p-id="2372"></path></svg>',
click() {
contentEditor.value.setValue("");
},
},
],
// vditor结构渲染完成后
after: () => {
emits("setHtml", getValue(), getHTML());
// 这里根据传入的disabled来设置vditor是否可输入内容
props.disabled && contentEditor.value.disabled();
},
input: () => {
// 变更事件回调
emits("setHtml", getValue(), getHTML());
},
// 上传图片
upload: {
accept: "image/*", // 图片类型
fieldName: "file",
url: import.meta.env.VITE_APP_BASE_API + imgUploadUrl.value, // 图片上传的接口路径
extraData: props.extraData, // 要携带的额外的formData参数
headers: {
Authorization: "Bearer " + getToken(), // 用户身份信息
},
filename(name) { // 过滤特殊字符
return name
.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, "")
.replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, "")
.replace("/\\s/g", "");
},
// 粘贴图片的情况下上传的接口路径
linkToImgUrl: import.meta.env.VITE_APP_BASE_API + imgUploadUrl.value,
max: 20 * 1024 * 1024,
multiple: false,
withCredentials: false,
// 图片上传校验,可以校验图片大小及类型
validate(files) {
const isLt2M = files[0].size / 1024 / 1024 < 20;
if (!isLt2M) {
proxy.$modal.msgError("上传图片大小不能超过 20MB!");
}
if (!files[0].type.includes("image")) {
return proxy.$modal.msgError("仅支持上传图片!");
}
},
//粘贴图片回显处理,如果有图片加了防盗链,则让后台代理替换成自己的图片
linkToImgFormat(responseText) {
let res = JSON.parse(responseText);
if (!res.data?.imgId) return;
let end = JSON.stringify({
msg: "",
code: 0,
data: {
originalURL:
import.meta.env.VITE_APP_BASE_API +
props.previewURL +
res.data?.imgId, //图片原始地址记录到本地文件中,可以防止跨站点调用。
url:
import.meta.env.VITE_APP_BASE_API +
props.previewURL +
res.data?.imgId, //图片链接记录到本地文件中,可以防止跨站点调用。
},
});
return end;
},
format(files, responseText) {
var res = JSON.parse(responseText);
if (!res.data?.imgId) return;
//图片回显
nextTick(() => {
emits("setHtml", getValue(), getHTML());
});
return JSON.stringify({
msg: "",
code: 0,
data: {
errFiles: [],
succMap: {
[res.data.imgPath]:
import.meta.env.VITE_APP_BASE_API +
props.previewURL +
res.data?.imgId,
},
},
});
},
},
});
});
function getValue() {
return contentEditor.value.getValue(); //获取 Markdown 内容
}
// 获取html结构
function getHTML() {
let dom = document.querySelector(
".vditor-content .vditor-wysiwyg .vditor-reset"
)?.innerHTML;
return dom;
}
function setValue(value) {
return contentEditor.value.setValue(value); //设置 Markdown 内容
}
defineExpose({
getHTML,
setValue,
});
</script>