🐛 fix(im):codex 评审修复发送中状态边角

- recomputeConversationLast 空 messages 时一并清 lastSendTime,
  避免刷新后被 drop 的媒体占位让空会话仍按旧时间排在列表前面
- 视频占位 coverUrl 不再赋 blob URL:<video poster> 期待图片资源,
  传 video blob 在部分浏览器会退化成黑底,cover 等 probe 出真实 URL 后由 commit 阶段一起 patch
- useMediaUploader 暴露 requireMediaHandler typed accessor,
  消除 video 链路 mediaTypeHandlers[VIDEO]! 非空断言
- MessageItem 把外层 loading 的 (!isUploading || isVoice) 抽成 showSendingLoading computed
im
YunaiV 2026-05-06 10:18:49 +08:00
parent 459eaa5428
commit 30d695d702
4 changed files with 38 additions and 15 deletions

View File

@ -35,7 +35,7 @@ export interface MediaTypeContext {
videoCoverUrl?: string
}
interface MediaTypeHandler {
export interface MediaTypeHandler {
/** 中文名,仅日志用(替代之前散落 9 处的 kind 字符串) */
kind: string
/** 由 file + url + context 生成 payload占位时 url 是 blob URLcommit 时是真实 url */
@ -201,6 +201,20 @@ export const useMediaUploader = () => {
/** 取媒体类型中文名(仅日志用);未注册 type 退化为通用「媒体」 */
const getMediaKind = (type: number): string => mediaTypeHandlers[type]?.kind ?? '媒体'
/**
* type handler
*
* `MediaTypeHandler` undefined `!`
* type image/file/voice/video dispatcher `mediaTypeHandlers[type]?.` optional chain
*/
const requireMediaHandler = (type: number): MediaTypeHandler => {
const handler = mediaTypeHandlers[type]
if (!handler) {
throw new Error(`[IM] 未注册的媒体类型 ${type}`)
}
return handler
}
/**
* + markMediaFailed + false
*
@ -317,6 +331,7 @@ export const useMediaUploader = () => {
commitMediaPlaceholder,
createUploadProgressHandler,
verifyMediaUploadStillAllowed,
getMediaKind
getMediaKind,
requireMediaHandler
}
}

View File

@ -170,10 +170,7 @@ import { useFriendStore } from '@/views/im/home/store/friendStore'
import { useDraftStore } from '@/views/im/home/store/draftStore'
import { getMemberDisplayName } from '@/views/im/utils/user'
import { useMessageSender } from '@/views/im/home/composables/useMessageSender'
import {
mediaTypeHandlers,
useMediaUploader
} from '@/views/im/home/composables/useMediaUploader'
import { useMediaUploader } from '@/views/im/home/composables/useMediaUploader'
import { useMuteOverlay } from '@/views/im/home/composables/useMuteOverlay'
import { getConversationKey } from '@/views/im/utils/conversation'
import { ImConversationType, ImMessageType } from '@/views/im/utils/constants'
@ -203,7 +200,8 @@ const {
markMediaFailed,
commitMediaPlaceholder,
createUploadProgressHandler,
verifyMediaUploadStillAllowed
verifyMediaUploadStillAllowed,
requireMediaHandler
} = useMediaUploader()
const editorRef = useTemplateRef<HTMLDivElement>('editorRef')
@ -997,13 +995,13 @@ async function uploadAndSendVideo(file: File) {
const replyQuote = context.quote
const startKey = getConversationKey(conversation)
// 1. blob URL url + coverUrl <video> _localFile file
// 1. url blob <video src> coverUrl blob
// <video poster> video blob 退
// cover probe URL commit patch_localFile file
// payload mediaTypeHandlers[VIDEO].build commit
const videoHandler = mediaTypeHandlers[ImMessageType.VIDEO]!
const videoHandler = requireMediaHandler(ImMessageType.VIDEO)
const buildPlaceholderContent = (blobUrl: string): string =>
serializeMessage(
withQuotePayload(videoHandler.build(file, blobUrl, { videoCoverUrl: blobUrl }), replyQuote)
)
serializeMessage(withQuotePayload(videoHandler.build(file, blobUrl, {}), replyQuote))
const { clientMessageId } = insertMediaPlaceholder({
file,
type: ImMessageType.VIDEO,

View File

@ -193,10 +193,8 @@
<!-- 状态区自己消息展示发送状态 + 已读/群回执对方消息 + @自己时展示 @徽标 -->
<div class="flex gap-1.5 items-center text-base">
<template v-if="message.selfSend">
<!-- SENDING 显示外层 loading图片/视频/文件气泡自身有进度反馈则抑制
语音气泡只有麦克风 + 时长无内嵌进度条必须保留外层 loading 让用户感知正在发送 -->
<Icon
v-if="message.status === ImMessageStatus.SENDING && (!isUploading || isVoice)"
v-if="showSendingLoading"
icon="ant-design:loading-outlined"
class="im-loading-spin"
/>
@ -506,6 +504,16 @@ const uploadProgress = computed(() => props.message.uploadProgress ?? 0)
/** 上传进度文案;图片/视频遮罩、文件进度条尾巴共用 */
const uploadProgressText = computed(() => `${uploadProgress.value}%`)
/**
* 是否在气泡尾部显示发送中loading 转圈
*
* 图片 / 视频 / 文件气泡内嵌已有进度反馈遮罩 / 进度条外层 loading 多余则抑制
* 语音气泡只有麦克风 + 时长无内嵌进度必须保留外层 loading 让用户感知正在发送
*/
const showSendingLoading = computed(
() => props.message.status === ImMessageStatus.SENDING && (!isUploading.value || isVoice.value)
)
/** 文件类型图标 + 配色:按扩展名分发,跟 ReplyPreview 共用 getFileIconInfo */
const fileIconInfo = computed(() => getFileIconInfo(filePayload.value?.name))

View File

@ -91,6 +91,8 @@ function recomputeConversationLast(conversation: Conversation): void {
conversation.lastMessageType = undefined
conversation.lastSelfSend = undefined
conversation.lastSenderDisplayName = undefined
// 排序时间也要清,否则空会话仍按旧 lastSendTime 排在前面(刷新后媒体占位被 drop 时容易踩到)
conversation.lastSendTime = 0
}
}