build(deps): 添加 Tiptap 富文本编辑器依赖

This commit is contained in:
yelan
2026-09-11 16:13:05 +08:00
parent 7035c6f378
commit 1b576c6095
5 changed files with 1083 additions and 9 deletions
@@ -33,7 +33,8 @@ const init = {
license_key: 'gpl',
menubar: false,
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式)。',
plugins: 'lists table paste',
// TinyMCE 8:paste 已并入核心,不能再声明(声明会导致插件加载失败)。
plugins: 'lists table',
toolbar:
'undo redo | bold italic underline | alignleft aligncenter alignright alignjustify | fontsizeselect forecolor backcolor | bullist numlist | table',
skin: false,
@@ -45,10 +46,10 @@ const init = {
`,
paste_data_images: true,
height: 460,
}
function onInit(_event: unknown, editor: TinyMceApi) {
editorRef.value = editor
// tinymce-vue 不派发 init 事件,编辑器实例只能从这里拿。
init_instance_callback: (editor: TinyMceApi) => {
editorRef.value = editor
},
}
function getText(): string {
@@ -68,7 +69,6 @@ defineExpose({ getText, insertText })
:model-value="props.modelValue"
:init="init"
@update:model-value="emit('update:modelValue', $event)"
@init="onInit"
/>
</div>
</template>
@@ -0,0 +1,206 @@
<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>