refactor(TiptapEditor): 清理代码注释和格式化代码

This commit is contained in:
Phoenix 2025-07-07 10:26:09 +08:00
parent 3f89777bf8
commit b18a1b2604

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
// Tiptap
import { Editor, EditorContent, useEditor } from '@tiptap/vue-3' import { Editor, EditorContent, useEditor } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image' import Image from '@tiptap/extension-image'
@ -10,83 +10,83 @@ import Link from '@tiptap/extension-link'
import { Extension, Node } from '@tiptap/core' import { Extension, Node } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state' import { Plugin, PluginKey } from '@tiptap/pm/state'
// Vue
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted, shallowRef } from 'vue' import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted, shallowRef } from 'vue'
// Naive UI
import { NPopover, NIcon } from 'naive-ui' import { NPopover, NIcon } from 'naive-ui'
//
import { import {
Voice as IconVoice, // Voice as IconVoice,
SourceCode, // SourceCode,
Local, // Local,
SmilingFace, // SmilingFace,
Pic, // Pic,
FolderUpload, // FolderUpload,
Ranking, // Ranking,
History, // History,
Close // Close
} from '@icon-park/vue-next' } from '@icon-park/vue-next'
//
import { useDialogueStore, useEditorDraftStore } from '@/store' import { useDialogueStore, useEditorDraftStore } from '@/store'
//
import { getImageInfo } from '@/utils/functions' import { getImageInfo } from '@/utils/functions'
//
import { EditorConst } from '@/constant/event-bus' import { EditorConst } from '@/constant/event-bus'
//
import { emitCall } from '@/utils/common' import { emitCall } from '@/utils/common'
//
import { defAvatar } from '@/constant/default' import { defAvatar } from '@/constant/default'
//
import suggestion from './suggestion.js' import suggestion from './suggestion.js'
//
import MeEditorVote from './MeEditorVote.vue' // import MeEditorVote from './MeEditorVote.vue'
import MeEditorEmoticon from './MeEditorEmoticon.vue' // import MeEditorEmoticon from './MeEditorEmoticon.vue'
import MeEditorCode from './MeEditorCode.vue' // import MeEditorCode from './MeEditorCode.vue'
import MeEditorRecorder from './MeEditorRecorder.vue' // import MeEditorRecorder from './MeEditorRecorder.vue'
// API
import { uploadImg } from '@/api/upload' import { uploadImg } from '@/api/upload'
// 线
import { useEventBus } from '@/hooks' import { useEventBus } from '@/hooks'
//
const emit = defineEmits(['editor-event']) const emit = defineEmits(['editor-event'])
//
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
// 稿
const editorDraftStore = useEditorDraftStore() const editorDraftStore = useEditorDraftStore()
// props
const props = defineProps({ const props = defineProps({
vote: { vote: {
type: Boolean, type: Boolean,
default: false // default: false
}, },
members: { members: {
default: () => [] // @ default: () => []
} }
}) })
//
const indexName = computed(() => dialogueStore.index_name) const indexName = computed(() => dialogueStore.index_name)
//
const isShowEditorVote = ref(false) const isShowEditorVote = ref(false)
//
const isShowEditorCode = ref(false) const isShowEditorCode = ref(false)
//
const isShowEditorRecorder = ref(false) const isShowEditorRecorder = ref(false)
const uploadingImages = ref(new Map()) const uploadingImages = ref(new Map())
// DOM
const fileImageRef = ref() const fileImageRef = ref()
// DOM
const uploadFileRef = ref() const uploadFileRef = ref()
//
const emoticonRef = ref() const emoticonRef = ref()
//
const showEmoticon = ref(false) const showEmoticon = ref(false)
//
const quoteData = ref(null) const quoteData = ref(null)
// Emoji
const Emoji = Node.create({ const Emoji = Node.create({
name: 'emoji', name: 'emoji',
group: 'inline', group: 'inline',
@ -129,12 +129,12 @@ const Emoji = Node.create({
// Enter
const EnterKeyPlugin = new Plugin({ const EnterKeyPlugin = new Plugin({
key: new PluginKey('enterKey'), key: new PluginKey('enterKey'),
props: { props: {
handleKeyDown: (view, event) => { handleKeyDown: (view, event) => {
// EnterShift
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault() event.preventDefault()
onSendMessage() onSendMessage()
@ -145,7 +145,7 @@ const EnterKeyPlugin = new Plugin({
}, },
}) })
//
const CustomKeyboard = Extension.create({ const CustomKeyboard = Extension.create({
name: 'customKeyboard', name: 'customKeyboard',
@ -156,7 +156,7 @@ const CustomKeyboard = Extension.create({
}, },
}) })
//
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
StarterKit, StarterKit,
@ -284,11 +284,6 @@ const editor = useEditor({
} }
}) })
/**
* 上传图片函数
* @param file 文件对象
* @returns Promise成功时返回图片URL
*/
function findImagePos(url) { function findImagePos(url) {
if (!editor.value) return -1 if (!editor.value) return -1
let pos = -1 let pos = -1
@ -318,51 +313,43 @@ function onUploadImage(file) {
image.onload = () => { image.onload = () => {
const form = new FormData() const form = new FormData()
form.append('file', file) form.append('file', file)
form.append("source", "fonchain-chat"); // form.append("source", "fonchain-chat");
// URL
form.append("urlParam", `width=${image.width}&height=${image.height}`); form.append("urlParam", `width=${image.width}&height=${image.height}`);
// API
uploadImg(form).then(({ code, data, message }) => { uploadImg(form).then(({ code, data, message }) => {
if (code == 0) { if (code == 0) {
resolve(data.ori_url) // URL resolve(data.ori_url)
} else { } else {
resolve('') resolve('')
window['$message'].error(message) // window['$message'].error(message)
} }
}) })
} }
}) })
} }
/**
* 投票事件处理
* @param data 投票数据
*/
function onVoteEvent(data) { function onVoteEvent(data) {
const msg = emitCall('vote_event', data, (ok) => { const msg = emitCall('vote_event', data, (ok) => {
if (ok) { if (ok) {
isShowEditorVote.value = false // isShowEditorVote.value = false
} }
}) })
emit('editor-event', msg) emit('editor-event', msg)
} }
/**
* 表情事件处理
* @param data 表情数据
*/
function onEmoticonEvent(data) { function onEmoticonEvent(data) {
//
showEmoticon.value = false showEmoticon.value = false
if (data.type == 1) { if (data.type == 1) {
//
if (!editor.value) return if (!editor.value) return
if (data.img) { if (data.img) {
//
editor.value.chain().focus().insertContent({ editor.value.chain().focus().insertContent({
type: 'emoji', type: 'emoji',
attrs: { attrs: {
@ -373,39 +360,31 @@ function onEmoticonEvent(data) {
}, },
}).run() }).run()
} else { } else {
//
editor.value.chain().focus().insertContent(data.value).run() editor.value.chain().focus().insertContent(data.value).run()
} }
} else { } else {
//
let fn = emitCall('emoticon_event', data.value, () => {}) let fn = emitCall('emoticon_event', data.value, () => {})
emit('editor-event', fn) emit('editor-event', fn)
} }
} }
/**
* 代码事件处理
* @param data 代码数据
*/
function onCodeEvent(data) { function onCodeEvent(data) {
const msg = emitCall('code_event', data, (ok) => { const msg = emitCall('code_event', data, (ok) => {
isShowEditorCode.value = false // isShowEditorCode.value = false
}) })
emit('editor-event', msg) emit('editor-event', msg)
} }
/**
* 文件上传处理
* @param e 上传事件对象
*/
async function onUploadFile(e) { async function onUploadFile(e) {
let file = e.target.files[0] let file = e.target.files[0]
e.target.value = null // input e.target.value = null
if (file.type.indexOf('image/') === 0) { if (file.type.indexOf('image/') === 0) {
// -
let fn = emitCall('image_event', file, () => {}) let fn = emitCall('image_event', file, () => {})
emit('editor-event', fn) emit('editor-event', fn)
@ -413,26 +392,22 @@ async function onUploadFile(e) {
} }
if (file.type.indexOf('video/') === 0) { if (file.type.indexOf('video/') === 0) {
//
let fn = emitCall('video_event', file, () => {}) let fn = emitCall('video_event', file, () => {})
emit('editor-event', fn) emit('editor-event', fn)
} else { } else {
//
let fn = emitCall('file_event', file, () => {}) let fn = emitCall('file_event', file, () => {})
emit('editor-event', fn) emit('editor-event', fn)
} }
} }
/**
* 录音事件处理
* @param file 录音文件
*/
function onRecorderEvent(file) { function onRecorderEvent(file) {
emit('editor-event', emitCall('file_event', file)) emit('editor-event', emitCall('file_event', file))
isShowEditorRecorder.value = false // isShowEditorRecorder.value = false
} }
// Tiptap
function tiptapToMessage() { function tiptapToMessage() {
if (!editor.value) return [] if (!editor.value) return []
@ -473,7 +448,7 @@ function tiptapToMessage() {
} else if (node.type === 'hardBreak') { } else if (node.type === 'hardBreak') {
currentTextBuffer += '\n' currentTextBuffer += '\n'
} else if (node.type === 'image') { } else if (node.type === 'image') {
//
flushTextBuffer() flushTextBuffer()
const data = { const data = {
...getImageInfo(node.attrs.src), ...getImageInfo(node.attrs.src),
@ -490,7 +465,7 @@ function tiptapToMessage() {
if (node.content) { if (node.content) {
processInlines(node.content) processInlines(node.content)
} }
currentTextBuffer += '\n' // Add newline after each paragraph currentTextBuffer += '\n'
} else if (node.type === 'image') { } else if (node.type === 'image') {
flushTextBuffer() flushTextBuffer()
const data = { const data = {
@ -517,20 +492,20 @@ function tiptapToMessage() {
return messages return messages
} }
// Tiptap
function tiptapToString() { function tiptapToString() {
if (!editor.value) return '' if (!editor.value) return ''
return editor.value.getText() return editor.value.getText()
} }
//
function isEditorEmpty() { function isEditorEmpty() {
if (!editor.value) return true if (!editor.value) return true
const json = editor.value.getJSON() const json = editor.value.getJSON()
//
return !json.content || ( return !json.content || (
json.content.length === 1 && json.content.length === 1 &&
json.content[0].type === 'paragraph' && json.content[0].type === 'paragraph' &&
@ -538,10 +513,6 @@ function isEditorEmpty() {
) )
} }
/**
* 发送消息处理
* 根据编辑器内容类型发送不同类型的消息
*/
function onSendMessage() { function onSendMessage() {
if (uploadingImages.value.size > 0) { if (uploadingImages.value.size > 0) {
return window['$message'].info('正在上传图片,请稍后再发') return window['$message'].info('正在上传图片,请稍后再发')
@ -563,7 +534,7 @@ function onSendMessage() {
return return
} }
//
if (quoteData.value) { if (quoteData.value) {
msg.data.quoteId = quoteData.value.id msg.data.quoteId = quoteData.value.id
msg.data.quote = { ...quoteData.value } msg.data.quote = { ...quoteData.value }
@ -578,7 +549,7 @@ function onSendMessage() {
url: msg.data.url, url: msg.data.url,
} }
//
if (quoteData.value) { if (quoteData.value) {
data.quoteId = quoteData.value.id data.quoteId = quoteData.value.id
data.quote = { ...quoteData.value } data.quote = { ...quoteData.value }
@ -588,7 +559,7 @@ function onSendMessage() {
} }
}) })
//
if (messages.length === 0 && quoteData.value) { if (messages.length === 0 && quoteData.value) {
const emptyData = { const emptyData = {
items: [{ type: 1, content: '' }], items: [{ type: 1, content: '' }],
@ -602,49 +573,41 @@ function onSendMessage() {
if (canClear) { if (canClear) {
editor.value?.commands.clearContent(true) editor.value?.commands.clearContent(true)
//
quoteData.value = null quoteData.value = null
// 稿
onEditorChange() onEditorChange()
} }
} }
/**
* 编辑器内容改变时的处理
* 保存草稿并触发输入事件
*/
function onEditorChange() { function onEditorChange() {
if (!editor.value) return if (!editor.value) return
const text = tiptapToString() const text = tiptapToString()
if (!isEditorEmpty() || quoteData.value) { if (!isEditorEmpty() || quoteData.value) {
// 稿store
editorDraftStore.items[indexName.value || ''] = JSON.stringify({ editorDraftStore.items[indexName.value || ''] = JSON.stringify({
text: text, text: text,
content: editor.value.getJSON(), content: editor.value.getJSON(),
quoteData: quoteData.value quoteData: quoteData.value
}) })
} else { } else {
// 稿
delete editorDraftStore.items[indexName.value || ''] delete editorDraftStore.items[indexName.value || '']
} }
//
emit('editor-event', emitCall('input_event', text)) emit('editor-event', emitCall('input_event', text))
} }
/**
* 加载编辑器草稿内容
* 当切换聊天对象时加载对应的草稿
*/
function loadEditorDraftText() { function loadEditorDraftText() {
if (!editor.value) return if (!editor.value) return
//
quoteData.value = null quoteData.value = null
// 稿
let draft = editorDraftStore.items[indexName.value || ''] let draft = editorDraftStore.items[indexName.value || '']
if (draft) { if (draft) {
const parsed = JSON.parse(draft) const parsed = JSON.parse(draft)
@ -654,26 +617,22 @@ function loadEditorDraftText() {
editor.value.commands.setContent(parsed.text) editor.value.commands.setContent(parsed.text)
} }
// 稿
if (parsed.quoteData) { if (parsed.quoteData) {
quoteData.value = parsed.quoteData quoteData.value = parsed.quoteData
} }
} else { } else {
editor.value.commands.clearContent(true) // 稿 editor.value.commands.clearContent(true)
} }
//
editor.value.commands.focus('end') editor.value.commands.focus('end')
} }
/**
* 处理@成员事件
* @param data @成员数据
*/
function onSubscribeMention(data) { function onSubscribeMention(data) {
if (!editor.value) return if (!editor.value) return
// @
editor.value.chain().focus().insertContent({ editor.value.chain().focus().insertContent({
type: 'mention', type: 'mention',
attrs: { attrs: {
@ -683,53 +642,42 @@ function onSubscribeMention(data) {
}).run() }).run()
} }
/**
* 处理引用事件
* @param data 引用数据
*/
function onSubscribeQuote(data) { function onSubscribeQuote(data) {
if (!editor.value) return if (!editor.value) return
//
quoteData.value = data quoteData.value = data
// 稿
onEditorChange() onEditorChange()
} }
/**
* 清空引用数据并更新草稿
*/
function clearQuoteData() { function clearQuoteData() {
quoteData.value = null quoteData.value = null
// 稿
onEditorChange() onEditorChange()
} }
/**
* 处理编辑消息事件
* @param data 消息数据
*/
function onSubscribeEdit(data) { function onSubscribeEdit(data) {
if (!editor.value) return if (!editor.value) return
//
editor.value.commands.clearContent(true) editor.value.commands.clearContent(true)
//
editor.value.commands.insertContent(data.content) editor.value.commands.insertContent(data.content)
//
editor.value.commands.focus('end') editor.value.commands.focus('end')
} }
//
const navs = reactive([ const navs = reactive([
{ {
title: '图片', title: '图片',
icon: markRaw(Pic), icon: markRaw(Pic),
show: true, show: true,
click: () => { click: () => {
fileImageRef.value.click() // fileImageRef.value.click()
} }
}, },
{ {
@ -737,38 +685,34 @@ const navs = reactive([
icon: markRaw(FolderUpload), icon: markRaw(FolderUpload),
show: true, show: true,
click: () => { click: () => {
uploadFileRef.value.click() // uploadFileRef.value.click()
} }
}, },
]) ])
// 稿
watch(indexName, loadEditorDraftText, { immediate: true }) watch(indexName, loadEditorDraftText, { immediate: true })
//
onMounted(() => { onMounted(() => {
loadEditorDraftText() loadEditorDraftText()
}) })
// 线
useEventBus([ useEventBus([
{ name: EditorConst.Mention, event: onSubscribeMention }, // @ { name: EditorConst.Mention, event: onSubscribeMention },
{ name: EditorConst.Quote, event: onSubscribeQuote }, // { name: EditorConst.Quote, event: onSubscribeQuote },
{ name: EditorConst.Edit, event: onSubscribeEdit } // { name: EditorConst.Edit, event: onSubscribeEdit }
]) ])
</script> </script>
<template> <template>
<!-- 编辑器容器 -->
<section class="el-container editor"> <section class="el-container editor">
<section class="el-container is-vertical"> <section class="el-container is-vertical">
<!-- 工具栏区域 -->
<header class="el-header toolbar bdr-t"> <header class="el-header toolbar bdr-t">
<div class="tools"> <div class="tools">
<!-- 表情选择器弹出框 -->
<n-popover <n-popover
placement="top-start" placement="top-start"
trigger="click" trigger="click"
@ -788,8 +732,6 @@ useEventBus([
<MeEditorEmoticon @on-select="onEmoticonEvent" /> <MeEditorEmoticon @on-select="onEmoticonEvent" />
</n-popover> </n-popover>
<!-- 工具栏其他功能按钮 -->
<div <div
class="item pointer" class="item pointer"
v-for="nav in navs" v-for="nav in navs"
@ -802,7 +744,7 @@ useEventBus([
</div> </div>
</div> </div>
</header> </header>
<!-- 引用消息块 -->
<div v-if="quoteData" class="quote-card-wrapper"> <div v-if="quoteData" class="quote-card-wrapper">
<div class="quote-card-content"> <div class="quote-card-content">
<div class="quote-card-title"> <div class="quote-card-title">
@ -817,20 +759,20 @@ useEventBus([
</div> </div>
</div> </div>
</div> </div>
<!-- 编辑器主体区域 -->
<main class="el-main height100"> <main class="el-main height100">
<editor-content :editor="editor" class="tiptap-editor" /> <editor-content :editor="editor" class="tiptap-editor" />
</main> </main>
</section> </section>
</section> </section>
<!-- 隐藏的文件上传表单 -->
<form enctype="multipart/form-data" style="display: none"> <form enctype="multipart/form-data" style="display: none">
<input type="file" ref="fileImageRef" accept="image/*" @change="onUploadFile" /> <input type="file" ref="fileImageRef" accept="image/*" @change="onUploadFile" />
<input type="file" ref="uploadFileRef" @change="onUploadFile" /> <input type="file" ref="uploadFileRef" @change="onUploadFile" />
</form> </form>
<!-- 条件渲染的功能组件 -->
<MeEditorVote v-if="isShowEditorVote" @close="isShowEditorVote = false" @submit="onVoteEvent" /> <MeEditorVote v-if="isShowEditorVote" @close="isShowEditorVote = false" @submit="onVoteEvent" />
<MeEditorCode <MeEditorCode
@ -847,12 +789,12 @@ useEventBus([
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
/* 编辑器容器样式 */
.editor { .editor {
--tip-bg-color: rgb(241 241 241 / 90%); /* 提示背景颜色 */ --tip-bg-color: rgb(241 241 241 / 90%);
height: 100%; height: 100%;
/* 引用消息块样式 */
.quote-card-wrapper { .quote-card-wrapper {
padding: 10px; padding: 10px;
background-color: #fff; background-color: #fff;
@ -925,7 +867,7 @@ useEventBus([
user-select: none; user-select: none;
.tip-title { .tip-title {
display: none; /* 默认隐藏提示文字 */ display: none;
position: absolute; position: absolute;
top: 40px; top: 40px;
left: 0px; left: 0px;
@ -943,7 +885,7 @@ useEventBus([
&:hover { &:hover {
.tip-title { .tip-title {
display: block; /* 悬停时显示提示文字 */ display: block;
} }
} }
} }
@ -951,7 +893,6 @@ useEventBus([
} }
} }
/* 暗色模式样式调整 */
html[theme-mode='dark'] { html[theme-mode='dark'] {
.editor { .editor {
--tip-bg-color: #48484d; --tip-bg-color: #48484d;
@ -982,7 +923,6 @@ html[theme-mode='dark'] {
</style> </style>
<style lang="less"> <style lang="less">
/* 全局编辑器样式 */
.tiptap-editor { .tiptap-editor {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
@ -1002,7 +942,7 @@ html[theme-mode='dark'] {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5) url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background:0 0"><circle cx="50" cy="50" r="32" stroke-width="8" stroke="%23fff" stroke-dasharray="50.26548245743669 50.26548245743669" fill="none" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="0 50 50;360 50 50"/></svg>'); background: rgba(0, 0, 0, 0.5) url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background:0 0"><circle cx="50" cy="50" r="32" stroke-width="8" stroke="%23fff" stroke-dasharray="50.26548245743669 50.26548245743669" fill="none" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="0 50 50;360 50 50"/></svg>');
background-size: 30px 30px; background-size: 30px 30px;
background-position: center center; background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -1010,7 +950,6 @@ html[theme-mode='dark'] {
} }
} }
/* 滚动条样式 */
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
height: 3px; height: 3px;