refactor(TiptapEditor): 清理代码注释和格式化代码
This commit is contained in:
parent
3f89777bf8
commit
b18a1b2604
@ -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) => {
|
||||||
// 如果按下Enter键且没有按下Shift键,则发送消息
|
|
||||||
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;
|
||||||
|
Loading…
Reference in New Issue
Block a user