文章类型: 原创阅读时长: 1分钟
文章地址:
文章在 7/29/2025 修改过
封装
<script setup lang="ts">
import Quill, { Delta, type EmitterSource, type QuillOptions, Range } from "quill";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import "quill/dist/quill.snow.css";
const props = defineProps<{
// HTML model value, supports v-model
modelValue?: string | null;
// Quill initialization options
options?: QuillOptions;
semantic?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
(e: "text-change", { delta, oldContent, source }: { delta: Delta; oldContent: Delta; source: EmitterSource }): void;
(e: "selection-change", { range, oldRange, source }: { range: Range; oldRange: Range; source: EmitterSource }): void;
(e: "editor-change", eventName: "text-change" | "selection-change"): void;
(e: "blur", quill: Quill): void;
(e: "focus", quill: Quill): void;
(e: "ready", quill: Quill): void;
}>();
let quillInstance: Quill | null = null;
const container = ref<HTMLElement>();
const model = ref<string | null>();
// Convert modelValue HTML to Delta and replace editor content
const pasteHTML = (quill: Quill) => {
model.value = props.modelValue;
const oldContent = quill.getContents();
const delta = quill.clipboard.convert({ html: props.modelValue ?? "" });
quill.setContents(delta);
emit("text-change", { delta, oldContent, source: "api" });
};
// Editor initialization, returns Quill instance
const initialize = () => {
const quill = new Quill(container.value as HTMLElement, props.options);
// Set editor initial model
if (props.modelValue) {
pasteHTML(quill);
}
// Handle editor selection change, emit blur and focus
quill.on("selection-change", (range, oldRange, source) => {
if (!range) {
emit("blur", quill);
} else {
emit("focus", quill);
}
emit("selection-change", { range, oldRange, source });
});
// Handle editor text change
quill.on("text-change", (delta, oldContent, source) => {
model.value = props.semantic ? quill.getSemanticHTML() : quill.root.innerHTML;
emit("text-change", { delta, oldContent, source });
});
// Handle editor change
quill.on("editor-change", (eventName: "text-change" | "selection-change") => {
emit("editor-change", eventName);
});
emit("ready", quill);
quillInstance = quill;
return quill;
};
// Watch modelValue and paste HTML if has changes
watch(
() => props.modelValue,
(newValue) => {
if (!quillInstance) return;
if (newValue && newValue !== model.value) {
pasteHTML(quillInstance);
// Update HTML model depending on type
model.value = props.semantic ? quillInstance.getSemanticHTML() : quillInstance.root.innerHTML;
} else if (!newValue) {
quillInstance.setContents([]);
}
}
);
// Watch model and update modelValue if has changes
watch(model, (newValue, oldValue) => {
if (!quillInstance) return;
if (newValue && newValue !== oldValue) {
emit("update:modelValue", newValue);
} else if (!newValue) {
quillInstance.setContents([]);
}
});
function getQuillInstance() {
return quillInstance;
}
onMounted(() => {
initialize();
console.log("init");
});
onBeforeUnmount(() => {
quillInstance = null;
});
// Expose init function
defineExpose<{
getQuillInstance: () => Quill | null;
}>({
getQuillInstance,
});
</script>
<template>
<div ref="container"></div>
</template>
<style>
.ql-container {
width: 100%;
min-height: 300px;
}
.ql-editor {
}
</style>
使用
<script setup lang="ts">
import { ref } from "vue";
import type { QuillOptions } from "quill";
import Quill from "quill";
import quill from "./components/quill.vue";
import { Mention, MentionBlot } from "quill-mention"; // 插件
import "quill-mention/dist/quill.mention.min.css"; // 样式
Quill.register(MentionBlot);
Quill.register("modules/mention", Mention);
const model = ref("");
const quillRef = ref();
const options: QuillOptions = {
theme: "snow",
placeholder: "Start your story here...",
readOnly: false,
modules: {
toolbar: {
container: [["bold", "italic", "underline"], [{ list: "ordered" }, { list: "bullet" }], [{ align: [] }], ["link", "image"]],
handlers: {
image: imageHandler, // 正确放置 handlers
},
},
mention: {
allowedChars: /^[\u4e00-\u9fa5\w]*$/, // 允许中文/英文/数字/下划线
mentionDenotationChars: ["@"], // 触发字符
source(searchTerm: any, renderList: any, mentionChar: any) {
// 这里可以换成后端接口
const users = [
{ id: 1, value: "张三" },
{ id: 2, value: "李四" },
];
const matches = users.filter((u) => u.value.toLowerCase().includes(searchTerm.toLowerCase()));
renderList(matches, searchTerm);
},
renderItem(item: any) {
return `${item.value}`;
},
onSelect(item: any, insertItem: any) {
// 插入后自动加个空格,体验更好
insertItem({ ...item, value: `@${item.value} ` });
},
},
},
};
function imageHandler() {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
// 模拟上传逻辑,实际应替换为真实上传接口
const formData = new FormData();
formData.append("image", file);
const quillInstance = quillRef.value.getQuillInstance();
const range = quillInstance.getSelection();
quillInstance.insertEmbed(range.index, "image", "xxxxx");
};
}
</script>
<template>
<div class="container">
<quill ref="quillRef" v-model="model" :options="options"></quill>
</div>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
box-sizing: border-box;
padding: 20px;
}
</style>
Tags
Recomends
Comments
登录
暂无评论,快来抢沙发吧!