✨ feat(im): 优化【消息引用】的功能,来自第一波 code review
parent
1dfab43b8a
commit
cfeee7bbb7
|
|
@ -546,6 +546,15 @@ function clearReply() {
|
||||||
draftStore.clearReply(conversation)
|
draftStore.clearReply(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取走当前 reply 快照(抓一次清一次),媒体上传链路在动手前统一调它拿 quote */
|
||||||
|
function consumeReply(): QuoteMessage | undefined {
|
||||||
|
const quote = replyTarget.value
|
||||||
|
if (quote) {
|
||||||
|
clearReply()
|
||||||
|
}
|
||||||
|
return quote
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 表情 ====================
|
// ==================== 表情 ====================
|
||||||
const emojiVisible = ref(false)
|
const emojiVisible = ref(false)
|
||||||
/** 切换表情面板;打开时互斥关掉语音面板 */
|
/** 切换表情面板;打开时互斥关掉语音面板 */
|
||||||
|
|
@ -767,8 +776,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||||
// ==================== 图片 / 文件上传 ====================
|
// ==================== 图片 / 文件上传 ====================
|
||||||
/** 上传并发送 IMAGE 消息;quote 抓取后立即清 draft.reply 让顶部引用条同步消失 */
|
/** 上传并发送 IMAGE 消息;quote 抓取后立即清 draft.reply 让顶部引用条同步消失 */
|
||||||
async function uploadAndSendImage(file: File) {
|
async function uploadAndSendImage(file: File) {
|
||||||
const replyQuote = replyTarget.value
|
const replyQuote = consumeReply()
|
||||||
clearReply()
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
const url = ((await updateFile(form)) as { data?: string })?.data
|
const url = ((await updateFile(form)) as { data?: string })?.data
|
||||||
|
|
@ -781,8 +789,7 @@ async function uploadAndSendImage(file: File) {
|
||||||
|
|
||||||
/** 上传并发送 FILE 消息;附原始 name / size 让接收端展示文件名和体积 */
|
/** 上传并发送 FILE 消息;附原始 name / size 让接收端展示文件名和体积 */
|
||||||
async function uploadAndSendFile(file: File) {
|
async function uploadAndSendFile(file: File) {
|
||||||
const replyQuote = replyTarget.value
|
const replyQuote = consumeReply()
|
||||||
clearReply()
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
const url = ((await updateFile(form)) as { data?: string })?.data
|
const url = ((await updateFile(form)) as { data?: string })?.data
|
||||||
|
|
@ -825,8 +832,7 @@ function openVoice() {
|
||||||
}
|
}
|
||||||
/** VoiceRecorder 录完后回传 blob,包成 webm 文件上传,发送 VOICE 消息 */
|
/** VoiceRecorder 录完后回传 blob,包成 webm 文件上传,发送 VOICE 消息 */
|
||||||
async function onVoiceSend(payload: { blob: Blob; duration: number }) {
|
async function onVoiceSend(payload: { blob: Blob; duration: number }) {
|
||||||
const replyQuote = replyTarget.value
|
const replyQuote = consumeReply()
|
||||||
clearReply()
|
|
||||||
const file = new File([payload.blob], `voice-${Date.now()}.webm`, { type: payload.blob.type })
|
const file = new File([payload.blob], `voice-${Date.now()}.webm`, { type: payload.blob.type })
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
|
|
@ -955,8 +961,7 @@ async function uploadAndSendVideo(file: File) {
|
||||||
}
|
}
|
||||||
const startKey = getConversationKey(startConversation)
|
const startKey = getConversationKey(startConversation)
|
||||||
// 1.2 quote 抓取后立即清 draft.reply,与图片 / 文件 / 语音上传链路一致
|
// 1.2 quote 抓取后立即清 draft.reply,与图片 / 文件 / 语音上传链路一致
|
||||||
const replyQuote = replyTarget.value
|
const replyQuote = consumeReply()
|
||||||
clearReply()
|
|
||||||
|
|
||||||
// 2. 三路并行起跑(probe 与两条上传无依赖,封面上传等 probe 出 cover 后立即接力)
|
// 2. 三路并行起跑(probe 与两条上传无依赖,封面上传等 probe 出 cover 后立即接力)
|
||||||
// 2.1 视频本体上传:立即 catch 兜底为 url=undefined,由 step 3.2 拿不到 url 时放弃;同时让 promise 不再 floating
|
// 2.1 视频本体上传:立即 catch 兜底为 url=undefined,由 step 3.2 拿不到 url 时放弃;同时让 promise 不再 floating
|
||||||
|
|
|
||||||
|
|
@ -535,7 +535,7 @@ const isAtMe = computed(() => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 右键菜单项:
|
* 右键菜单项:
|
||||||
* - 回复:仅已落库(id≠0)且未撤回的消息可引用,引用块写入 draftStore.reply
|
* - 回复:仅已落库(id≠0)且未撤回的消息可引用,引用块写入 draftStore.reply
|
||||||
* - 删除:从本地消息列表移除(不动后端)
|
* - 删除:从本地消息列表移除(不动后端)
|
||||||
* - 撤回:仅自己发送、已送达(有 id)的消息
|
* - 撤回:仅自己发送、已送达(有 id)的消息
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<!--
|
<!--
|
||||||
引用消息预览块,对齐微信 PC:浅灰底块 + 大 padding + 文本可换行(line-clamp 2 行)
|
引用消息预览块,对齐微信 PC:浅灰底块 + 大 padding + 文本可换行(line-clamp 2 行)
|
||||||
- clickable=true(气泡内): 点击触发 locate emit;撤回态禁用跳转
|
- clickable=true(气泡内):点击触发 locate emit;撤回态禁用跳转
|
||||||
- closable=true(输入条): 显示右上 × 圆形按钮,hover 时显示圆形底
|
- closable=true(输入条):显示右上 × 圆形按钮,hover 时显示圆形底
|
||||||
- 撤回降级:命中本地缓存且 type === RECALL 时显示「原消息已撤回」斜体灰字
|
- 撤回降级:命中本地缓存且 type === RECALL 时显示「原消息已撤回」斜体灰字
|
||||||
- 富预览:type 为 IMAGE / VIDEO 时直接从 quote.content 取缩略图,不依赖本地缓存
|
- 富预览:type 为 IMAGE / VIDEO 时直接从 quote.content 取缩略图,不依赖本地缓存
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class="im-reply-preview flex gap-2 items-start min-w-0 px-3 py-2 rounded text-13px bg-[var(--el-fill-color-light)]"
|
class="im-reply-preview flex gap-2 items-start min-w-0 px-3 py-2 rounded text-13px bg-[var(--el-fill-color-light)]"
|
||||||
|
|
@ -101,25 +101,29 @@ const senderName = computed(() => {
|
||||||
return getSenderDisplayName(props.quote.senderId, conversation.type, conversation.targetId)
|
return getSenderDisplayName(props.quote.senderId, conversation.type, conversation.targetId)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 摘要文案:已撤回降级,否则按 type 从 quote.content 派生(文本截断 / 非文本走类型 tag) */
|
/** quote.content 解析一次缓存,让 snippetText / thumbnailUrl 复用,长会话每条引用气泡少一次 JSON.parse */
|
||||||
|
type AnyQuotePayload = Partial<TextMessage & ImageMessage & FileMessage & AudioMessage & VideoMessage>
|
||||||
|
const parsedPayload = computed(() => parseMessage<AnyQuotePayload>(props.quote.content))
|
||||||
|
|
||||||
|
/** 摘要文案:已撤回降级,否则按 type 从 quote.content 派生(文本截断 / 非文本走类型 tag) */
|
||||||
const snippetText = computed(() => {
|
const snippetText = computed(() => {
|
||||||
if (isRecalled.value) {
|
if (isRecalled.value) {
|
||||||
return '原消息已撤回'
|
return '原消息已撤回'
|
||||||
}
|
}
|
||||||
const { type, content } = props.quote
|
const { type } = props.quote
|
||||||
if (type === ImMessageType.TEXT) {
|
if (type === ImMessageType.TEXT) {
|
||||||
const text = parseMessage<TextMessage>(content)?.content ?? ''
|
const text = parsedPayload.value?.content ?? ''
|
||||||
return text.length <= MAX_TEXT_PREVIEW_LEN ? text : `${text.substring(0, MAX_TEXT_PREVIEW_LEN)}…`
|
return text.length <= MAX_TEXT_PREVIEW_LEN ? text : `${text.substring(0, MAX_TEXT_PREVIEW_LEN)}…`
|
||||||
}
|
}
|
||||||
if (type === ImMessageType.IMAGE) {
|
if (type === ImMessageType.IMAGE) {
|
||||||
return '[图片]'
|
return '[图片]'
|
||||||
}
|
}
|
||||||
if (type === ImMessageType.FILE) {
|
if (type === ImMessageType.FILE) {
|
||||||
const name = parseMessage<FileMessage>(content)?.name
|
const name = parsedPayload.value?.name
|
||||||
return name ? `[文件 ${name}]` : '[文件]'
|
return name ? `[文件 ${name}]` : '[文件]'
|
||||||
}
|
}
|
||||||
if (type === ImMessageType.VOICE) {
|
if (type === ImMessageType.VOICE) {
|
||||||
const duration = parseMessage<AudioMessage>(content)?.duration
|
const duration = parsedPayload.value?.duration
|
||||||
return duration ? `[语音 ${duration}″]` : '[语音]'
|
return duration ? `[语音 ${duration}″]` : '[语音]'
|
||||||
}
|
}
|
||||||
if (type === ImMessageType.VIDEO) {
|
if (type === ImMessageType.VIDEO) {
|
||||||
|
|
@ -128,18 +132,17 @@ const snippetText = computed(() => {
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 缩略图 URL:仅图片 / 视频从 quote.content 直接取,不依赖本地缓存 */
|
/** 缩略图 URL:仅图片 / 视频从 quote.content 直接取,不依赖本地缓存 */
|
||||||
const thumbnailUrl = computed<string | undefined>(() => {
|
const thumbnailUrl = computed<string | undefined>(() => {
|
||||||
if (isRecalled.value) {
|
if (isRecalled.value) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const { type, content } = props.quote
|
const { type } = props.quote
|
||||||
if (type === ImMessageType.IMAGE) {
|
if (type === ImMessageType.IMAGE) {
|
||||||
const payload = parseMessage<ImageMessage>(content)
|
return parsedPayload.value?.thumbnailUrl || parsedPayload.value?.url
|
||||||
return payload?.thumbnailUrl || payload?.url
|
|
||||||
}
|
}
|
||||||
if (type === ImMessageType.VIDEO) {
|
if (type === ImMessageType.VIDEO) {
|
||||||
return parseMessage<VideoMessage>(content)?.coverUrl
|
return parsedPayload.value?.coverUrl
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue