Files
YschcmsFront/src/components/RichTextEditor.vue
T
chenxing 6f1f666479
build / build (push) Successful in 4m47s
周末修订
2026-09-15 23:41:11 +08:00

136 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef, watch } from "vue";
import { Message } from "view-ui-plus";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import config from "@/assets/config/host.config";
/**
* 通用富文本编辑器(事项库 C-5:从 ArticleEdit 内联 wangEditor 抽取的公共组件;
* 审计批次 6 起 ArticleEdit 亦复用本组件,消除两份实现)。
* v-model 绑定 HTML;图片 customUpload 直传 /api/attachment/upload(fetch + credentials),
* 上传成功 emit uploaded(attachmentId) 供调用方收集(保存时绑定归属)。
* 销毁时 editor.destroy()(wangEditor 必须显式销毁防内存泄漏)。
*/
const props = defineProps<{
modelValue: string;
siteId?: number;
/** 附件场景(article_asset 正文资产等;空=不带 scene;svc 场景已迁办事大厅) */
scene?: string;
/** 文章 id(文章编辑器上传时的归属参数;无文章 id 的页面不传) */
articleId?: number;
placeholder?: string;
/** 建议 ≥320(长文显示不全修复口径:容器随内容增高,短文铺满 min-height) */
height?: number;
/** slate 内容区最小高度(CSS 值);缺省用事项页口径,文章页传入自己的口径保持原观感 */
slateMinHeight?: string;
}>();
const emit = defineEmits<{
(e: "update:modelValue", v: string): void;
(e: "uploaded", attachmentId: number): void;
}>();
const editorRef = shallowRef();
const serviceBase = config.isMock ? "" : config.serviceHost;
const toolbarConfig = { excludeKeys: ["group-video", "fullScreen"] };
const editorConfig = {
placeholder: props.placeholder || "请输入内容……",
MENU_CONF: {
uploadImage: {
async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
const fd = new FormData();
fd.append("file", file);
if (props.siteId) {
fd.append("siteId", String(props.siteId));
}
if (props.articleId) {
fd.append("articleId", String(props.articleId));
}
if (props.scene) {
fd.append("scene", props.scene);
}
try {
const resp = await fetch(`${serviceBase}/api/attachment/upload`, {
method: "POST",
body: fd,
credentials: "include",
});
const res = await resp.json();
if (res.code === 0 && res.data) {
emit("uploaded", res.data.id);
insertFn(res.data.url, res.data.fileName || "", res.data.url);
} else {
Message.error(res.msg || "图片上传失败");
}
} catch (e) {
console.warn("[rich-editor] 图片上传失败", e);
Message.error("图片上传失败");
}
},
},
},
};
function handleCreated(editor: any) {
editorRef.value = editor;
}
onBeforeUnmount(() => {
editorRef.value?.destroy();
});
// 外部回显值变化同步进编辑器(编辑页 load 后异步回填)
watch(
() => props.modelValue,
(v) => {
if (editorRef.value && v !== editorRef.value.getHtml()) {
editorRef.value.setHtml(v || "<p><br></p>");
}
}
);
</script>
<template>
<div
class="rich-editor"
:style="{ minHeight: (height || 360) + 'px', '--rich-editor-slate-min': slateMinHeight || 'max(240px, calc(100vh - 560px))' }"
>
<Toolbar class="editor-toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
<Editor
class="editor-content"
:model-value="modelValue"
:defaultConfig="editorConfig"
mode="default"
style="height: 100%"
@onCreated="handleCreated"
@update:model-value="(v: string) => emit('update:modelValue', v)"
/>
</div>
</template>
<style scoped lang="scss">
.rich-editor {
display: flex;
flex-direction: column;
border: 1px solid #dcdee2;
border-radius: 4px;
z-index: 100;
.editor-toolbar {
border-bottom: 1px solid #dcdee2;
}
.editor-content {
flex: 1;
overflow: visible;
// 长文显示不全修复(2026-09-10)同款口径:文本容器高度随内容自动增长,
// 整篇平铺由页面滚动;空态/短文 slate 区 min-height 铺满(调用方可用 slateMinHeight 覆写口径)
:deep(.w-e-text-container) {
height: auto !important;
}
:deep([data-slate-editor]) {
min-height: var(--rich-editor-slate-min) !important;
}
}
}
</style>