1. 在图片消息组件中添加上传进度显示和加载状态 2. 重构图片上传逻辑,先显示本地预览再上传 3. 修复文件消息组件中从文件名获取扩展名改为从文件路径获取 4. 根据消息浮动方向调整提及文本颜色 重构了图片上传流程,现在会先显示本地预览图片,然后在上传过程中显示进度条。同时修复了文件扩展名获取逻辑,现在从文件路径而非文件名获取扩展名。优化了提及文本的颜色显示,使其根据消息浮动方向(左/右)显示不同颜色。
856 lines
21 KiB
Vue
856 lines
21 KiB
Vue
<script lang="ts" setup>
|
||
// 引入Quill编辑器的样式文件
|
||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||
// 引入图片上传插件的样式
|
||
import 'quill-image-uploader/dist/quill.imageUploader.min.css'
|
||
// 引入自定义的提及功能样式
|
||
import '@/assets/css/editor-mention.less'
|
||
// 引入Vue核心功能
|
||
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted } 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'
|
||
// 引入Quill编辑器及其核心实例
|
||
import { QuillEditor, Quill } from '@vueup/vue-quill'
|
||
// 引入图片上传插件
|
||
import ImageUploader from 'quill-image-uploader'
|
||
// 引入自定义表情符号格式
|
||
import EmojiBlot from './formats/emoji'
|
||
// 引入自定义引用格式
|
||
import QuoteBlot from './formats/quote'
|
||
// 引入提及功能
|
||
import 'quill-mention'
|
||
// 引入状态管理
|
||
import { useDialogueStore, useEditorDraftStore } from '@/store'
|
||
// 引入编辑器工具函数
|
||
import { deltaToMessage, deltaToString, isEmptyDelta } from './util'
|
||
// 引入获取图片信息的工具函数
|
||
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 { ServeUploadImage } from '@/api/upload'
|
||
import { uploadImg } from '@/api/upload'
|
||
// 引入事件总线钩子
|
||
import { useEventBus } from '@/hooks'
|
||
// 注册Quill编辑器的自定义格式
|
||
Quill.register('formats/emoji', EmojiBlot) // 注册表情格式
|
||
Quill.register('formats/quote', QuoteBlot) // 注册引用格式
|
||
Quill.register('modules/imageUploader', ImageUploader) // 注册图片上传模块
|
||
|
||
// 定义组件的事件
|
||
const emit = defineEmits(['editor-event'])
|
||
// 获取对话状态管理
|
||
const dialogueStore = useDialogueStore()
|
||
// 获取编辑器草稿状态管理
|
||
const editorDraftStore = useEditorDraftStore()
|
||
// 定义组件props
|
||
const props = defineProps({
|
||
vote: {
|
||
type: Boolean,
|
||
default: false // 是否显示投票功能
|
||
},
|
||
members: {
|
||
default: () => [] // 聊天成员列表,用于@功能
|
||
}
|
||
})
|
||
|
||
// 编辑器引用
|
||
const editor = ref()
|
||
|
||
// 获取Quill编辑器实例
|
||
const getQuill = () => {
|
||
return editor.value?.getQuill()
|
||
}
|
||
|
||
// 获取当前编辑器光标位置
|
||
const getQuillSelectionIndex = () => {
|
||
let quill = getQuill()
|
||
|
||
return (quill.getSelection() || {}).index || quill.getLength()
|
||
}
|
||
|
||
// 计算当前对话索引名称(标识当前聊天)
|
||
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 editorOption = {
|
||
debug: false,
|
||
modules: {
|
||
toolbar: false, // 禁用默认工具栏
|
||
clipboard: {
|
||
// 粘贴处理,去除粘贴时的自带样式
|
||
matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]]
|
||
},
|
||
|
||
keyboard: {
|
||
bindings: {
|
||
enter: {
|
||
key: 13,
|
||
handler: onSendMessage // 按Enter键发送消息
|
||
}
|
||
}
|
||
},
|
||
|
||
// 图片上传配置
|
||
imageUploader: {
|
||
upload: onEditorUpload
|
||
},
|
||
|
||
// @功能配置
|
||
mention: {
|
||
allowedChars: /^[\u4e00-\u9fa5]*$/, // 允许中文字符
|
||
mentionDenotationChars: ['@'], // @符号触发
|
||
positioningStrategy: 'fixed', // 定位策略
|
||
// 渲染@项目的函数
|
||
renderItem: (data: any) => {
|
||
const el = document.createElement('div')
|
||
el.className = 'ed-member-item'
|
||
el.innerHTML = `<img src="${data.avatar}" class="avator"/>`
|
||
el.innerHTML += `<span class="nickname">${data.nickname}</span>`
|
||
return el
|
||
},
|
||
// 数据源函数,过滤匹配的用户
|
||
source: function (searchTerm: string, renderList: any) {
|
||
console.log("source")
|
||
if (!props.members.length) {
|
||
return renderList([])
|
||
}
|
||
let list = [
|
||
...props.members
|
||
] as any
|
||
if((dialogueStore.groupInfo as any).is_manager){
|
||
list.unshift({ id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' })
|
||
}
|
||
const items = list.filter(
|
||
(item: any) => item.nickname.toLowerCase().indexOf(searchTerm) !== -1
|
||
)
|
||
|
||
renderList(items)
|
||
},
|
||
mentionContainerClass: 'ql-mention-list-container me-scrollbar me-scrollbar-thumb'
|
||
}
|
||
},
|
||
placeholder: '按Enter发送 / Shift+Enter 换行',
|
||
theme: 'snow' // 使用snow主题
|
||
}
|
||
|
||
// 底部工具栏配置
|
||
const navs = reactive([
|
||
{
|
||
title: '图片',
|
||
icon: markRaw(Pic),
|
||
show: true,
|
||
click: () => {
|
||
fileImageRef.value.click() // 触发图片上传
|
||
}
|
||
},
|
||
{
|
||
title: '文件',
|
||
icon: markRaw(FolderUpload),
|
||
show: true,
|
||
click: () => {
|
||
uploadFileRef.value.click() // 触发文件上传
|
||
}
|
||
},
|
||
// 以下功能已被注释掉,但保留代码
|
||
// {
|
||
// title: '代码',
|
||
// icon: markRaw(SourceCode),
|
||
// show: true,
|
||
// click: () => {
|
||
// isShowEditorCode.value = true
|
||
// }
|
||
// },
|
||
// {
|
||
// title: '语音消息',
|
||
// icon: markRaw(IconVoice),
|
||
// show: true,
|
||
// click: () => {
|
||
// isShowEditorRecorder.value = true
|
||
// }
|
||
// },
|
||
// {
|
||
// title: '地理位置',
|
||
// icon: markRaw(Local),
|
||
// show: true,
|
||
// click: () => {}
|
||
// },
|
||
// {
|
||
// title: '群投票',
|
||
// icon: markRaw(Ranking),
|
||
// show: computed(() => props.vote),
|
||
// click: () => {
|
||
// isShowEditorVote.value = true
|
||
// }
|
||
// },
|
||
// {
|
||
// title: '历史记录',
|
||
// icon: markRaw(History),
|
||
// show: true,
|
||
// click: () => {
|
||
// emit('editor-event', emitCall('history_event'))
|
||
// }
|
||
// }
|
||
])
|
||
|
||
/**
|
||
* 上传图片函数
|
||
* @param file 文件对象
|
||
* @returns Promise,成功时返回图片URL
|
||
*/
|
||
function onUploadImage(file: 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 file 要上传的文件
|
||
* @returns Promise
|
||
*/
|
||
function onEditorUpload(file: File) {
|
||
async function fn(file: File, resolve: Function, reject: Function) {
|
||
if (file.type.indexOf('image/') === 0) {
|
||
// 如果是图片,使用图片上传处理
|
||
return resolve(await onUploadImage(file))
|
||
}
|
||
|
||
reject()
|
||
|
||
// 非图片文件的处理
|
||
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)
|
||
}
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
fn(file, resolve, reject)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 投票事件处理
|
||
* @param data 投票数据
|
||
*/
|
||
function onVoteEvent(data: any) {
|
||
const msg = emitCall('vote_event', data, (ok: boolean) => {
|
||
if (ok) {
|
||
isShowEditorVote.value = false // 成功后关闭投票界面
|
||
}
|
||
})
|
||
|
||
emit('editor-event', msg)
|
||
}
|
||
|
||
/**
|
||
* 表情事件处理
|
||
* @param data 表情数据
|
||
*/
|
||
function onEmoticonEvent(data: any) {
|
||
emoticonRef.value.setShow(false) // 关闭表情面板
|
||
|
||
if (data.type == 1) {
|
||
// 插入文本表情
|
||
const quill = getQuill()
|
||
let index = getQuillSelectionIndex()
|
||
|
||
// 删除编辑器中多余的换行符
|
||
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
|
||
quill.deleteText(0, 1)
|
||
index = 0
|
||
}
|
||
|
||
if (data.img) {
|
||
// 插入图片表情
|
||
quill.insertEmbed(index, 'emoji', {
|
||
alt: data.value,
|
||
src: data.img,
|
||
width: '24px',
|
||
height: '24px'
|
||
})
|
||
} else {
|
||
// 插入文本表情
|
||
quill.insertText(index, data.value)
|
||
}
|
||
|
||
// 设置光标位置
|
||
quill.setSelection(index + 1, 0, 'user')
|
||
} else {
|
||
// 发送整个表情包
|
||
let fn = emitCall('emoticon_event', data.value, () => {})
|
||
emit('editor-event', fn)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 代码事件处理
|
||
* @param data 代码数据
|
||
*/
|
||
function onCodeEvent(data: any) {
|
||
const msg = emitCall('code_event', data, (ok: boolean) => {
|
||
isShowEditorCode.value = false // 成功后关闭代码界面
|
||
})
|
||
|
||
emit('editor-event', msg)
|
||
}
|
||
|
||
/**
|
||
* 文件上传处理
|
||
* @param e 上传事件对象
|
||
*/
|
||
async function onUploadFile(e: any) {
|
||
let file = e.target.files[0]
|
||
|
||
e.target.value = null // 清空input,允许再次选择相同文件
|
||
|
||
console.log("文件类型"+file.type)
|
||
if (file.type.indexOf('image/') === 0) {
|
||
console.log("进入图片")
|
||
// 处理图片文件 - 立即显示临时消息,然后上传
|
||
let fn = emitCall('image_event', file, () => {})
|
||
emit('editor-event', fn)
|
||
|
||
return
|
||
}
|
||
|
||
if (file.type.indexOf('video/') === 0) {
|
||
console.log("进入视频")
|
||
// 处理视频文件
|
||
let fn = emitCall('video_event', file, () => {})
|
||
emit('editor-event', fn)
|
||
} else {
|
||
console.log("进入其他")
|
||
// 处理其他类型文件
|
||
let fn = emitCall('file_event', file, () => {})
|
||
emit('editor-event', fn)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 录音事件处理
|
||
* @param file 录音文件
|
||
*/
|
||
function onRecorderEvent(file: any) {
|
||
emit('editor-event', emitCall('file_event', file))
|
||
isShowEditorRecorder.value = false // 关闭录音界面
|
||
}
|
||
|
||
/**
|
||
* 粘贴内容处理,移除粘贴内容中的样式
|
||
* @param node DOM节点
|
||
* @param Delta Quill Delta对象
|
||
* @returns 处理后的Delta
|
||
*/
|
||
function onClipboardMatcher(node: any, Delta) {
|
||
const ops: any[] = []
|
||
|
||
Delta.ops.forEach((op) => {
|
||
// 处理粘贴内容
|
||
if (op.insert && typeof op.insert === 'string') {
|
||
ops.push({
|
||
insert: op.insert, // 文字内容
|
||
attributes: {} // 移除所有样式
|
||
})
|
||
} else {
|
||
ops.push(op)
|
||
}
|
||
})
|
||
|
||
Delta.ops = ops
|
||
return Delta
|
||
}
|
||
|
||
/**
|
||
* 发送消息处理
|
||
* 根据编辑器内容类型发送不同类型的消息
|
||
*/
|
||
function onSendMessage() {
|
||
var delta = getQuill().getContents()
|
||
let data = deltaToMessage(delta) // 转换Delta为消息格式
|
||
if (data.items.length === 0||!data.items[0].content.trim()) {
|
||
return // 没有内容不发送
|
||
}
|
||
|
||
switch (data.msgType) {
|
||
case 1: // 文字消息
|
||
if (data.items[0].content.length > 1024) {
|
||
return window['$message'].info('发送内容超长,请分条发送')
|
||
}
|
||
|
||
// 发送文本消息
|
||
emit(
|
||
'editor-event',
|
||
emitCall('text_event', data, (ok: any) => {
|
||
ok && getQuill().setContents([], Quill.sources.USER) // 成功发送后清空编辑器
|
||
})
|
||
)
|
||
break
|
||
case 3: // 图片消息
|
||
// 发送图片消息
|
||
emit(
|
||
'editor-event',
|
||
emitCall(
|
||
'image_event',
|
||
{ ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
|
||
(ok: any) => {
|
||
ok && getQuill().setContents([]) // 成功发送后清空编辑器
|
||
}
|
||
)
|
||
)
|
||
break
|
||
case 12: // 图文混合消息
|
||
// 发送混合消息
|
||
emit(
|
||
'editor-event',
|
||
emitCall('mixed_event', data, (ok: any) => {
|
||
ok && getQuill().setContents([]) // 成功发送后清空编辑器
|
||
})
|
||
)
|
||
break
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 编辑器内容改变时的处理
|
||
* 保存草稿并触发输入事件
|
||
*/
|
||
function onEditorChange() {
|
||
let delta = getQuill().getContents()
|
||
let text = deltaToString(delta) // 将Delta转为纯文本
|
||
|
||
if (!isEmptyDelta(delta)) {
|
||
// 保存草稿到store
|
||
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
|
||
text: text,
|
||
ops: delta.ops
|
||
})
|
||
} else {
|
||
// 编辑器为空时删除对应草稿
|
||
delete editorDraftStore.items[indexName.value || '']
|
||
}
|
||
|
||
// 触发输入事件
|
||
emit('editor-event', emitCall('input_event', text))
|
||
}
|
||
|
||
/**
|
||
* 加载编辑器草稿内容
|
||
* 当切换聊天对象时,加载对应的草稿
|
||
*/
|
||
function loadEditorDraftText() {
|
||
if (!editor.value) return
|
||
|
||
// 延迟处理,确保DOM已渲染
|
||
setTimeout(() => {
|
||
hideMentionDom() // 隐藏@菜单
|
||
|
||
const quill = getQuill()
|
||
|
||
if (!quill) return
|
||
|
||
// 从缓存中加载编辑器草稿
|
||
let draft = editorDraftStore.items[indexName.value || '']
|
||
if (draft) {
|
||
quill.setContents(JSON.parse(draft)?.ops || [])
|
||
} else {
|
||
quill.setContents([]) // 没有草稿则清空编辑器
|
||
}
|
||
|
||
// 设置光标位置到末尾
|
||
const index = getQuillSelectionIndex()
|
||
quill.setSelection(index, 0, 'user')
|
||
}, 0)
|
||
}
|
||
|
||
/**
|
||
* 处理@成员事件
|
||
* @param data @成员数据
|
||
*/
|
||
function onSubscribeMention(data: any) {
|
||
const mention = getQuill().getModule('mention')
|
||
// 插入@项
|
||
mention.insertItem({ id: data?.id, denotationChar: '@', value: data.value }, true)
|
||
}
|
||
|
||
/**
|
||
* 处理引用事件
|
||
* @param data 引用数据
|
||
*/
|
||
function onSubscribeQuote(data: any) {
|
||
// 检查是否已有引用内容
|
||
const delta = getQuill().getContents()
|
||
if (delta.ops?.some((item: any) => item.insert.quote)) {
|
||
return // 已有引用则不再添加
|
||
}
|
||
|
||
const quill = getQuill()
|
||
const index = getQuillSelectionIndex()
|
||
|
||
// 在编辑器开头插入引用
|
||
quill.insertEmbed(0, 'quote', data)
|
||
quill.setSelection(index + 1, 0, 'user') // 设置光标到引用后
|
||
}
|
||
|
||
/**
|
||
* 隐藏@成员DOM元素
|
||
*/
|
||
function hideMentionDom() {
|
||
let el = document.querySelector('.ql-mention-list-container')
|
||
if (el) {
|
||
document.querySelector('body')?.removeChild(el)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理编辑消息事件
|
||
* @param data 消息数据
|
||
*/
|
||
function onSubscribeEdit(data: any) {
|
||
const quill = getQuill()
|
||
if (!quill) return
|
||
|
||
// 清空当前编辑器内容
|
||
quill.setContents([])
|
||
|
||
// 插入要编辑的文本内容
|
||
quill.setText(data.content)
|
||
|
||
// 设置光标位置到末尾
|
||
const index = quill.getLength() - 1
|
||
quill.setSelection(index > 0 ? index : 0, 0, 'user')
|
||
}
|
||
|
||
// 监听聊天索引变化,切换聊天时加载对应草稿
|
||
watch(indexName, loadEditorDraftText, { immediate: true })
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
loadEditorDraftText()
|
||
})
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
hideMentionDom()
|
||
})
|
||
|
||
// 订阅编辑器相关事件总线事件
|
||
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"
|
||
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">
|
||
<QuillEditor
|
||
ref="editor"
|
||
id="editor"
|
||
:options="editorOption"
|
||
@editorChange="onEditorChange"
|
||
style="height: 100%; border: none"
|
||
/>
|
||
</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">
|
||
/* 全局编辑器样式 */
|
||
#editor {
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 编辑器主体区域样式 */
|
||
.ql-editor {
|
||
padding: 8px;
|
||
|
||
/* 滚动条样式 */
|
||
&::-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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 编辑器占位符样式 */
|
||
.ql-editor.ql-blank::before {
|
||
font-family:
|
||
PingFang SC,
|
||
Microsoft YaHei,
|
||
'Alibaba PuHuiTi 2.0 45' !important;
|
||
left: 8px;
|
||
}
|
||
|
||
/* 编辑器中图片样式 */
|
||
.ql-snow .ql-editor img {
|
||
max-width: 100px;
|
||
border-radius: 3px;
|
||
background-color: #48484d;
|
||
margin: 0px 2px;
|
||
}
|
||
|
||
/* 图片上传中样式 */
|
||
.image-uploading {
|
||
display: flex;
|
||
width: 100px;
|
||
height: 100px;
|
||
background: #f5f5f5;
|
||
border-radius: 5px;
|
||
|
||
img {
|
||
filter: unset;
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* 表情符号样式 */
|
||
.ed-emoji {
|
||
background-color: unset !important;
|
||
}
|
||
|
||
/* 编辑器占位符样式 */
|
||
.ql-editor.ql-blank::before {
|
||
font-style: unset;
|
||
color: #b8b3b3;
|
||
}
|
||
|
||
/* 引用卡片样式 */
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* 暗色模式下的样式调整 */
|
||
html[theme-mode='dark'] {
|
||
.ql-editor.ql-blank::before {
|
||
color: #57575a;
|
||
}
|
||
|
||
.quote-card-content {
|
||
background-color: var(--im-message-bg-color);
|
||
}
|
||
}
|
||
</style>
|