feat: add rich text help article editor

This commit is contained in:
wushumin
2026-05-27 14:41:28 +08:00
parent 2ccbd0ffe4
commit 7d901fb435
17 changed files with 1311 additions and 114 deletions

View File

@@ -0,0 +1,220 @@
<script setup lang="ts">
import "@wangeditor/editor/dist/css/style.css";
import { computed, onBeforeUnmount, ref, shallowRef, watch } from "vue";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import type { IDomEditor, IEditorConfig, IToolbarConfig } from "@wangeditor/editor";
import { ElMessage } from "element-plus";
const props = withDefaults(defineProps<{
modelValue: string;
disabled?: boolean;
minHeight?: number;
uploadImage?: (file: File) => Promise<string>;
}>(), {
minHeight: 560,
});
const emit = defineEmits<{
(event: "update:modelValue", value: string): void;
}>();
const mode = "default";
const editorRef = shallowRef<IDomEditor>();
const valueHtml = ref(props.modelValue || "");
const editorHeight = computed(() => `${props.minHeight}px`);
const toolbarConfig: Partial<IToolbarConfig> = {
toolbarKeys: [
"header2",
"header3",
"header4",
"|",
"bold",
"italic",
"underline",
"through",
"blockquote",
"|",
"bulletedList",
"numberedList",
"|",
"insertLink",
{
key: "group-image",
title: "图片",
menuKeys: ["insertImage", "uploadImage"],
},
"|",
"clearStyle",
"undo",
"redo",
],
};
const editorConfig: Partial<IEditorConfig> = {
placeholder: "请输入文章正文,可使用标题、列表、引用、链接和图片。",
scroll: true,
MENU_CONF: {
uploadImage: {
maxFileSize: 5 * 1024 * 1024,
allowedFileTypes: ["image/*"],
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
if (!file.type.startsWith("image/")) {
ElMessage.error("仅支持上传图片文件");
return;
}
if (!props.uploadImage) {
ElMessage.error("图片上传接口未配置");
return;
}
try {
const url = await props.uploadImage(file);
insertFn(url, file.name);
ElMessage.success("图片已插入");
} catch (error) {
console.error(error);
ElMessage.error("图片上传失败");
}
},
},
insertLink: {
checkLink: (_text: string, url: string) => {
if (!url.trim()) {
return "链接地址不能为空";
}
return true;
},
},
},
};
watch(
() => props.modelValue,
(value) => {
if (value === valueHtml.value) {
return;
}
valueHtml.value = value || "";
},
);
watch(valueHtml, (value) => {
emit("update:modelValue", value);
});
watch(
() => props.disabled,
(disabled) => {
const editor = editorRef.value;
if (!editor) {
return;
}
if (disabled) {
editor.disable();
return;
}
editor.enable();
},
);
function handleCreated(editor: IDomEditor) {
editorRef.value = editor;
if (props.disabled) {
editor.disable();
}
}
onBeforeUnmount(() => {
editorRef.value?.destroy();
});
</script>
<template>
<div class="rich-text-editor" :class="{ 'rich-text-editor--disabled': disabled }">
<Toolbar class="rich-text-editor__toolbar" :editor="editorRef" :default-config="toolbarConfig" :mode="mode" />
<Editor
v-model="valueHtml"
class="rich-text-editor__body"
:style="{ height: editorHeight }"
:default-config="editorConfig"
:mode="mode"
@on-created="handleCreated"
/>
</div>
</template>
<style scoped>
.rich-text-editor {
overflow: hidden;
border: 1px solid var(--admin-border);
border-radius: 8px;
background: #fff;
box-shadow: 0 14px 36px rgba(34, 28, 18, 0.06);
--w-e-toolbar-bg-color: #fff;
--w-e-toolbar-color: var(--admin-text-main);
--w-e-toolbar-active-bg-color: rgba(200, 164, 93, 0.14);
--w-e-toolbar-active-color: var(--admin-text-main);
--w-e-toolbar-border-color: var(--admin-border);
--w-e-textarea-bg-color: #fff;
--w-e-textarea-color: var(--admin-text-main);
--w-e-textarea-selected-border-color: rgba(200, 164, 93, 0.55);
--w-e-textarea-slight-bg-color: #fbf7ef;
--w-e-textarea-slight-color: #9aa0aa;
--w-e-modal-button-bg-color: #fff;
--w-e-modal-button-border-color: var(--admin-border);
}
.rich-text-editor--disabled {
opacity: 0.74;
}
.rich-text-editor__toolbar {
border-bottom: 1px solid var(--admin-border);
background: linear-gradient(180deg, #fff 0%, #fbfbfc 100%);
}
:deep(.w-e-toolbar) {
padding: 7px 10px;
}
:deep(.w-e-bar-item button) {
border-radius: 6px;
font-weight: 600;
}
:deep(.w-e-bar-item button:hover) {
background: rgba(200, 164, 93, 0.14);
}
:deep(.w-e-text-container) {
background: #fff;
}
:deep(.w-e-text-placeholder) {
color: #9aa0aa;
font-style: normal;
}
:deep(.w-e-scroll) {
padding: 24px 30px;
}
:deep(.w-e-text-container [data-slate-editor]) {
color: var(--admin-text-main);
font-size: 16px;
line-height: 1.9;
}
:deep(.w-e-text-container [data-slate-editor] h2),
:deep(.w-e-text-container [data-slate-editor] h3),
:deep(.w-e-text-container [data-slate-editor] h4) {
color: var(--admin-text);
line-height: 1.45;
}
:deep(.w-e-text-container [data-slate-editor] img) {
border-radius: 8px;
}
</style>