chat-pc/src/views/message/inner/panel/PanelFooter.vue
Phoenix ef0eb903a7 feat(消息组件): 优化图片消息上传体验并修复文件扩展名获取
1. 在图片消息组件中添加上传进度显示和加载状态
2. 重构图片上传逻辑,先显示本地预览再上传
3. 修复文件消息组件中从文件名获取扩展名改为从文件路径获取
4. 根据消息浮动方向调整提及文本颜色

重构了图片上传流程,现在会先显示本地预览图片,然后在上传过程中显示进度条。同时修复了文件扩展名获取逻辑,现在从文件路径而非文件名获取扩展名。优化了提及文本的颜色显示,使其根据消息浮动方向(左/右)显示不同颜色。
2025-06-05 14:13:50 +08:00

361 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import {
useTalkStore,
useDialogueStore,
useSettingsStore,
useUploadsStore,
useEditorStore,
useUserStore
} from '@/store'
import ws from '@/connect'
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
import { throttle, getVideoImage } from '@/utils/common'
import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import {scrollToBottom} from '@/utils/dom.ts'
const userStore = useUserStore()
const talkStore = useTalkStore()
const editorStore = useEditorStore()
const settingsStore = useSettingsStore()
const uploadsStore = useUploadsStore()
const dialogueStore = useDialogueStore()
const props = defineProps({
uid: {
type: Number,
default: 0
},
talk_type: {
type: Number,
default: 0
},
receiver_id: {
type: Number,
default: 0
},
index_name: {
type: String,
default: ''
},
online: {
type: Boolean,
default: false
},
members: {
default: () => []
}
})
const isShowHistory = ref(false)
const onSendMessage = (data = {}, callBack: any) => {
let message = {
...data,
receiver: {
receiver_id: props.receiver_id,
talk_type: props.talk_type
}
}
ServePublishMessage(message)
.then(({ code, message }) => {
if (code == 200) {
callBack(true)
} else {
window['$message'].warning(message)
}
})
.catch(() => {
window['$message'].warning('网络繁忙,请稍后重试!')
})
}
// 发送文本消息
const onSendTextEvent = throttle((value: any) => {
let { data, callBack } = value
let message = {
type: 'text',
content: data.items[0].content,
quote_id: data.quoteId,
mentions: data.mentionUids
}
onSendMessage(message, (ok: boolean) => {
if (!ok) return
let el = document.getElementById('talk-session-list')
el?.scrollTo({ top: 0, behavior: 'smooth' })
callBack(true)
})
}, 1000)
// 发送图片消息
const onSendImageEvent = ({ data }) => {
console.log('onSendImageEvent')
// 先创建一个带有上传ID的临时消息对象用于显示进度
const uploadId = `image-${Date.now()}-${Math.floor(Math.random() * 1000)}`
// 创建本地预览URL
const previewUrl = URL.createObjectURL(data)
// 创建临时消息记录
const tempMessage = {
msg_id: uploadId,
sequence: Date.now(),
talk_type: props.talk_type,
msg_type: 3, // 图片消息类型
user_id: props.uid,
receiver_id: props.receiver_id,
is_revoke: 0,
is_mark: 0,
is_read: 1,
content: '',
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
extra: {
url: previewUrl, // 使用本地预览URL
size: data.size,
is_uploading: true,
upload_id: uploadId,
percentage: 0
},
isCheck: false,
send_status: 1,
float: 'right' // 我发送的消息显示在右侧
}
// 直接添加到对话记录中
dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(
data,
props.talk_type,
props.receiver_id,
uploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(uploadId, percentage)
},
async () => {
// 清理本地预览URL
URL.revokeObjectURL(previewUrl)
dialogueStore.batchDelDialogueRecord([uploadId])
}
)
}
// 发送视频消息
const onSendVideoEvent = async ({ data }) => {
console.log('onSendVideoEvent')
// 获取视频首帧作为封面图
// let resp = await getVideoImage(data)
// 先创建一个带有上传ID的临时消息对象用于显示进度
const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}`
// 创建临时消息记录
const tempMessage = {
msg_id: uploadId,
sequence: Date.now(),
talk_type: props.talk_type,
msg_type: 5, // 视频消息类型
user_id: props.uid,
receiver_id: props.receiver_id,
is_revoke: 0,
is_mark: 0,
is_read: 1,
content: '',
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
extra: {
url: '',
size: data.size,
is_uploading: true,
upload_id: uploadId,
percentage: 0
},
isCheck: false,
send_status: 1,
float: 'right' // 我发送的消息显示在右侧
}
// 直接添加到对话记录中
dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(
data,
props.talk_type,
props.receiver_id,
uploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(uploadId, percentage)
},
async () => {
dialogueStore.batchDelDialogueRecord([uploadId])
}
)
}
// 发送代码消息
const onSendCodeEvent = ({ data, callBack }) => {
onSendMessage({ type: 'code', code: data.code, lang: data.lang }, callBack)
}
// 发送文件消息
const onSendFileEvent = ({ data }) => {
let maxsize = 200 * 1024 * 1024
if (data.size > maxsize) {
return window['$message'].warning('上传文件不能超过100M!')
}
const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}`
const tempMessage = {
msg_id: clientUploadId,
sequence: Date.now(),
talk_type: props.talk_type,
msg_type: 6,
user_id: props.uid,
receiver_id: props.receiver_id,
nickname: dialogueStore.talk.username,
avatar: userStore.avatar,
is_revoke: 0,
is_read: 0,
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
extra: {
name: data.name,
url: '',
size: data.size,
is_uploading: true,
upload_id: clientUploadId,
percentage: 0
},
float: 'right'
}
dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(data, props.talk_type, props.receiver_id,clientUploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(clientUploadId, percentage)
},
async () => {
dialogueStore.batchDelDialogueRecord([clientUploadId])
}
)
}
// 发送投票消息
const onSendVoteEvent = ({ data, callBack }) => {
let response = ServeSendVote({
receiver_id: props.receiver_id,
mode: data.mode,
anonymous: data.anonymous,
title: data.title,
options: data.options
})
response.then(({ code, message }) => {
if (code == 200) {
callBack(true)
} else {
window['$message'].warning(message)
}
})
response.catch(() => callBack(false))
}
// 发送表情消息
const onSendEmoticonEvent = ({ data, callBack }) => {
onSendMessage({ type: 'emoticon', emoticon_id: data }, callBack)
}
const onSendMixedEvent = ({ data, callBack }) => {
let message = {
type: 'mixed',
quote_id: data.quoteId,
items: data.items
}
onSendMessage(message, callBack)
}
const onKeyboardPush = throttle(() => {
ws.emit('im.message.keyboard', {
sender_id: props.uid,
receiver_id: props.receiver_id
})
}, 3000)
// 编辑器输入事件
const onInputEvent = ({ data }) => {
talkStore.updateItem({
index_name: props.index_name,
draft_text: data
})
// 判断对方是否在线和是否需要推送
// 3秒时间内推送一次
if (settingsStore.isKeyboard && props.online) {
onKeyboardPush()
}
}
// 注册事件
const evnets = {
text_event: onSendTextEvent,
image_event: onSendImageEvent,
video_event: onSendVideoEvent,
code_event: onSendCodeEvent,
file_event: onSendFileEvent,
input_event: onInputEvent,
vote_event: onSendVoteEvent,
emoticon_event: onSendEmoticonEvent,
history_event: () => {
isShowHistory.value = true
},
mixed_event: onSendMixedEvent
}
// 编辑器事件
const onEditorEvent = (msg: any) => {
evnets[msg.event] && evnets[msg.event](msg)
}
onMounted(() => {
editorStore.loadUserEmoticon()
})
</script>
<template>
<footer class="el-footer">
<MultiSelectFooter v-if="dialogueStore.isOpenMultiSelect" />
<Editor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" />
</footer>
<HistoryRecord
v-if="isShowHistory"
:talk-type="talk_type"
:receiver-id="receiver_id"
@close="isShowHistory = false"
/>
</template>
<style lang="less">
.el-footer {
height: inherit;
}
</style>