build(deps): 新增 CKEditor 与 TinyMCE 依赖

This commit is contained in:
yelan
2026-09-11 15:28:18 +08:00
parent 23d85cf8ce
commit 7035c6f378
13 changed files with 3165 additions and 184 deletions
@@ -1,12 +1,11 @@
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
import { reactive, ref, watch } from 'vue'
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
import EditorCkPane from './editors/EditorCkPane.vue'
import EditorTinyPane from './editors/EditorTinyPane.vue'
import EditorWangPane from './editors/EditorWangPane.vue'
import type { DocumentTemplate, TemplateEditorForm } from '../types'
const props = defineProps<{
@@ -24,21 +23,45 @@ const emit = defineEmits<{
// Word 自维护方案:科室把文书从 Word/WPS 直接粘贴进来(保留表格与基本格式),
// 或上传 .docx 由 mammoth 转换;签署时由交互管线识别"勾选"表并转为签署域。
const editorRef = shallowRef<IDomEditor>()
// 富文本引擎做成可切换的三套(对比 Word 粘贴保真度),存储统一为编辑器产出的 HTML。
type EditorEngine = 'wangeditor' | 'ckeditor5' | 'tinymce'
interface EditorPaneHandle {
getText: () => string
insertText: (text: string) => void
}
const engineComponents = {
wangeditor: EditorWangPane,
ckeditor5: EditorCkPane,
tinymce: EditorTinyPane,
}
const engineOptions: Array<{ key: EditorEngine; label: string; note: string }> = [
{
key: 'wangeditor',
label: 'WangEditor',
note: '已接管粘贴,尽量保留内联样式(MIT)',
},
{
key: 'ckeditor5',
label: 'CKEditor 5',
note: 'PasteFromOffice 插件,Word 粘贴保真最强(GPL2)',
},
{
key: 'tinymce',
label: 'TinyMCE',
note: '均衡之选(MIT)',
},
]
const engine = ref<EditorEngine>('wangeditor')
const engineNote = ref(engineOptions[0]?.note ?? '')
const valueHtml = ref('<p><br></p>')
const editorMode = 'default'
const docxInput = ref<HTMLInputElement | null>(null)
const importing = ref(false)
const importMessage = ref('')
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ['group-video', 'fullScreen'],
}
const editorConfig: Partial<IEditorConfig> = {
placeholder:
'将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式),或点击上方「导入 .docx」自动转换。',
}
const paneRef = ref<EditorPaneHandle | null>(null)
// 占位符在签署时由系统按患者与就诊信息自动填充;签署交互管线同时会识别"勾选"表格。
const placeholders = [
@@ -102,13 +125,13 @@ function handleCampusChange() {
}
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
function selectEngine(next: EditorEngine) {
engine.value = next
engineNote.value = engineOptions.find((option) => option.key === next)?.note ?? ''
}
function insertPlaceholder(text: string) {
// slate 文本节点:{ text };由编辑器在当前光标处插入。
editorRef.value?.insertNode({ text })
paneRef.value?.insertText(text)
}
async function handleDocxChange(event: Event) {
@@ -139,7 +162,7 @@ async function handleDocxChange(event: Event) {
}
function save() {
const text = editorRef.value?.getText().trim()
const text = paneRef.value?.getText() ?? ''
if (!form.name.trim()) {
importMessage.value = '请填写文书名称'
@@ -154,10 +177,6 @@ function save() {
form.contentHtml = valueHtml.value
emit('save', { ...form, contentHtml: valueHtml.value })
}
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<template>
@@ -242,6 +261,20 @@ onBeforeUnmount(() => {
</div>
<div class="editor-shell">
<div class="engine-bar">
<span class="engine-label">编辑器引擎</span>
<button
v-for="option in engineOptions"
:key="option.key"
type="button"
class="engine-button"
:class="{ active: engine === option.key }"
@click="selectEngine(option.key)"
>
{{ option.label }}
</button>
<span class="engine-note">{{ engineNote }}</span>
</div>
<div class="docx-bar">
<button
type="button"
@@ -265,18 +298,12 @@ onBeforeUnmount(() => {
</span>
</div>
<div class="editor-area">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
v-model="valueHtml"
class="editor-content"
:default-config="editorConfig"
:mode="editorMode"
@on-created="handleCreated"
<component
:is="engineComponents[engine]"
:key="engine"
ref="paneRef"
:model-value="valueHtml"
@update:model-value="valueHtml = $event"
/>
</div>
</div>
@@ -478,24 +505,52 @@ onBeforeUnmount(() => {
font-size: 12px;
}
.engine-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.engine-label {
color: var(--ink);
font-size: 12px;
font-weight: 700;
}
.engine-button {
padding: 5px 12px;
color: var(--mut);
font-size: 12px;
font-weight: 700;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
}
.engine-button:hover {
border-color: var(--brand);
color: var(--brand);
}
.engine-button.active {
color: #fff;
background: var(--brand);
border-color: var(--brand);
}
.engine-note {
color: var(--mut);
font-size: 11.5px;
}
.editor-area {
display: flex;
flex-direction: column;
flex: 1;
min-height: 380px;
min-height: 420px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.editor-toolbar {
border-bottom: 1px solid var(--line);
}
.editor-content {
flex: 1;
min-height: 340px;
overflow-y: auto;
}
.dialog-footer {