2025-07-02 03:31:35 +00:00
|
|
|
|
<script setup>
|
|
|
|
|
// 引入Tiptap编辑器相关依赖
|
|
|
|
|
import { Editor, EditorContent, useEditor } from '@tiptap/vue-3'
|
|
|
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
|
|
|
import Image from '@tiptap/extension-image'
|
|
|
|
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
|
|
|
import Mention from '@tiptap/extension-mention'
|
|
|
|
|
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 } from 'naive-ui'
|
|
|
|
|
// 引入图标组件
|
|
|
|
|
import {
|
|
|
|
|
Voice as IconVoice, // 语音图标
|
|
|
|
|
SourceCode, // 代码图标
|
|
|
|
|
Local, // 地理位置图标
|
|
|
|
|
SmilingFace, // 表情图标
|
|
|
|
|
Pic, // 图片图标
|
|
|
|
|
FolderUpload, // 文件上传图标
|
|
|
|
|
Ranking, // 排名图标(用于投票)
|
|
|
|
|
History // 历史记录图标
|
|
|
|
|
} 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 MeEditorVote from './MeEditorVote.vue' // 投票组件
|
|
|
|
|
import MeEditorEmoticon from './MeEditorEmoticon.vue' // 表情组件
|
|
|
|
|
import MeEditorCode from './MeEditorCode.vue' // 代码编辑组件
|
|
|
|
|
import MeEditorRecorder from './MeEditorRecorder.vue' // 录音组件
|
|
|
|
|
// 引入上传API
|
|
|
|
|
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 // 是否显示投票功能
|
|
|
|
|
},
|
|
|
|
|
members: {
|
|
|
|
|
default: () => [] // 聊天成员列表,用于@功能
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 计算当前对话索引名称(标识当前聊天)
|
|
|
|
|
const indexName = computed(() => dialogueStore.index_name)
|
|
|
|
|
// 控制是否显示编辑器的投票界面
|
|
|
|
|
const isShowEditorVote = ref(false)
|
|
|
|
|
// 控制是否显示编辑器的代码界面
|
|
|
|
|
const isShowEditorCode = ref(false)
|
|
|
|
|
// 控制是否显示录音界面
|
|
|
|
|
const isShowEditorRecorder = ref(false)
|
|
|
|
|
// 图片文件上传DOM引用
|
|
|
|
|
const fileImageRef = ref()
|
|
|
|
|
// 文件上传DOM引用
|
|
|
|
|
const uploadFileRef = ref()
|
|
|
|
|
// 表情面板引用
|
|
|
|
|
const emoticonRef = ref()
|
|
|
|
|
// 表情面板显示状态
|
|
|
|
|
const showEmoticon = ref(false)
|
|
|
|
|
|
|
|
|
|
// 自定义Emoji扩展
|
|
|
|
|
const Emoji = Node.create({
|
|
|
|
|
name: 'emoji',
|
|
|
|
|
group: 'inline',
|
|
|
|
|
inline: true,
|
|
|
|
|
selectable: true,
|
|
|
|
|
atom: true,
|
|
|
|
|
|
|
|
|
|
addAttributes() {
|
|
|
|
|
return {
|
|
|
|
|
alt: {
|
|
|
|
|
default: null,
|
|
|
|
|
},
|
|
|
|
|
src: {
|
|
|
|
|
default: null,
|
|
|
|
|
},
|
|
|
|
|
width: {
|
|
|
|
|
default: '24px',
|
|
|
|
|
},
|
|
|
|
|
height: {
|
|
|
|
|
default: '24px',
|
|
|
|
|
},
|
|
|
|
|
class: {
|
|
|
|
|
default: 'ed-emoji',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
parseHTML() {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
tag: 'img.ed-emoji',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
|
return ['img', HTMLAttributes]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 自定义Quote扩展
|
2025-07-02 05:15:16 +00:00
|
|
|
|
const Quote = Node.create({
|
2025-07-02 03:31:35 +00:00
|
|
|
|
name: 'quote',
|
2025-07-02 05:15:16 +00:00
|
|
|
|
group: 'block',
|
|
|
|
|
atom: true,
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
addAttributes() {
|
|
|
|
|
return {
|
2025-07-02 05:15:16 +00:00
|
|
|
|
id: { default: null },
|
|
|
|
|
title: { default: null },
|
|
|
|
|
describe: { default: null },
|
|
|
|
|
image: { default: '' }
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
parseHTML() {
|
2025-07-02 05:15:16 +00:00
|
|
|
|
return [{ tag: 'div.quote-card' }]
|
2025-07-02 03:31:35 +00:00
|
|
|
|
},
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
|
const { id, title, describe, image } = HTMLAttributes
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
|
|
|
|
const titleEl = ['span', { class: 'quote-card-title' }, title || '']
|
|
|
|
|
let contentChildren = [titleEl]
|
|
|
|
|
|
|
|
|
|
if (image && image.length > 0) {
|
|
|
|
|
contentChildren.push(['img', { src: image, style: 'width:30px;height:30px;margin-right:10px;' }])
|
|
|
|
|
} else if (describe) {
|
|
|
|
|
contentChildren.push(['span', { class: 'quote-card-meta' }, describe])
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
|
|
|
|
const cardContent = ['span', { class: 'quote-card-content' }, ...contentChildren]
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'div',
|
|
|
|
|
{
|
|
|
|
|
class: 'quote-card',
|
|
|
|
|
'data-id': id,
|
|
|
|
|
'data-title': title,
|
|
|
|
|
'data-describe': describe,
|
|
|
|
|
'data-image': image || '',
|
|
|
|
|
contenteditable: 'false'
|
|
|
|
|
},
|
|
|
|
|
cardContent
|
|
|
|
|
]
|
2025-07-02 03:31:35 +00:00
|
|
|
|
},
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
addKeyboardShortcuts() {
|
|
|
|
|
return {
|
2025-07-02 05:15:16 +00:00
|
|
|
|
Backspace: () => {
|
2025-07-02 03:31:35 +00:00
|
|
|
|
const { selection } = this.editor.state
|
2025-07-02 05:15:16 +00:00
|
|
|
|
const { $from, empty } = selection
|
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
if (!empty) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
|
|
|
|
if ($from.parent.isTextblock && $from.parentOffset === 0) {
|
|
|
|
|
const nodeBefore = $from.nodeBefore
|
|
|
|
|
if (nodeBefore && nodeBefore.type.name === this.name) {
|
|
|
|
|
return this.editor.commands.deleteNode(this.name)
|
|
|
|
|
}
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
return false
|
2025-07-02 05:15:16 +00:00
|
|
|
|
}
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:15:16 +00:00
|
|
|
|
}
|
2025-07-02 03:31:35 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 创建自定义键盘处理插件,处理Enter键发送消息
|
|
|
|
|
const EnterKeyPlugin = new Plugin({
|
|
|
|
|
key: new PluginKey('enterKey'),
|
|
|
|
|
props: {
|
|
|
|
|
handleKeyDown: (view, event) => {
|
|
|
|
|
// 如果按下Enter键且没有按下Shift键,则发送消息
|
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
onSendMessage()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 自定义键盘扩展
|
|
|
|
|
const CustomKeyboard = Extension.create({
|
|
|
|
|
name: 'customKeyboard',
|
|
|
|
|
|
|
|
|
|
addProseMirrorPlugins() {
|
|
|
|
|
return [
|
|
|
|
|
EnterKeyPlugin,
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 创建编辑器实例
|
|
|
|
|
const editor = useEditor({
|
|
|
|
|
extensions: [
|
|
|
|
|
StarterKit,
|
|
|
|
|
Image.configure({
|
|
|
|
|
inline: true,
|
|
|
|
|
allowBase64: true,
|
|
|
|
|
}),
|
|
|
|
|
Placeholder.configure({
|
|
|
|
|
placeholder: '按Enter发送 / Shift+Enter 换行',
|
|
|
|
|
}),
|
|
|
|
|
Mention.configure({
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class: 'mention',
|
|
|
|
|
},
|
|
|
|
|
suggestion: {
|
|
|
|
|
items: ({ query }) => {
|
|
|
|
|
if (!props.members.length) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let list = [...props.members]
|
|
|
|
|
|
|
|
|
|
if ((dialogueStore.groupInfo).is_manager) {
|
|
|
|
|
list.unshift({ id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return list.filter(
|
|
|
|
|
(item) => item.nickname.toLowerCase().includes(query.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
render: () => {
|
|
|
|
|
let component
|
|
|
|
|
let popup
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
onStart: (props) => {
|
|
|
|
|
// 创建提及列表容器
|
|
|
|
|
popup = document.createElement('div')
|
|
|
|
|
popup.classList.add('ql-mention-list-container', 'me-scrollbar', 'me-scrollbar-thumb')
|
|
|
|
|
document.body.appendChild(popup)
|
|
|
|
|
|
|
|
|
|
// 渲染提及列表
|
|
|
|
|
props.items.forEach((item, index) => {
|
|
|
|
|
const mentionItem = document.createElement('div')
|
|
|
|
|
mentionItem.classList.add('ed-member-item')
|
|
|
|
|
mentionItem.innerHTML = `<img src="${item.avatar}" class="avator"/><span class="nickname">${item.nickname}</span>`
|
|
|
|
|
mentionItem.addEventListener('click', () => {
|
|
|
|
|
props.command({ id: item.id, label: item.nickname })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (index === props.selectedIndex) {
|
|
|
|
|
mentionItem.classList.add('selected')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
popup.appendChild(mentionItem)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 定位提及列表
|
|
|
|
|
const coords = props.clientRect()
|
|
|
|
|
popup.style.position = 'fixed'
|
|
|
|
|
popup.style.top = `${coords.top + window.scrollY}px`
|
|
|
|
|
popup.style.left = `${coords.left + window.scrollX}px`
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onUpdate: (props) => {
|
|
|
|
|
// 更新选中项
|
|
|
|
|
const items = popup.querySelectorAll('.ed-member-item')
|
|
|
|
|
items.forEach((item, index) => {
|
|
|
|
|
if (index === props.selectedIndex) {
|
|
|
|
|
item.classList.add('selected')
|
|
|
|
|
} else {
|
|
|
|
|
item.classList.remove('selected')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onKeyDown: (props) => {
|
|
|
|
|
// 处理键盘事件
|
|
|
|
|
if (props.event.key === 'Escape') {
|
|
|
|
|
popup.remove()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onExit: () => {
|
|
|
|
|
// 清理
|
|
|
|
|
if (popup) {
|
|
|
|
|
popup.remove()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
Link,
|
|
|
|
|
Emoji,
|
|
|
|
|
Quote,
|
|
|
|
|
CustomKeyboard,
|
|
|
|
|
],
|
|
|
|
|
content: '',
|
|
|
|
|
autofocus: true,
|
|
|
|
|
editable: true,
|
|
|
|
|
injectCSS: false,
|
|
|
|
|
onUpdate: () => {
|
|
|
|
|
onEditorChange()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 上传图片函数
|
|
|
|
|
* @param file 文件对象
|
|
|
|
|
* @returns Promise,成功时返回图片URL
|
|
|
|
|
*/
|
|
|
|
|
function onUploadImage(file) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
let image = new Image()
|
|
|
|
|
image.src = URL.createObjectURL(file)
|
|
|
|
|
image.onload = () => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('file', file)
|
|
|
|
|
form.append("source", "fonchain-chat"); // 图片来源标识
|
|
|
|
|
// 添加图片尺寸信息作为URL参数
|
|
|
|
|
form.append("urlParam", `width=${image.width}&height=${image.height}`);
|
|
|
|
|
|
|
|
|
|
// 调用上传API
|
|
|
|
|
uploadImg(form).then(({ code, data, message }) => {
|
|
|
|
|
if (code == 0) {
|
|
|
|
|
resolve(data.ori_url) // 返回原始图片URL
|
|
|
|
|
} else {
|
|
|
|
|
resolve('')
|
|
|
|
|
window['$message'].error(message) // 显示错误信息
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 投票事件处理
|
|
|
|
|
* @param data 投票数据
|
|
|
|
|
*/
|
|
|
|
|
function onVoteEvent(data) {
|
|
|
|
|
const msg = emitCall('vote_event', data, (ok) => {
|
|
|
|
|
if (ok) {
|
|
|
|
|
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: {
|
|
|
|
|
alt: data.value,
|
|
|
|
|
src: data.img,
|
|
|
|
|
width: '24px',
|
|
|
|
|
height: '24px',
|
|
|
|
|
},
|
|
|
|
|
}).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 // 成功后关闭代码界面
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
emit('editor-event', msg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文件上传处理
|
|
|
|
|
* @param e 上传事件对象
|
|
|
|
|
*/
|
|
|
|
|
async function onUploadFile(e) {
|
|
|
|
|
let file = e.target.files[0]
|
|
|
|
|
|
|
|
|
|
e.target.value = null // 清空input,允许再次选择相同文件
|
|
|
|
|
|
|
|
|
|
if (file.type.indexOf('image/') === 0) {
|
|
|
|
|
// 处理图片文件 - 立即显示临时消息,然后上传
|
|
|
|
|
let fn = emitCall('image_event', file, () => {})
|
|
|
|
|
emit('editor-event', fn)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 // 关闭录音界面
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将Tiptap内容转换为消息格式
|
|
|
|
|
function tiptapToMessage() {
|
2025-07-02 05:34:23 +00:00
|
|
|
|
if (!editor.value) return []
|
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
const json = editor.value.getJSON()
|
2025-07-02 05:34:23 +00:00
|
|
|
|
const messages = []
|
|
|
|
|
let quoteId = null
|
|
|
|
|
let currentTextBuffer = ''
|
|
|
|
|
let currentMentions = []
|
|
|
|
|
let currentMentionUids = new Set()
|
|
|
|
|
|
|
|
|
|
const flushTextBuffer = () => {
|
|
|
|
|
const content = currentTextBuffer.trim()
|
|
|
|
|
if (content) {
|
|
|
|
|
const data = {
|
|
|
|
|
items: [{ type: 1, content: content }],
|
|
|
|
|
mentions: [...currentMentions],
|
|
|
|
|
mentionUids: Array.from(currentMentionUids)
|
|
|
|
|
}
|
|
|
|
|
if (quoteId) {
|
|
|
|
|
data.quoteId = quoteId
|
|
|
|
|
quoteId = null
|
|
|
|
|
}
|
|
|
|
|
messages.push({ type: 'text', data })
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
currentTextBuffer = ''
|
|
|
|
|
currentMentions = []
|
|
|
|
|
currentMentionUids.clear()
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
|
|
|
|
|
const processInlines = nodes => {
|
|
|
|
|
nodes.forEach(node => {
|
|
|
|
|
if (node.type === 'text') {
|
|
|
|
|
currentTextBuffer += node.text
|
|
|
|
|
} else if (node.type === 'mention') {
|
|
|
|
|
currentTextBuffer += `@${node.attrs.label} `
|
|
|
|
|
const uid = parseInt(node.attrs.id)
|
|
|
|
|
if (!currentMentionUids.has(uid)) {
|
|
|
|
|
currentMentionUids.add(uid)
|
|
|
|
|
currentMentions.push({ name: `@${node.attrs.label}`, atid: uid })
|
|
|
|
|
}
|
|
|
|
|
} else if (node.type === 'emoji') {
|
|
|
|
|
currentTextBuffer += node.attrs.alt
|
|
|
|
|
} else if (node.type === 'hardBreak') {
|
|
|
|
|
currentTextBuffer += '\n'
|
|
|
|
|
} else if (node.type === 'image') {
|
|
|
|
|
// 处理段落内的图片
|
|
|
|
|
flushTextBuffer()
|
|
|
|
|
const data = {
|
|
|
|
|
...getImageInfo(node.attrs.src),
|
|
|
|
|
url: node.attrs.src
|
|
|
|
|
}
|
|
|
|
|
if (quoteId) {
|
|
|
|
|
data.quoteId = quoteId
|
|
|
|
|
quoteId = null
|
|
|
|
|
}
|
|
|
|
|
messages.push({ type: 'image', data })
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
|
|
|
|
|
if (json.content) {
|
|
|
|
|
const quoteIndex = json.content.findIndex(node => node.type === 'quote')
|
|
|
|
|
if (quoteIndex > -1) {
|
|
|
|
|
quoteId = json.content[quoteIndex].attrs.id
|
|
|
|
|
json.content.splice(quoteIndex, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
json.content.forEach(node => {
|
|
|
|
|
if (node.type === 'paragraph') {
|
|
|
|
|
if (node.content) {
|
|
|
|
|
processInlines(node.content)
|
|
|
|
|
}
|
|
|
|
|
currentTextBuffer += '\n' // Add newline after each paragraph
|
|
|
|
|
} else if (node.type === 'image') {
|
|
|
|
|
flushTextBuffer()
|
|
|
|
|
const data = {
|
|
|
|
|
...getImageInfo(node.attrs.src),
|
|
|
|
|
url: node.attrs.src
|
|
|
|
|
}
|
|
|
|
|
if (quoteId) {
|
|
|
|
|
data.quoteId = quoteId
|
|
|
|
|
quoteId = null
|
|
|
|
|
}
|
|
|
|
|
messages.push({ type: 'image', data })
|
|
|
|
|
}
|
2025-07-02 03:31:35 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
|
|
|
|
|
flushTextBuffer()
|
|
|
|
|
|
|
|
|
|
if (messages.length > 0) {
|
|
|
|
|
const lastMessage = messages[messages.length - 1]
|
|
|
|
|
if (lastMessage.type === 'text') {
|
|
|
|
|
lastMessage.data.items[0].content = lastMessage.data.items[0].content.trim()
|
|
|
|
|
if (!lastMessage.data.items[0].content) {
|
|
|
|
|
messages.pop()
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
|
|
|
|
|
return messages
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将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' &&
|
|
|
|
|
(!json.content[0].content || json.content[0].content.length === 0)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送消息处理
|
|
|
|
|
* 根据编辑器内容类型发送不同类型的消息
|
|
|
|
|
*/
|
|
|
|
|
function onSendMessage() {
|
2025-07-02 05:34:23 +00:00
|
|
|
|
if (!editor.value || isEditorEmpty()) return
|
|
|
|
|
|
|
|
|
|
const messages = tiptapToMessage()
|
|
|
|
|
|
|
|
|
|
if (messages.length === 0) {
|
|
|
|
|
return
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 05:34:23 +00:00
|
|
|
|
let canClear = true
|
|
|
|
|
messages.forEach(msg => {
|
|
|
|
|
if (msg.type === 'text') {
|
|
|
|
|
if (msg.data.items[0].content.length > 1024) {
|
|
|
|
|
window['$message'].info('发送内容超长,请分条发送')
|
|
|
|
|
canClear = false
|
|
|
|
|
return
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
2025-07-02 05:34:23 +00:00
|
|
|
|
emit('editor-event', emitCall('text_event', msg.data))
|
|
|
|
|
} else if (msg.type === 'image') {
|
|
|
|
|
const data = {
|
|
|
|
|
height: 0,
|
|
|
|
|
width: 0,
|
|
|
|
|
size: 10000,
|
|
|
|
|
url: msg.data.url,
|
|
|
|
|
}
|
|
|
|
|
emit('editor-event', emitCall('image_event', data))
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-07-02 03:31:35 +00:00
|
|
|
|
|
2025-07-02 05:34:23 +00:00
|
|
|
|
if (canClear) {
|
|
|
|
|
editor.value?.commands.clearContent(true)
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 编辑器内容改变时的处理
|
|
|
|
|
* 保存草稿并触发输入事件
|
|
|
|
|
*/
|
|
|
|
|
function onEditorChange() {
|
|
|
|
|
if (!editor.value) return
|
|
|
|
|
|
|
|
|
|
const text = tiptapToString()
|
|
|
|
|
|
|
|
|
|
if (!isEditorEmpty()) {
|
|
|
|
|
// 保存草稿到store
|
|
|
|
|
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
|
|
|
|
|
text: text,
|
|
|
|
|
content: editor.value.getJSON()
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// 编辑器为空时删除对应草稿
|
|
|
|
|
delete editorDraftStore.items[indexName.value || '']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 触发输入事件
|
|
|
|
|
emit('editor-event', emitCall('input_event', text))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加载编辑器草稿内容
|
|
|
|
|
* 当切换聊天对象时,加载对应的草稿
|
|
|
|
|
*/
|
|
|
|
|
function loadEditorDraftText() {
|
|
|
|
|
if (!editor.value) return
|
|
|
|
|
|
|
|
|
|
// 从缓存中加载编辑器草稿
|
|
|
|
|
let draft = editorDraftStore.items[indexName.value || '']
|
|
|
|
|
if (draft) {
|
|
|
|
|
const parsed = JSON.parse(draft)
|
|
|
|
|
if (parsed.content) {
|
|
|
|
|
editor.value.commands.setContent(parsed.content)
|
|
|
|
|
} else if (parsed.text) {
|
|
|
|
|
editor.value.commands.setContent(parsed.text)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
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: {
|
|
|
|
|
id: data?.id,
|
|
|
|
|
label: data.value,
|
|
|
|
|
},
|
|
|
|
|
}).run()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理引用事件
|
|
|
|
|
* @param data 引用数据
|
|
|
|
|
*/
|
|
|
|
|
function onSubscribeQuote(data) {
|
|
|
|
|
if (!editor.value) return
|
2025-07-02 05:15:16 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
// 检查是否已有引用内容
|
|
|
|
|
const json = editor.value.getJSON()
|
2025-07-02 05:15:16 +00:00
|
|
|
|
if (json.content?.some(node => node.type === 'quote')) {
|
|
|
|
|
return // 已有引用则不再添加
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在编辑器开头插入引用
|
2025-07-02 05:15:16 +00:00
|
|
|
|
editor.value
|
|
|
|
|
.chain()
|
|
|
|
|
.focus()
|
|
|
|
|
.insertContentAt(0, [
|
|
|
|
|
{
|
|
|
|
|
type: 'quote',
|
|
|
|
|
attrs: data
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'paragraph'
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
.run()
|
2025-07-02 03:31:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理编辑消息事件
|
|
|
|
|
* @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() // 触发图片上传
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '文件',
|
|
|
|
|
icon: markRaw(FolderUpload),
|
|
|
|
|
show: true,
|
|
|
|
|
click: () => {
|
|
|
|
|
uploadFileRef.value.click() // 触发文件上传
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-07-02 05:34:23 +00:00
|
|
|
|
|
2025-07-02 03:31:35 +00:00
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// 监听聊天索引变化,切换聊天时加载对应草稿
|
|
|
|
|
watch(indexName, loadEditorDraftText, { immediate: true })
|
|
|
|
|
|
|
|
|
|
// 组件挂载时初始化
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadEditorDraftText()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 订阅编辑器相关事件总线事件
|
|
|
|
|
useEventBus([
|
|
|
|
|
{ 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"
|
|
|
|
|
raw
|
|
|
|
|
:show-arrow="false"
|
|
|
|
|
:width="300"
|
|
|
|
|
ref="emoticonRef"
|
|
|
|
|
v-model:show="showEmoticon"
|
|
|
|
|
style="width: 500px; height: 250px; border-radius: 10px; overflow: hidden"
|
|
|
|
|
>
|
|
|
|
|
<template #trigger>
|
|
|
|
|
<div class="item pointer">
|
|
|
|
|
<n-icon size="18" class="icon" :component="SmilingFace" />
|
|
|
|
|
<p class="tip-title">表情符号</p>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<MeEditorEmoticon @on-select="onEmoticonEvent" />
|
|
|
|
|
</n-popover>
|
|
|
|
|
|
|
|
|
|
<!-- 工具栏其他功能按钮 -->
|
|
|
|
|
<div
|
|
|
|
|
class="item pointer"
|
|
|
|
|
v-for="nav in navs"
|
|
|
|
|
:key="nav.title"
|
|
|
|
|
v-show="nav.show"
|
|
|
|
|
@click="nav.click"
|
|
|
|
|
>
|
|
|
|
|
<n-icon size="18" class="icon" :component="nav.icon" />
|
|
|
|
|
<p class="tip-title">{{ nav.title }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- 编辑器主体区域 -->
|
|
|
|
|
<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
|
|
|
|
|
v-if="isShowEditorCode"
|
|
|
|
|
@on-submit="onCodeEvent"
|
|
|
|
|
@close="isShowEditorCode = false"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<MeEditorRecorder
|
|
|
|
|
v-if="isShowEditorRecorder"
|
|
|
|
|
@on-submit="onRecorderEvent"
|
|
|
|
|
@close="isShowEditorRecorder = false"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
.editor {
|
|
|
|
|
--tip-bg-color: rgb(241 241 241 / 90%); /* 提示背景颜色 */
|
|
|
|
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
height: 38px;
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
.tools {
|
|
|
|
|
height: 100%;
|
|
|
|
|
flex: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
.item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 35px;
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
position: relative;
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
.tip-title {
|
|
|
|
|
display: none; /* 默认隐藏提示文字 */
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 40px;
|
|
|
|
|
left: 0px;
|
|
|
|
|
line-height: 26px;
|
|
|
|
|
background-color: var(--tip-bg-color);
|
|
|
|
|
color: var(--im-text-color);
|
|
|
|
|
min-width: 20px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
white-space: pre;
|
|
|
|
|
user-select: none;
|
|
|
|
|
z-index: 999999999999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
.tip-title {
|
|
|
|
|
display: block; /* 悬停时显示提示文字 */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 暗色模式样式调整 */
|
|
|
|
|
html[theme-mode='dark'] {
|
|
|
|
|
.editor {
|
|
|
|
|
--tip-bg-color: #48484d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<style lang="less">
|
|
|
|
|
/* 全局编辑器样式 */
|
|
|
|
|
.tiptap-editor {
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
outline: none;
|
|
|
|
|
|
|
|
|
|
/* 滚动条样式 */
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
width: 3px;
|
|
|
|
|
height: 3px;
|
|
|
|
|
background-color: unset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 悬停时显示滚动条 */
|
|
|
|
|
&:hover {
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
background-color: var(--im-scrollbar-thumb);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 占位符样式 */
|
|
|
|
|
.is-empty::before {
|
|
|
|
|
content: attr(data-placeholder);
|
|
|
|
|
float: left;
|
|
|
|
|
color: #b8b3b3;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
height: 0;
|
|
|
|
|
font-family: PingFang SC, Microsoft YaHei, 'Alibaba PuHuiTi 2.0 45' !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 编辑器中图片样式 */
|
|
|
|
|
img {
|
|
|
|
|
max-width: 100px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
background-color: #48484d;
|
|
|
|
|
margin: 0px 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 表情符号样式 */
|
|
|
|
|
.ed-emoji {
|
|
|
|
|
background-color: unset !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 提及样式 */
|
|
|
|
|
.mention {
|
|
|
|
|
color: #0366d6;
|
|
|
|
|
background-color: rgba(3, 102, 214, 0.1);
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
padding: 0 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 引用卡片样式 */
|
|
|
|
|
.quote-card-wrapper {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quote-card-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
background-color: #f6f6f6;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
.quote-card-title {
|
|
|
|
|
height: 22px;
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
|
|
.quote-card-remove {
|
|
|
|
|
margin-right: 15px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quote-card-meta {
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 20px;
|
|
|
|
|
color: #999;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 提及列表样式 */
|
|
|
|
|
.ql-mention-list-container {
|
|
|
|
|
width: 270px;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
|
|
|
|
.ed-member-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
&:hover, &.selected {
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avator {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nickname {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 暗色模式下的样式调整 */
|
|
|
|
|
html[theme-mode='dark'] {
|
|
|
|
|
.tiptap-editor {
|
|
|
|
|
.is-empty::before {
|
|
|
|
|
color: #57575a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quote-card-content {
|
|
|
|
|
background-color: var(--im-message-bg-color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ql-mention-list-container {
|
|
|
|
|
background-color: #1e1e1e;
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
|
|
|
|
.ed-member-item {
|
|
|
|
|
&:hover, &.selected {
|
|
|
|
|
background-color: #2c2c2c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nickname {
|
|
|
|
|
color: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|