build(deps): 添加 Tiptap 富文本编辑器依赖
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user