feat(im): 初始化消息转发 v0.2:第二次优化部分代码(一些工具类等)

im
YunaiV 2026-05-07 20:34:09 +08:00
parent cf01143632
commit 82d065c270
4 changed files with 71 additions and 93 deletions

View File

@ -215,21 +215,27 @@ const isUploading = computed(() => props.uploadProgress != null)
const uploadProgress = computed(() => props.uploadProgress ?? 0)
const uploadProgressText = computed(() => `${uploadProgress.value}%`)
/** 各 payload */
const textPayload = computed(() => (isText.value ? parseMessage<TextMessage>(props.content) : null))
/**
* 单一 parse 入口content 一变只 parse 一次 type 分发到下面 7 payload
*
* 各类型 payload 共用同一棵 JSON 避免 7 computed 各自重 parse 同一份 content
*/
const parsedContent = computed<unknown>(() => parseMessage(props.content))
const textPayload = computed(() => (isText.value ? (parsedContent.value as TextMessage | null) : null))
const imagePayload = computed(() =>
isImage.value ? parseMessage<ImageMessage>(props.content) : null
isImage.value ? (parsedContent.value as ImageMessage | null) : null
)
const filePayload = computed(() => (isFile.value ? parseMessage<FileMessage>(props.content) : null))
const filePayload = computed(() => (isFile.value ? (parsedContent.value as FileMessage | null) : null))
const voicePayload = computed(() =>
isVoice.value ? parseMessage<AudioMessage>(props.content) : null
isVoice.value ? (parsedContent.value as AudioMessage | null) : null
)
const videoPayload = computed(() =>
isVideo.value ? parseMessage<VideoMessage>(props.content) : null
isVideo.value ? (parsedContent.value as VideoMessage | null) : null
)
const cardPayload = computed(() => (isCard.value ? parseMessage<CardMessage>(props.content) : null))
const cardPayload = computed(() => (isCard.value ? (parsedContent.value as CardMessage | null) : null))
const mergePayload = computed(() =>
isMerge.value ? parseMessage<MergeMessage>(props.content) : null
isMerge.value ? (parsedContent.value as MergeMessage | null) : null
)
/** 合并消息内嵌前 N 条派生「{昵称}{摘要}」 */
@ -248,7 +254,7 @@ const facePayload = computed(() => {
if (!isFace.value) {
return null
}
const raw = parseMessage<FaceMessage>(props.content)
const raw = parsedContent.value as FaceMessage | null
if (!raw) {
return null
}
@ -324,3 +330,50 @@ onBeforeUnmount(() => {
voicePlaying.value = false
})
</script>
<style scoped>
/*
border 4 边色画三角透明 3 + 实色 1 省一张图片颜色与气泡背景对应 1px 视觉吃进去 */
.message-bubble--other::before,
.message-bubble--self::before {
content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
border-style: solid;
}
.message-bubble--other::before {
left: -5px;
border-width: 5px 6px 5px 0;
border-color: transparent var(--el-fill-color-light) transparent transparent;
}
.message-bubble--self::before {
right: -5px;
border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
}
/* el-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
/* 播放中的脉冲动画 */
.im-voice-playing {
animation: im-voice-icon-pulse 0.8s infinite;
}
@keyframes im-voice-icon-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
}
</style>

View File

@ -861,55 +861,7 @@ function handleDelete() {
</script>
<style scoped>
/*
- border 4 边色画三角透明 3 + 实色 1 省一张图片
- 颜色对应气泡背景 1px 视觉吃进去UnoCSS 写不顺手索性用 scoped CSS */
.message-bubble--other::before,
.message-bubble--self::before {
content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
border-style: solid;
}
.message-bubble--other::before {
left: -5px;
border-width: 5px 6px 5px 0;
border-color: transparent var(--el-fill-color-light) transparent transparent;
}
.message-bubble--self::before {
right: -5px;
border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
}
/* el-icon color .el-icon{color:var(--color)}
这里把 voice 图标的 fill 锁死避免字体色跟随主题变白
file 图标已迁到 Iconify 按扩展名走彩色不在这里强制 */
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
/* 播放中的脉冲动画keyframes 用 UnoCSS 不好写,保留 scoped */
.im-voice-playing {
animation: im-voice-icon-pulse 0.8s infinite;
}
@keyframes im-voice-icon-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
}
/* SENDING 状态的转圈动画el-icon 自带 .is-loading 旋转,迁到 Iconify 后丢了,自己补一份 */
/* SENDING 状态的转圈动画 */
.im-loading-spin {
animation: im-loading-spin 1s linear infinite;
}

View File

@ -228,9 +228,12 @@ provide(IM_MERGE_DETAIL_DIALOG_KEY, (content) => mergeDetailDialogRef.value?.ope
const multiSelect = useMessageMultiSelect()
/** 切会话退出多选;避免上一会话的勾选状态泄漏到新会话 */
/** 切会话退出多选;避免上一会话的勾选状态泄漏到新会话type+targetId 一起监听,私聊与群聊 id 同号时也能触发) */
watch(
() => conversationStore.activeConversation?.targetId,
() => [
conversationStore.activeConversation?.type,
conversationStore.activeConversation?.targetId
],
() => multiSelect.exit()
)

View File

@ -137,6 +137,7 @@ import {
import CardLineLabel from '@/views/im/home/components/card/CardLineLabel.vue'
import {
parseMessage,
getFileIconInfo,
type ImageMessage,
type FileMessage,
type AudioMessage,
@ -218,39 +219,8 @@ function openVideo() {
}
}
/** 文件图标:按扩展名分配 icon + 颜色,对齐 home 端 MessageItem 的观感 */
const fileIconInfo = computed<{ icon: string; color: string }>(() => {
const name = filePayload.value?.name || ''
const ext = name.split('.').pop()?.toLowerCase() || ''
if (ext === 'pdf') {
return { icon: 'ant-design:file-pdf-filled', color: '#ed5757' }
}
if (['doc', 'docx'].includes(ext)) {
return { icon: 'ant-design:file-word-filled', color: '#2b7cd3' }
}
if (['xls', 'xlsx'].includes(ext)) {
return { icon: 'ant-design:file-excel-filled', color: '#1f7244' }
}
if (['ppt', 'pptx'].includes(ext)) {
return { icon: 'ant-design:file-ppt-filled', color: '#d24726' }
}
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
return { icon: 'ant-design:file-zip-filled', color: '#f0ad4e' }
}
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
return { icon: 'ant-design:file-image-filled', color: '#9c27b0' }
}
if (['mp4', 'mov', 'avi', 'mkv', 'wmv', 'flv'].includes(ext)) {
return { icon: 'ant-design:video-camera-filled', color: '#9c27b0' }
}
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) {
return { icon: 'ant-design:audio-filled', color: '#9c27b0' }
}
if (['txt', 'md', 'log', 'json', 'xml'].includes(ext)) {
return { icon: 'ant-design:file-text-filled', color: '#909399' }
}
return { icon: 'ant-design:file-filled', color: '#909399' }
})
/** 文件图标:按扩展名分配 icon + 颜色 */
const fileIconInfo = computed(() => getFileIconInfo(filePayload.value?.name))
/** 系统事件 / 未知类型 fallback取 JSON 首层 content否则原文 */
const fallbackText = computed(() => {