🐛 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 computedim
parent
459eaa5428
commit
30d695d702
|
|
@ -35,7 +35,7 @@ export interface MediaTypeContext {
|
|||
videoCoverUrl?: string
|
||||
}
|
||||
|
||||
interface MediaTypeHandler {
|
||||
export interface MediaTypeHandler {
|
||||
/** 中文名,仅日志用(替代之前散落 9 处的 kind 字符串) */
|
||||
kind: string
|
||||
/** 由 file + url + context 生成 payload;占位时 url 是 blob URL,commit 时是真实 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ function recomputeConversationLast(conversation: Conversation): void {
|
|||
conversation.lastMessageType = undefined
|
||||
conversation.lastSelfSend = undefined
|
||||
conversation.lastSenderDisplayName = undefined
|
||||
// 排序时间也要清,否则空会话仍按旧 lastSendTime 排在前面(刷新后媒体占位被 drop 时容易踩到)
|
||||
conversation.lastSendTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue