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
@@ -0,0 +1,149 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import {
Alignment,
Bold,
ClassicEditor,
Essentials,
FontColor,
FontSize,
GeneralHtmlSupport,
Heading,
Italic,
PasteFromOffice,
Paragraph,
Table,
TableToolbar,
Underline,
} from 'ckeditor5'
import type { Editor } from 'ckeditor5'
import 'ckeditor5/ckeditor5.css'
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editorRef = shallowRef<Editor>()
// PasteFromOffice + GeneralHtmlSupport:CKEditor 的 Word 粘贴保真核心,
// 开源 GPL 构建即包含(对应 config 中 licenseKey: 'GPL')。
const config = {
licenseKey: 'GPL',
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(PasteFromOffice 插件处理 Word 粘贴)。',
plugins: [
Essentials,
Paragraph,
Heading,
Bold,
Italic,
Underline,
Alignment,
FontSize,
FontColor,
Table,
TableToolbar,
GeneralHtmlSupport,
PasteFromOffice,
],
toolbar: [
'heading',
'|',
'bold',
'italic',
'underline',
'|',
'fontSize',
'fontColor',
'|',
'alignment:left',
'alignment:center',
'alignment:right',
'alignment:justify',
'|',
'bulletedList',
'numberedList',
'|',
'insertTable',
'undo',
'redo',
],
heading: {
options: [
{ model: 'paragraph' as const, title: '正文', class: 'ck-heading_paragraph' },
{ model: 'heading1' as const, view: 'h1', title: '标题 1', class: 'ck-heading_heading1' },
{ model: 'heading2' as const, view: 'h2', title: '标题 2', class: 'ck-heading_heading2' },
{ model: 'heading3' as const, view: 'h3', title: '标题 3', class: 'ck-heading_heading3' },
],
},
// 尽量保留粘贴内容中的内联样式与 class(Word 居中等视觉信息)。
htmlSupport: {
allow: [{ name: /[\s\S]+/, classes: /[\s\S]+/, styles: /[\s\S]+/, attributes: /[\s\S]+/ }],
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells'],
},
}
function onReady(editor: Editor) {
editorRef.value = editor
}
function getText(): string {
const html = editorRef.value?.getData() ?? ''
return html
.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/g, ' ')
.trim()
}
function insertText(text: string) {
const editor = editorRef.value
if (!editor) {
return
}
editor.model.change((writer) => {
const focus = editor.model.document.selection.focus
if (focus) {
writer.insertText(text, focus)
}
})
}
defineExpose({ getText, insertText })
</script>
<template>
<div class="ck-pane">
<Ckeditor
class="pane-editor"
:editor="ClassicEditor"
:model-value="props.modelValue"
:config="config"
@update:model-value="emit('update:modelValue', String($event))"
@ready="onReady"
/>
</div>
</template>
<style scoped>
.ck-pane {
height: 100%;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.ck-pane :deep(.ck-editor__editable_inline) {
min-height: 380px;
max-height: 460px;
}
</style>
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
// 副作用导入:注册全局 window.tinymce(@tinymce/tinymce-vue 会检测全局实例,从而走本地包而非 CDN)。
import 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
import 'tinymce/models/dom'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/table'
import 'tinymce/skins/ui/oxide/skin.min.css'
import TinyEditor from '@tinymce/tinymce-vue'
// TinyMCE 8(GPL 开源模式需声明 license_key: 'gpl')。
// skin: false / content_css: false —— 皮肤样式由打包引入,iframe 内容样式用 content_style 内联。
const editorRef = shallowRef<TinyMceApi | null>(null)
interface TinyMceApi {
getContent: (options?: { format?: string }) => string
setContent: (content: string) => void
insertContent: (content: string) => void
}
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const init = {
license_key: 'gpl',
menubar: false,
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式)。',
plugins: 'lists table paste',
toolbar:
'undo redo | bold italic underline | alignleft aligncenter alignright alignjustify | fontsizeselect forecolor backcolor | bullist numlist | table',
skin: false,
content_css: false,
content_style: `
body { font-family: 'SimSun', 'Songti SC', serif; font-size: 14px; line-height: 1.8; color: #1c2b33; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #52646e; padding: 5px 7px; }
`,
paste_data_images: true,
height: 460,
}
function onInit(_event: unknown, editor: TinyMceApi) {
editorRef.value = editor
}
function getText(): string {
return editorRef.value?.getContent({ format: 'text' }).trim() ?? ''
}
function insertText(text: string) {
editorRef.value?.insertContent(text)
}
defineExpose({ getText, insertText })
</script>
<template>
<div class="tiny-pane">
<TinyEditor
:model-value="props.modelValue"
:init="init"
@update:model-value="emit('update:modelValue', $event)"
@init="onInit"
/>
</div>
</template>
<style scoped>
.tiny-pane {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
</style>
@@ -0,0 +1,107 @@
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editorRef = shallowRef<IDomEditor>()
const editorMode = 'default'
// 剪贴板富 HTML 的轻量清理:去掉脚本/样式表/条件注释(WangEditor 默认粘贴会过滤掉
// 大量内联样式导致"粘贴变形",这里接管粘贴以尽量保留 Word 原样式)。
function cleanClipboardHtml(html: string): string {
return html
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<(script|style|meta|link)\b[^>]*>[\s\S]*?<\/\1>/gi, '')
.replace(/<(script|meta|link)\b[^>]*\/?>/gi, '')
}
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ['group-video', 'fullScreen'],
}
const editorConfig: Partial<IEditorConfig> = {
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式)。',
// 接管粘贴:优先使用剪贴板中的富 HTML 原样插入(保留居中、加粗、表格等内联样式);
// 纯文本粘贴(无富 HTML)时返回 true 走默认处理。
customPaste: (editor, event) => {
const html = event.clipboardData?.getData('text/html')
if (html) {
editor.dangerouslyInsertHtml(cleanClipboardHtml(html))
return false
}
return true
},
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
}
function getText(): string {
return editorRef.value?.getText().trim() ?? ''
}
// slate 文本节点:{ text };由编辑器在当前光标处插入。
function insertText(text: string) {
editorRef.value?.insertNode({ text })
}
defineExpose({ getText, insertText })
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<template>
<div class="wang-pane">
<Toolbar
class="pane-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
class="pane-content"
:model-value="props.modelValue"
:default-config="editorConfig"
:mode="editorMode"
@update:model-value="emit('update:modelValue', $event)"
@on-created="handleCreated"
/>
</div>
</template>
<style scoped>
.wang-pane {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.pane-toolbar {
flex-shrink: 0;
border-bottom: 1px solid var(--line);
}
.pane-content {
flex: 1;
min-height: 340px;
overflow-y: auto;
}
</style>