继上篇vue2集成quill-editor,本文实现vue3集成相同功能,基于狮子大佬 ruoyi-vue-plus 框架。
安装vue-quill
npm install @vueup/vue-quill@alpha --save
自定义选人插件开发
<div
id="myPanel"
v-if="showMetric"
style="
position: absolute;
top: 80px;
left: 10px;
background-color: #ffffff;
"
>
<el-card
class="box-card"
style="height: 150px; overflow-y: auto; overflow-x: hidden"
>
<div
v-for="(item, index) in userList"
:key="index"
class="box-div"
style="width: 210px; height: 30px; background-color: #ffffff"
>
<el-button type="text" @click="handleClick(item)">
<span style="float: left; width: 50px">{{ item.name }}</span>
<span
style="
float: right;
color: #8492a6;
font-size: 13px;
width: 150px;
text-align: right;
"
>{{ item.mobile }}</span
>
</el-button>
</div>
</el-card>
</div>
引入js
import LinkUserBlot from "./LinkUserBlot.js";
Quill.register(LinkUserBlot, true);
LinkFileBlot.js
/**
* LinkUserBlot.js
*/
import {
Quill
} from '@vueup/vue-quill'
const Embed = Quill.import("blots/embed");
class LinkUserBlot extends Embed {
static create(value) {
// console.log('value', value)
let node = super.create();
node.innerHTML = value.text
node.setAttribute('id', value.id);
node.setAttribute('contenteditable', false);
node.setAttribute('class', 'at-some-one');
// @人样式不受影响
node.setAttribute('style', 'color:#4498F0;text-decoration: none;display: inline-block;font-style: normal;background-color: #fff;margin: 0 2px;');
return node;
}
static value(node) {
return {
id: node.getAttribute('id'),
text: node.innerHTML.trim()
};
}
}
LinkUserBlot.blotName = 'atusertag';
LinkUserBlot.tagName = 'span';
LinkUserBlot.className = 'user-at-span';
export default LinkUserBlot
自定义选人插件参数
/** 自定义选人插件参数
* showMetric 控制选人下拉框显示
* userList 模拟用户选项
* */
const showMetric = ref(false);
const userList = ref([
{id:1, name: "赵", mobile: "111"},
{id:2, name: "钱", mobile: "222"},
{id:3, name: "孙", mobile: "333"},
{id:4, name: "李", mobile: "444"}
]);
编辑器工具栏增加配置
配置图标样式
.ql-insertMetric {
background: url("../../assets/icons/user-solid.png") !important ;
background-size: 18px 18px !important;
background-position: center center !important;
background-repeat: no-repeat !important;
}
选人显示文本框内
/** 自定义选人插件 **/
function handleClick(row) {
insert(row);
showMetric.value = false;
}
function insert(user) {
const name = user.name;
let quill = toRaw(quillEditorRef.value).getQuill();
let length = quill.selection.savedRange.index;
quill.insertEmbed(
length,
"atusertag",
{ text: `@${name} `, id: user.id },
Quill.sources.USER
);
quill.insertText(length + 1, " ");
quill.setSelection(length + 2);
quill.focus();
quill.update();
}
整体代码直接复制
<template>
<div>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
class="editor-img-uploader"
name="file"
:show-file-list="false"
:headers="headers"
ref="uploadRef"
v-if="type == 'url'"
>
</el-upload>
<div class="editor">
<quill-editor
ref="quillEditorRef"
v-model:content="content"
contentType="html"
@textChange="(e) => $emit('update:modelValue', content)"
:options="options"
:style="styles"
class="quilleditor"
/>
<div
id="myPanel"
v-if="showMetric"
style="
position: absolute;
top: 80px;
left: 10px;
background-color: #ffffff;
"
>
<el-card
class="box-card"
style="height: 150px; overflow-y: auto; overflow-x: hidden"
>
<div
v-for="(item, index) in userList"
:key="index"
class="box-div"
style="width: 210px; height: 30px; background-color: #ffffff"
>
<el-button type="text" @click="handleClick(item)">
<span style="float: left; width: 50px">{{ item.name }}</span>
<span
style="
float: right;
color: #8492a6;
font-size: 13px;
width: 150px;
text-align: right;
"
>{{ item.mobile }}</span
>
</el-button>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup>
import { QuillEditor, Quill } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { getToken } from "@/utils/auth";
import LinkUserBlot from "./LinkUserBlot.js";
Quill.register(LinkUserBlot, true);
const props = defineProps({
/* 编辑器的内容 */
modelValue: {
type: String,
},
/* 高度 */
height: {
type: Number,
default: null,
},
/* 最小高度 */
minHeight: {
type: Number,
default: null,
},
/* 只读 */
readOnly: {
type: Boolean,
default: false,
},
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
},
/* 类型(base64格式、url格式) */
type: {
type: String,
default: "url",
}
});
const { proxy } = getCurrentInstance();
// 上传的图片服务器地址
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/system/oss/upload");
const headers = ref({ Authorization: "Bearer " + getToken() });
const quillEditorRef = ref();
/** 自定义选人插件参数*/
const showMetric = ref(false);
const userList = ref([
{id:1, name: "赵", mobile: "111"},
{id:2, name: "钱", mobile: "222"},
{id:3, name: "孙", mobile: "333"},
{id:4, name: "李", mobile: "444"}
]);
const options = ref({
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
// 工具栏配置
toolbar: {
container: [
["insertMetric"], //新添加的工具 - 自定义选人
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
["blockquote", "code-block"], // 引用 代码块
[{ list: "ordered" }, { list: "bullet"} ], // 有序、无序列表
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ align: [] }], // 对齐方式
["clean"], // 清除文本格式
["link", "image", "video"] // 链接、图片、视频
],
handlers: {
image: function (value) {
if (value) {
// 调用element图片上传
document.querySelector(".editor-img-uploader>.el-upload").click();
} else {
Quill.format("image", true);
}
},
shadeBox: null,
that: this,
insertMetric: function () {
/** 自定义选人插件 **/
showMetric.value = !showMetric.value;
},
},
}
},
placeholder: "请输入内容",
readOnly: props.readOnly,
});
const styles = computed(() => {
let style = {};
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`;
}
if (props.height) {
style.height = `${props.height}px`;
}
return style;
});
const content = ref("");
watch(() => props.modelValue, (v) => {
if (v !== content.value) {
content.value = v === undefined ? "<p></p>" : v;
}
}, { immediate: true });
// 图片上传成功返回图片地址
function handleUploadSuccess(res, file) {
// 如果上传成功
if (res.code == 200) {
// 获取富文本实例
let quill = toRaw(quillEditorRef.value).getQuill();
// 获取光标位置
let length = quill.selection.savedRange.index;
// 插入图片,res为服务器返回的图片链接地址
quill.insertEmbed(length, "image", res.data.url);
// 调整光标到最后
quill.setSelection(length + 1);
proxy.$modal.closeLoading();
} else {
proxy.$modal.loading(res.msg);
proxy.$modal.closeLoading();
}
}
// 图片上传前拦截
function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
const isJPG = type.includes(file.type);
//检验文件格式
if (!isJPG) {
proxy.$modal.msgError(`图片格式错误!`);
return false;
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
return true;
}
// 图片失败拦截
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
}
/** 自定义选人插件 **/
function handleClick(row) {
insert(row);
showMetric.value = false;
}
function insert(user) {
const name = user.name;
let quill = toRaw(quillEditorRef.value).getQuill();
let length = quill.selection.savedRange.index;
quill.insertEmbed(
length,
"atusertag",
{ text: `@${name} `, id: user.id },
Quill.sources.USER
);
quill.insertText(length + 1, " ");
quill.setSelection(length + 2);
quill.focus();
quill.update();
}
</script>
<style>
.editor-img-uploader {
display: none;
}
.editor, .ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.quill-img {
display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: "保存";
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px";
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体";
}
.ql-insertMetric {
background: url("../../assets/icons/user-solid.png") !important ;
background-size: 18px 18px !important;
background-position: center center !important;
background-repeat: no-repeat !important;
}
</style>
直接复制即可实现富文本编辑器+附件上传功能。