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