| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue' |
| | | |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | import { formatFileSize } from '#/packages/utils/src' |
| | | |
| | | import { CardLineLabel } from '#/views/im/home/components/card' |
| | | import { ImContentType } from '#/views/im/utils/constants' |
| | | import { getClientConversationId } from '#/views/im/utils/db' |
| | | import { |
| | | type AudioMessage, |
| | | type CardMessage, |
| | | type FaceMessage, |
| | | type FileMessage, |
| | | getFileIconInfo, |
| | | type ImageMessage, |
| | | type MaterialMessage, |
| | | parseMessage, |
| | | type QuoteMessage, |
| | | type TextMessage, |
| | | type VideoMessage |
| | | } from '#/views/im/utils/message' |
| | | import { formatSeconds } from '#/views/im/utils/time' |
| | | import { getSenderDisplayName } from '#/views/im/utils/user' |
| | | |
| | | import { useConversationStore } from '../../../../store/conversationStore' |
| | | import { useMessageStore } from '../../../../store/messageStore' |
| | | |
| | | defineOptions({ name: 'ImReplyPreview' }) |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | /** æ°æ³¡å
为 true æ¯æç¹å»è·³è½¬,è¾å
¥æ¡ä¸º false */ |
| | | clickable?: boolean |
| | | /** è¾å
¥æ¡ä¸º true æ¾ç¤º à å
³éæé® */ |
| | | closable?: boolean |
| | | /** èªå·±åéçæ°æ³¡ä¸º trueï¼æç«çº¿éåå°å³ä¾§ï¼ä¸æ°æ³¡åä¾§ */ |
| | | mirrored?: boolean |
| | | quote: QuoteMessage |
| | | }>(), |
| | | { |
| | | clickable: false, |
| | | closable: false, |
| | | mirrored: false |
| | | } |
| | | ) |
| | | |
| | | const emit = defineEmits<{ |
| | | close: [] |
| | | locate: [messageId: number] |
| | | }>() |
| | | |
| | | const MAX_TEXT_PREVIEW_LEN = 60 // ææ¬æè¦å¨å¼ç¨åéå±ç¤ºçæå¤§åç¬¦æ° |
| | | |
| | | const conversationStore = useConversationStore() |
| | | const messageStore = useMessageStore() |
| | | |
| | | /** å¨å½åä¼è¯æ¶æ¯åè¡¨éæ¥æ¾åæ¶æ¯,ä»
ç¨äºå®æ¶å¤ææ¯å¦å·²æ¤å;æè¦ / 缩ç¥å¾é½ä» quote.content ç´æ¥æ´¾ç */ |
| | | const liveMessage = computed(() => { |
| | | const conversation = conversationStore.activeConversation |
| | | if (!conversation || !props.quote.messageId) { |
| | | return undefined |
| | | } |
| | | return messageStore |
| | | .getMessages(getClientConversationId(conversation.type, conversation.targetId)) |
| | | .find((message) => message.id === props.quote.messageId) |
| | | }) |
| | | |
| | | /** å½ä¸æ¬å°ç¼åä¸ type === RECALL æå¤å®ä¸ºå·²æ¤å;ä¸å¨ç¼åçå½å¿«ç
§ä»ææ */ |
| | | const isRecalled = computed(() => liveMessage.value?.type === ImContentType.RECALL) |
| | | |
| | | /** æ¸²ææ¶å®æ¶ç®,䏿°æ³¡ä¸æ¹æ¾ç¤ºåèµ°åä¸å¥è§å,é¿å
夿³¨åæ´åå¼ç¨åéæ§ */ |
| | | const senderName = computed(() => { |
| | | const conversation = conversationStore.activeConversation |
| | | if (!conversation) { |
| | | return '' |
| | | } |
| | | return getSenderDisplayName(props.quote.senderId, conversation.type, conversation.targetId) |
| | | }) |
| | | |
| | | /** quote.content è§£æä¸æ¬¡ç¼åï¼è®©å¤ä¸ª computed å¤ç¨ï¼é¿ä¼è¯æ¯æ¡å¼ç¨æ°æ³¡å°ä¸æ¬¡ JSON.parse */ |
| | | type AnyQuotePayload = Partial< |
| | | AudioMessage & |
| | | CardMessage & |
| | | FaceMessage & |
| | | FileMessage & |
| | | ImageMessage & |
| | | MaterialMessage & |
| | | TextMessage & |
| | | VideoMessage |
| | | > |
| | | const parsedPayload = computed(() => parseMessage<AnyQuotePayload>(props.quote.content)) |
| | | |
| | | const isText = computed(() => props.quote.type === ImContentType.TEXT) |
| | | const isFile = computed(() => props.quote.type === ImContentType.FILE) |
| | | const isVoice = computed(() => props.quote.type === ImContentType.VOICE) |
| | | const isCard = computed(() => props.quote.type === ImContentType.CARD) |
| | | const isFace = computed(() => props.quote.type === ImContentType.FACE) |
| | | const isMaterial = computed(() => props.quote.type === ImContentType.MATERIAL) |
| | | |
| | | /** ææ¬è¶
è¿ MAX_TEXT_PREVIEW_LEN æªæï¼é¿å
容䏿çå¼ç¨å */ |
| | | const textPreview = computed(() => { |
| | | const text = parsedPayload.value?.content ?? '' |
| | | return text.length <= MAX_TEXT_PREVIEW_LEN ? text : `${text.slice(0, Math.max(0, MAX_TEXT_PREVIEW_LEN))}â¦` |
| | | }) |
| | | |
| | | /** æä»¶ iconï¼ææ©å±åæè²ï¼è·ä¸»æ°æ³¡æ¸²æåæº */ |
| | | const fileIcon = computed(() => getFileIconInfo(parsedPayload.value?.name)) |
| | | |
| | | /** 缩ç¥å¾ URLï¼å¾ç / è§é¢ / 表æ
è´´å¾ / é¢éç´ æå°é¢ä» quote.content ç´æ¥åï¼ä¸ä¾èµæ¬å°ç¼å */ |
| | | const thumbnailUrl = computed<string | undefined>(() => { |
| | | if (isRecalled.value) { |
| | | return undefined |
| | | } |
| | | const { type } = props.quote |
| | | if (type === ImContentType.IMAGE) { |
| | | return parsedPayload.value?.thumbnailUrl || parsedPayload.value?.url |
| | | } |
| | | if (type === ImContentType.VIDEO || type === ImContentType.MATERIAL) { |
| | | return parsedPayload.value?.coverUrl |
| | | } |
| | | if (type === ImContentType.FACE) { |
| | | return parsedPayload.value?.url |
| | | } |
| | | return undefined |
| | | }) |
| | | |
| | | /** ä»
clickable 䏿ªæ¤åæ¶è§¦å跳转 */ |
| | | function onClick() { |
| | | if (!props.clickable || isRecalled.value) { |
| | | return |
| | | } |
| | | emit('locate', props.quote.messageId) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | å¼ç¨é¢è§åï¼å¯¹é½å¾®ä¿¡ PCï¼ï¼2px ç«çº¿ä½ä¸ºãå¼ç¨ãè§è§æ è¯ + ç´§åå
容é¢è§ |
| | | - clickable=trueï¼æ°æ³¡å
ï¼ï¼ç¹å»è§¦å locate emitï¼æ¤åæç¦ç¨è·³è½¬ |
| | | - closable=trueï¼è¾å
¥æ¡ï¼ï¼å°¾é¨ à å
³éæé® |
| | | - mirrored=trueï¼èªå·±åéçæ°æ³¡ï¼ï¼ç«çº¿éåå°å³ä¾§ï¼ä¸æ°æ³¡åä¾§ |
| | | - å
容é¢è§ï¼ä¸ä¸»æ°æ³¡åæºï¼ä¸ç¼©åæã[æä»¶]/[è¯é³]ãï¼ï¼ |
| | | - ææ¬ï¼æªæå纯æå |
| | | - å¾ç / è§é¢ï¼ç¼©ç¥å¾ï¼IMAGE ç¨ thumbnailUrl/urlï¼VIDEO ç¨ coverUrlï¼ |
| | | - æä»¶ï¼file icon + æä»¶å + å¤§å° |
| | | - è¯é³ï¼audio icon + æ¶é¿ |
| | | - æ¤åï¼å½ä¸æ¬å°ç¼åä¸ type === RECALLï¼ï¼ãåæ¶æ¯å·²æ¤åãæä½ç°å |
| | | --> |
| | | <div |
| | | class="flex w-fit gap-1.5 items-center min-w-0 py-0.5 text-12px text-[var(--ant-color-text-secondary)] rounded transition-colors" |
| | | :class="[ |
| | | mirrored |
| | | ? 'pl-1 pr-2 border-r-2 border-r-solid border-r-[var(--ant-color-border)]' |
| | | : 'pl-2 pr-1 border-l-2 border-l-solid border-l-[var(--ant-color-border)]', |
| | | { |
| | | 'cursor-pointer hover:text-[var(--ant-color-text)]': clickable && !isRecalled, |
| | | 'hover:bg-[var(--ant-color-fill-secondary)]': (clickable && !isRecalled) || closable |
| | | } |
| | | ]" |
| | | @click="onClick" |
| | | > |
| | | <span class="flex-shrink-0">{{ senderName }}:</span> |
| | | |
| | | <!-- æ¤åé级 --> |
| | | <span v-if="isRecalled" class="italic">åæ¶æ¯å·²æ¤å</span> |
| | | |
| | | <!-- ææ¬ --> |
| | | <span v-else-if="isText" class="min-w-0 line-clamp-2 break-words">{{ textPreview }}</span> |
| | | |
| | | <!-- æä»¶ï¼icon + æä»¶å + å¤§å° --> |
| | | <template v-else-if="isFile"> |
| | | <Icon :icon="fileIcon.icon" :color="fileIcon.color" :size="14" class="flex-shrink-0" /> |
| | | <span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words"> |
| | | {{ parsedPayload.name }} |
| | | </span> |
| | | <span |
| | | v-if="parsedPayload?.size" |
| | | class="flex-shrink-0 text-[var(--ant-color-text-placeholder)]" |
| | | > |
| | | {{ formatFileSize(parsedPayload.size) }} |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- è¯é³ï¼audio icon + æ¶é¿ --> |
| | | <template v-else-if="isVoice"> |
| | | <Icon icon="ant-design:audio-outlined" :size="14" class="flex-shrink-0" /> |
| | | <span v-if="parsedPayload?.duration" class="flex-shrink-0"> |
| | | {{ formatSeconds(parsedPayload.duration) }} |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- åç --> |
| | | <CardLineLabel |
| | | v-else-if="isCard" |
| | | :card="parsedPayload" |
| | | class="min-w-0 line-clamp-2 break-words" |
| | | /> |
| | | |
| | | <!-- 表æ
è´´å¾ï¼ç¼©ç¥å¾ + nameï¼æ name ä»
æ¾ç¤º [表æ
]ï¼ --> |
| | | <template v-else-if="isFace"> |
| | | <span class="flex-shrink-0">[表æ
]</span> |
| | | <span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words"> |
| | | {{ parsedPayload.name }} |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- é¢éç´ æï¼[é¢é] + æ é¢ + å°é¢ç¼©ç¥å¾ --> |
| | | <template v-else-if="isMaterial"> |
| | | <span class="flex-shrink-0">[é¢é]</span> |
| | | <span v-if="parsedPayload?.title" class="min-w-0 line-clamp-2 break-words"> |
| | | {{ parsedPayload.title }} |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- å¾ç / è§é¢ / 表æ
è´´å¾ / é¢éç´ æç¼©ç¥å¾ --> |
| | | <img |
| | | v-if="thumbnailUrl" |
| | | :src="thumbnailUrl" |
| | | class="flex-shrink-0 object-cover w-6 h-6 rounded" |
| | | alt="" |
| | | /> |
| | | |
| | | <!-- å
³éæé® --> |
| | | <button |
| | | v-if="closable" |
| | | type="button" |
| | | class="flex-shrink-0 inline-flex items-center justify-center w-4 h-4 cursor-pointer rounded-full bg-transparent border-none text-[var(--ant-color-text-secondary)] hover:bg-[var(--ant-color-fill)] hover:text-[var(--ant-color-text)]" |
| | | @click.stop="emit('close')" |
| | | > |
| | | <Icon icon="ant-design:close-outlined" :size="10" /> |
| | | </button> |
| | | </div> |
| | | </template> |