build(deps): 移除 ckeditor 和 tiptap 相关依赖
This commit is contained in:
@@ -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(/ /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>
|
||||
|
||||
Reference in New Issue
Block a user