| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { getDictObj } from '#/packages/effects/hooks/src'; |
| | | import { IconifyIcon } from '#/packages/icons/src'; |
| | | import { formatFileSize, openSafeUrl } from '#/packages/utils/src'; |
| | | |
| | | import { Image } from 'ant-design-vue'; |
| | | |
| | | import { CardLineLabel } from '#/views/im/home/components/card'; |
| | | import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config'; |
| | | import { |
| | | ImContentType, |
| | | ImRtcCallEndReason, |
| | | ImRtcCallMediaType, |
| | | isFriendChatTip, |
| | | isGroupNotification, |
| | | isRtcCallTip, |
| | | } from '#/views/im/utils/constants'; |
| | | import { |
| | | buildFacePreviewText, |
| | | summarizeMessageContent, |
| | | } from '#/views/im/utils/conversation'; |
| | | import { |
| | | getFileIconInfo, |
| | | parseMessage, |
| | | parseRtcCallPayload, |
| | | resolveFriendNotificationText, |
| | | resolveGroupNotificationText, |
| | | } from '#/views/im/utils/message'; |
| | | import { formatSeconds } from '#/views/im/utils/time'; |
| | | |
| | | defineOptions({ name: 'ImManagerMessageContentPreview' }); |
| | | |
| | | const props = defineProps<{ |
| | | content?: string; |
| | | senderNickname?: string; |
| | | type?: number; |
| | | }>(); |
| | | |
| | | const payload = computed<null | Record<string, any>>(() => |
| | | parseMessage<Record<string, any>>(props.content ?? ''), |
| | | ); |
| | | |
| | | const textContent = computed(() => payload.value?.content || ''); |
| | | |
| | | const fileIconInfo = computed(() => getFileIconInfo(payload.value?.name)); |
| | | |
| | | const cardPayload = computed(() => { |
| | | if (props.type !== ImContentType.CARD || !payload.value) { |
| | | return undefined; |
| | | } |
| | | return { |
| | | ...payload.value, |
| | | name: payload.value.name || payload.value.nickname || payload.value.userId || '', |
| | | }; |
| | | }); |
| | | |
| | | const mergePreviewLines = computed(() => { |
| | | const messages = payload.value?.messages; |
| | | if (!Array.isArray(messages)) { |
| | | return []; |
| | | } |
| | | return messages |
| | | .slice(0, MESSAGE_MERGE_PREVIEW_LINES) |
| | | .map((item) => `${item.senderNickname}ï¼${summarizeMessageContent(item)}`); |
| | | }); |
| | | |
| | | const fallbackText = computed(() => { |
| | | const raw = props.content || ''; |
| | | if (!raw) { |
| | | return ''; |
| | | } |
| | | try { |
| | | const parsed = JSON.parse(raw); |
| | | if (parsed && typeof parsed === 'object' && parsed.content) { |
| | | return String(parsed.content); |
| | | } |
| | | } catch {} |
| | | return raw; |
| | | }); |
| | | |
| | | const friendTipText = computed(() => |
| | | resolveFriendNotificationText({ type: props.type }), |
| | | ); |
| | | |
| | | const groupTipText = computed(() => |
| | | resolveGroupNotificationText( |
| | | { type: props.type, content: props.content }, |
| | | (id) => `ç¨æ·(${id})`, |
| | | props.senderNickname, |
| | | ), |
| | | ); |
| | | |
| | | const rtcCallTipText = computed(() => { |
| | | const rtcPayload = parseRtcCallPayload(props.content); |
| | | if (!rtcPayload) { |
| | | return ''; |
| | | } |
| | | const mediaLabel = |
| | | rtcPayload.mediaType === ImRtcCallMediaType.VIDEO ? 'è§é¢' : 'è¯é³'; |
| | | if (props.type === ImContentType.RTC_CALL_START) { |
| | | const inviter = |
| | | rtcPayload.inviterNickname?.trim() || |
| | | `ç¨æ·(${rtcPayload.inviterUserId ?? ''})`; |
| | | return `${inviter} åèµ·äº${mediaLabel}éè¯`; |
| | | } |
| | | const segments = [`${mediaLabel}éè¯å·²ç»æ`]; |
| | | if ( |
| | | rtcPayload.endReason && |
| | | rtcPayload.endReason !== ImRtcCallEndReason.HANGUP |
| | | ) { |
| | | const reason = getDictObj( |
| | | 'im_rtc_call_end_reason', |
| | | String(rtcPayload.endReason), |
| | | )?.label; |
| | | if (reason) { |
| | | segments.push(reason); |
| | | } |
| | | } |
| | | const duration = rtcPayload.durationSeconds ?? 0; |
| | | if (duration > 0) { |
| | | segments.push(`æ¶é¿ ${formatSeconds(duration)}`); |
| | | } |
| | | return segments.join('ï¼'); |
| | | }); |
| | | |
| | | /** æå¼è§é¢ */ |
| | | function openVideo() { |
| | | openSafeUrl(payload.value?.url); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <span v-if="type === ImContentType.TEXT" class="whitespace-pre-wrap break-all"> |
| | | {{ textContent }} |
| | | </span> |
| | | |
| | | <Image |
| | | v-else-if="type === ImContentType.IMAGE && payload" |
| | | :height="60" |
| | | :src="payload.thumbnailUrl || payload.url" |
| | | :width="60" |
| | | /> |
| | | |
| | | <span |
| | | v-else-if="type === ImContentType.FILE && payload" |
| | | class="inline-flex items-center gap-1.5" |
| | | > |
| | | <IconifyIcon |
| | | :color="fileIconInfo.color" |
| | | :icon="fileIconInfo.icon" |
| | | class="size-4" |
| | | /> |
| | | <span class="max-w-52 truncate">{{ payload.name }}</span> |
| | | <span v-if="payload.size > 0" class="text-xs text-gray-400"> |
| | | {{ formatFileSize(payload.size) }} |
| | | </span> |
| | | </span> |
| | | |
| | | <span |
| | | v-else-if="type === ImContentType.VOICE && payload" |
| | | class="inline-flex items-center gap-1.5" |
| | | > |
| | | <IconifyIcon class="size-4" icon="ant-design:audio-outlined" /> |
| | | <span>{{ formatSeconds(payload.duration || 0) }}</span> |
| | | </span> |
| | | |
| | | <span |
| | | v-else-if="type === ImContentType.VIDEO && payload" |
| | | class="inline-flex items-center gap-1.5" |
| | | > |
| | | <button |
| | | v-if="payload.coverUrl" |
| | | class="relative size-[60px] overflow-hidden rounded border-0 p-0" |
| | | type="button" |
| | | @click="openVideo" |
| | | > |
| | | <img :src="payload.coverUrl" class="size-full object-cover" /> |
| | | <IconifyIcon |
| | | class="absolute inset-0 m-auto size-6 text-white drop-shadow" |
| | | icon="ant-design:play-circle-filled" |
| | | /> |
| | | </button> |
| | | <span v-else class="inline-flex items-center gap-1.5"> |
| | | <IconifyIcon class="size-4" icon="ant-design:video-camera-filled" /> |
| | | <span>[è§é¢]</span> |
| | | </span> |
| | | <span v-if="payload.duration" class="text-xs text-gray-400"> |
| | | {{ formatSeconds(payload.duration) }} |
| | | </span> |
| | | <span v-if="payload.size > 0" class="text-xs text-gray-400"> |
| | | {{ formatFileSize(payload.size) }} |
| | | </span> |
| | | </span> |
| | | |
| | | <CardLineLabel |
| | | v-else-if="type === ImContentType.CARD && cardPayload" |
| | | :card="cardPayload" |
| | | :icon-size="16" |
| | | /> |
| | | |
| | | <span |
| | | v-else-if="type === ImContentType.FACE && payload" |
| | | class="inline-flex items-center gap-1.5" |
| | | > |
| | | <img |
| | | v-if="payload.url" |
| | | :alt="payload.name || '表æ
'" |
| | | :src="payload.url" |
| | | class="size-[30px] rounded object-contain" |
| | | draggable="false" |
| | | /> |
| | | <span>{{ buildFacePreviewText(payload) }}</span> |
| | | </span> |
| | | |
| | | <span |
| | | v-else-if="type === ImContentType.MERGE && payload" |
| | | class="inline-flex flex-col gap-0.5 align-middle" |
| | | > |
| | | <span>[è天记å½] {{ payload.title }}</span> |
| | | <span |
| | | v-for="(line, index) in mergePreviewLines" |
| | | :key="index" |
| | | class="truncate text-xs text-gray-400" |
| | | > |
| | | {{ line }} |
| | | </span> |
| | | </span> |
| | | |
| | | <span v-else-if="type === ImContentType.RECALL" class="text-xs text-gray-400"> |
| | | [æ¶æ¯å·²æ¤å] |
| | | </span> |
| | | <span v-else-if="type === ImContentType.READ" class="text-xs text-gray-400"> |
| | | [å·²è¯»åæ§] |
| | | </span> |
| | | <span v-else-if="type === ImContentType.RECEIPT" class="text-xs text-gray-400"> |
| | | [åæ§] |
| | | </span> |
| | | <span v-else-if="isGroupNotification(type ?? -1)" class="text-xs text-gray-400"> |
| | | {{ groupTipText }} |
| | | </span> |
| | | <span v-else-if="isFriendChatTip(type ?? -1)" class="text-xs text-gray-400"> |
| | | {{ friendTipText }} |
| | | </span> |
| | | <span |
| | | v-else-if="isRtcCallTip(type ?? -1)" |
| | | class="inline-flex items-center gap-1.5 text-xs text-gray-400" |
| | | > |
| | | <IconifyIcon class="size-4 rotate-[135deg]" icon="ant-design:phone-outlined" /> |
| | | <span>{{ rtcCallTipText }}</span> |
| | | </span> |
| | | <span v-else class="whitespace-pre-wrap break-all">{{ fallbackText }}</span> |
| | | </template> |