build(deps): 移除 ckeditor 和 tiptap 相关依赖

This commit is contained in:
yelan
2026-09-14 09:38:24 +08:00
parent 545326bd81
commit ff67f88696
9 changed files with 144 additions and 3882 deletions
@@ -1,149 +0,0 @@
<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>
@@ -1,206 +0,0 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import TextAlign from '@tiptap/extension-text-align'
import TextStyle from '@tiptap/extension-text-style'
import Color from '@tiptap/extension-color'
import FontFamily from '@tiptap/extension-font-family'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableHeader from '@tiptap/extension-table-header'
import TableCell from '@tiptap/extension-table-cell'
// TipTap(ProseMirror 架构):自定义能力最强(未来可把"勾选签署域"做成自定义节点),
// 但粘贴内容会经过 schema 过滤,未注册的内联样式(如 font-size)会丢失——保真是它的弱项。
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editor = shallowRef<Editor>()
// 工具栏激活状态依赖 editor 内部状态,用事务计数驱动模板刷新。
const transactionTick = ref(0)
const editorInstance = new Editor({
content: props.modelValue || '<p></p>',
extensions: [
StarterKit,
Underline,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
TextStyle,
Color,
FontFamily,
Table.configure({ resizable: false }),
TableRow,
TableHeader,
TableCell,
],
onUpdate: ({ editor: current }) => {
emit('update:modelValue', current.getHTML())
},
onTransaction: () => {
transactionTick.value += 1
},
})
editor.value = editorInstance
watch(
() => props.modelValue,
(value) => {
const current = editor.value
if (current && value !== current.getHTML()) {
current.commands.setContent(value || '<p></p>')
}
},
)
function getText(): string {
return editor.value?.getText().trim() ?? ''
}
function insertText(text: string) {
editor.value?.commands.insertContent(text)
}
defineExpose({ getText, insertText })
onBeforeUnmount(() => {
editorInstance.destroy()
})
</script>
<template>
<div class="tiptap-pane">
<div v-if="editor" class="pane-toolbar">
<button
type="button"
:class="{ active: editor.isActive('bold') }"
:data-tick="transactionTick"
@click="editor.chain().focus().toggleBold().run()"
>
B
</button>
<button
type="button"
:class="{ active: editor.isActive('italic') }"
@click="editor.chain().focus().toggleItalic().run()"
>
I
</button>
<button
type="button"
:class="{ active: editor.isActive('underline') }"
@click="editor.chain().focus().toggleUnderline().run()"
>
U
</button>
<button
type="button"
:class="{ active: editor.isActive('heading', { level: 1 }) }"
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
>
H1
</button>
<button
type="button"
:class="{ active: editor.isActive('heading', { level: 2 }) }"
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
>
H2
</button>
<button
type="button"
:class="{ active: editor.isActive({ textAlign: 'center' }) }"
@click="editor.chain().focus().setTextAlign('center').run()"
>
居中
</button>
<button
type="button"
:class="{ active: editor.isActive({ textAlign: 'left' }) }"
@click="editor.chain().focus().setTextAlign('left').run()"
>
左
</button>
<button
type="button"
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"
>
表格
</button>
<button type="button" @click="editor.chain().focus().undo().run()">撤销</button>
<button type="button" @click="editor.chain().focus().redo().run()">重做</button>
</div>
<EditorContent :editor="editor" class="pane-content" />
</div>
</template>
<style scoped>
.tiptap-pane {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.pane-toolbar {
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
gap: 4px;
padding: 8px;
border-bottom: 1px solid var(--line);
}
.pane-toolbar button {
padding: 4px 10px;
color: var(--ink);
font-size: 12px;
background: #fff;
border: 1px solid var(--line);
border-radius: 5px;
}
.pane-toolbar button.active {
color: #fff;
background: var(--brand);
border-color: var(--brand);
}
.pane-content {
flex: 1;
min-height: 340px;
max-height: 460px;
overflow-y: auto;
padding: 12px 16px;
}
.pane-content :deep(.ProseMirror) {
min-height: 320px;
outline: none;
}
.pane-content :deep(.ProseMirror table) {
width: 100%;
margin: 8px 0;
border-collapse: collapse;
table-layout: fixed;
}
.pane-content :deep(.ProseMirror th),
.pane-content :deep(.ProseMirror td) {
padding: 5px 7px;
border: 1px solid #52646e;
vertical-align: middle;
}
</style>
@@ -5,6 +5,8 @@ import { onBeforeUnmount, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
import { normalizePastedHtml } from '@/utils/rich-text'
const props = defineProps<{
modelValue: string
}>()
@@ -16,28 +18,20 @@ const emit = defineEmits<{
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(保留居中、加粗、表格等结构),
// 但先做归一化——Word 表格的绝对列宽与 nowrap 会让整表在窄容器里被裁切;
// 纯文本粘贴(无富 HTML)时返回 true 走默认处理。
customPaste: (editor, event) => {
const html = event.clipboardData?.getData('text/html')
if (html) {
editor.dangerouslyInsertHtml(cleanClipboardHtml(html))
editor.dangerouslyInsertHtml(normalizePastedHtml(html))
return false
}
@@ -102,6 +96,28 @@ onBeforeUnmount(() => {
.pane-content {
flex: 1;
min-height: 340px;
overflow-y: auto;
overflow: auto;
}
/* 粘贴的 Word 表格:解除绝对列宽、允许单元格换行,
超出容器时横向滚动而不是被裁切 */
.pane-content :deep(table) {
width: 100%;
border-collapse: collapse;
table-layout: auto;
}
.pane-content :deep(th),
.pane-content :deep(td) {
padding: 4px 6px;
vertical-align: top;
border: 1px solid #52646e;
overflow-wrap: anywhere;
word-break: break-word;
}
.pane-content :deep(th p),
.pane-content :deep(td p) {
margin: 0;
}
</style>