136 lines
4.9 KiB
Vue
136 lines
4.9 KiB
Vue
<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>
|