| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { Conversation } from '../../../../types' |
| | | |
| | | import { computed } from 'vue' |
| | | |
| | | import { confirm } from '#/packages/effects/common-ui/src' |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { Tag } from 'ant-design-vue' |
| | | |
| | | import { buildRecallTip } from '#/views/im/utils/conversation' |
| | | import { formatConversationTime } from '#/views/im/utils/time' |
| | | import { getSenderDisplayName } from '#/views/im/utils/user' |
| | | |
| | | import { ImContentType, ImConversationType, isNormalMessage } from '../../../../../utils/constants' |
| | | import { GroupAvatar } from '../../../../components/group' |
| | | import { UserAvatar } from '../../../../components/user' |
| | | import { useConversationStore } from '../../../../store/conversationStore' |
| | | import { useFriendStore } from '../../../../store/friendStore' |
| | | import { useGroupRequestStore } from '../../../../store/groupRequestStore' |
| | | import { useGroupStore } from '../../../../store/groupStore' |
| | | import { useImUiStore } from '../../../../store/uiStore' |
| | | |
| | | defineOptions({ name: 'ImConversationItem' }) |
| | | |
| | | // å¨ä¸æåï¼dayjs ç day() è¿å 0-6ï¼0=卿¥ï¼ï¼é¡¹ç®æ²¡å
¨å±è£
dayjs/locale/zh-cnï¼æ¬å°æ å°é¿å
å¼å¯ä½ç¨ |
| | | const props = defineProps<{ |
| | | conversation: Conversation |
| | | }>() |
| | | |
| | | const conversationStore = useConversationStore() |
| | | const friendStore = useFriendStore() |
| | | const groupStore = useGroupStore() |
| | | const groupRequestStore = useGroupRequestStore() |
| | | const uiStore = useImUiStore() |
| | | |
| | | const isActive = computed( |
| | | () => |
| | | conversationStore.activeConversation?.targetId === props.conversation.targetId && |
| | | conversationStore.activeConversation?.type === props.conversation.type |
| | | ) |
| | | |
| | | /** |
| | | * å½åä¼è¯çè稿快ç
§ï¼å卿¶å表æ¾ç¤º [è稿] åç¼ + ææ¬ï¼çæ sender åç¼å @æ 红å |
| | | * å½åæå¼çä¼è¯ä¸å±ç¤ºè稿ï¼è¾å
¥å
容已ç»å¨ç¼è¾å¨éå¯è§ï¼åè¡¨åæ¾ç¤º [è稿] åèåä½ |
| | | */ |
| | | const draft = computed(() => { |
| | | if (isActive.value) { |
| | | return undefined |
| | | } |
| | | return conversationStore.getConversationDraft(props.conversation) |
| | | }) |
| | | |
| | | const isGroup = computed(() => props.conversation.type === ImConversationType.GROUP) |
| | | |
| | | /** æå䏿¡æ¶æ¯åéè
çå±ç¤ºåï¼å®æ¶ç® + å¿«ç
§ fallbackï¼getSenderDisplayName ç®ä¸åºæ¶å
åºï¼ */ |
| | | const lastSenderDisplayName = computed(() => { |
| | | const senderId = props.conversation.lastSenderId |
| | | if (!senderId) { |
| | | return '' |
| | | } |
| | | return getSenderDisplayName( |
| | | senderId, |
| | | props.conversation.type, |
| | | props.conversation.targetId, |
| | | props.conversation.lastSenderDisplayName |
| | | ) |
| | | }) |
| | | |
| | | /** 群è + ææååéè
+ æå䏿¡æ¯æ®éæ¶æ¯æ¶ï¼æ¾ç¤ºåéè
åç¼ï¼FRIEND_* / GROUP_* / RECALL / è稿æä¸å¸¦åç¼ï¼ */ |
| | | const showSendName = computed(() => { |
| | | if (draft.value) { |
| | | return false |
| | | } |
| | | if (!isGroup.value) { |
| | | return false |
| | | } |
| | | if (!props.conversation.lastSenderId) { |
| | | return false |
| | | } |
| | | const lastType = props.conversation.lastMessageType |
| | | return lastType != null && isNormalMessage(lastType) |
| | | }) |
| | | |
| | | /** å表å±ç¤ºææ¡ï¼è稿ä¼å
ï¼å¯¹é½å¾®ä¿¡ PCï¼æè稿æ¶çææå䏿¡é¢è§ï¼â æ¤å宿¶ç® â lastContent å
åº */ |
| | | const lastContentDisplay = computed(() => { |
| | | if (draft.value) { |
| | | return draft.value.plain |
| | | } |
| | | if ( |
| | | props.conversation.lastMessageType === ImContentType.RECALL && |
| | | props.conversation.lastSenderId != null |
| | | ) { |
| | | return buildRecallTip( |
| | | props.conversation.lastSenderId, |
| | | !!props.conversation.lastSelfSend, |
| | | props.conversation.type, |
| | | props.conversation.targetId, |
| | | props.conversation.lastSenderDisplayName |
| | | ) |
| | | } |
| | | return props.conversation.lastContent |
| | | }) |
| | | |
| | | /** ä¼è¯å表 "[è稿]" / "@ æ" / "@ å
¨ä½æå" 红åæç¤ºï¼è稿ä¼å
ï¼å¯¹é½å¾®ä¿¡ PCï¼ */ |
| | | const atText = computed(() => { |
| | | if (draft.value) { |
| | | return '[è稿]' |
| | | } |
| | | if (props.conversation.atMe) { |
| | | return '[æäºº@æ]' |
| | | } |
| | | if (props.conversation.atAll) { |
| | | return '[@å
¨ä½æå]' |
| | | } |
| | | return '' |
| | | }) |
| | | |
| | | /** å
ææ°ä¼è¯æªè¯»æ¡æ°ææ¡ */ |
| | | const mutedUnreadText = computed(() => { |
| | | if (!props.conversation.silent || props.conversation.unreadCount <= 0) { |
| | | return '' |
| | | } |
| | | const count = props.conversation.unreadCount > 99 ? '99+' : props.conversation.unreadCount |
| | | return `[${count}æ¡]` |
| | | }) |
| | | |
| | | /** ç¾¤èæªå¤çå 群ç³è¯·çº¢ååç¼ï¼store å·²ç»æãæç®¡çç群ãè¿æ»¤è¿ï¼count > 0 å³å¯æ¾ç¤º */ |
| | | const requestText = computed(() => { |
| | | if (!isGroup.value) { |
| | | return '' |
| | | } |
| | | const count = groupRequestStore.getUnhandledGroupRequestCountMap.get(props.conversation.targetId) ?? 0 |
| | | return count > 0 ? `[${count}æ¡è¿ç¾¤ç³è¯·]` : '' |
| | | }) |
| | | |
| | | /** ç¹å»åä¼è¯ */ |
| | | function handleClick() { |
| | | conversationStore.setActiveConversation(props.conversation) |
| | | } |
| | | |
| | | /** åæ¢ç½®é¡¶ */ |
| | | function handleTop() { |
| | | conversationStore.setConversationTop( |
| | | props.conversation.type, |
| | | props.conversation.targetId, |
| | | !props.conversation.top |
| | | ) |
| | | } |
| | | |
| | | /** 忢å
ææ°ï¼ä¹è§ UIï¼å
æ¬å°åæ¢ï¼èåç«å³å
³ï¼åç«¯å¤±è´¥åæ» conversation ç¶æï¼ */ |
| | | function handleMuted() { |
| | | const next = !props.conversation.silent |
| | | const { type, targetId } = props.conversation |
| | | conversationStore.setConversationSilent(type, targetId, next) |
| | | const sync = |
| | | type === ImConversationType.PRIVATE |
| | | ? friendStore.setFriendSilent(targetId, next) |
| | | : groupStore.setGroupSilent(targetId, next) |
| | | sync.catch((error) => { |
| | | console.error('[IM] 忢å
ææ°å¤±è´¥', error) |
| | | conversationStore.setConversationSilent(type, targetId, !next) |
| | | }) |
| | | } |
| | | |
| | | /** å é¤ä¼è¯ï¼äºæ¬¡ç¡®è®¤å软å */ |
| | | async function handleDelete() { |
| | | try { |
| | | await confirm(`ç¡®å®å é¤ä¸ã${props.conversation.name}ãçä¼è¯åï¼`, 'å é¤ä¼è¯') |
| | | conversationStore.removeConversation(props.conversation.type, props.conversation.targetId) |
| | | } catch {} |
| | | } |
| | | |
| | | /** å³é®èåï¼ç½®é¡¶ / å
ææ° / å é¤ */ |
| | | function handleContextMenu(e: MouseEvent) { |
| | | uiStore.openContextMenu( |
| | | { x: e.clientX, y: e.clientY }, |
| | | [ |
| | | { key: 'TOP', name: props.conversation.top ? 'åæ¶ç½®é¡¶' : '置顶' }, |
| | | { key: 'MUTED', name: props.conversation.silent ? 'å
è®¸æ¶æ¯éç¥' : 'æ¶æ¯å
ææ°' }, |
| | | { key: 'DELETE', name: 'å é¤', divided: true, danger: true } |
| | | ], |
| | | (item) => { |
| | | switch (item.key) { |
| | | case 'DELETE': { |
| | | void handleDelete() |
| | | |
| | | break; |
| | | } |
| | | case 'MUTED': { |
| | | handleMuted() |
| | | |
| | | break; |
| | | } |
| | | case 'TOP': { |
| | | handleTop() |
| | | |
| | | break; |
| | | } |
| | | // No default |
| | | } |
| | | } |
| | | ) |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <template> |
| | | <div |
| | | class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]" |
| | | :class="{ '!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': isActive }" |
| | | :data-conversation-key="`${conversation.type}-${conversation.targetId}`" |
| | | @click="handleClick" |
| | | @contextmenu.prevent="handleContextMenu" |
| | | > |
| | | <!-- 头å + æªè¯»æç¤º --> |
| | | <div class="relative"> |
| | | <GroupAvatar |
| | | v-if="isGroup" |
| | | :group-id="conversation.targetId" |
| | | :url="conversation.avatar" |
| | | :name="conversation.name" |
| | | :size="40" |
| | | /> |
| | | <UserAvatar |
| | | v-else |
| | | :url="conversation.avatar" |
| | | :name="conversation.name" |
| | | :size="40" |
| | | :clickable="false" |
| | | /> |
| | | <!-- æ°åå¾½æ ï¼éå
ææ°ä¸ææªè¯»æ¶æ¾ç¤ºå
·ä½æ¡æ° --> |
| | | <span |
| | | v-show="!conversation.silent && conversation.unreadCount > 0" |
| | | class="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1.5 text-11px leading-[18px] text-white text-center bg-[#f56c6c] border border-solid border-white dark:border-[var(--ant-color-bg-container)] rounded-full box-border whitespace-nowrap" |
| | | > |
| | | {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }} |
| | | </span> |
| | | <!-- å°çº¢ç¹ï¼å
ææ°ä¸ææªè¯»æ¶æç¤ºå卿°æ¶æ¯ --> |
| | | <span |
| | | v-show="conversation.silent && conversation.unreadCount > 0" |
| | | class="absolute -top-1 -right-1 w-2.5 h-2.5 bg-[#f56c6c] border border-solid border-white dark:border-[var(--ant-color-bg-container)] rounded-full box-border" |
| | | ></span> |
| | | </div> |
| | | |
| | | <div class="flex-1 min-w-0"> |
| | | <div class="flex items-center justify-between"> |
| | | <span class="flex flex-1 items-center gap-1 min-w-0"> |
| | | <span class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]"> |
| | | {{ conversation.name }} |
| | | </span> |
| | | <Tag |
| | | v-if="isGroup" |
| | | type="primary" |
| | | size="small" |
| | | class="conversation-item__tag flex-shrink-0 !h-[18px] !px-1 !leading-4" |
| | | > |
| | | 群 |
| | | </Tag> |
| | | </span> |
| | | <span class="flex-shrink-0 ml-1 text-12px text-[var(--ant-color-text-secondary)]"> |
| | | {{ formatConversationTime(conversation.lastSendTime) }} |
| | | </span> |
| | | </div> |
| | | <div class="flex items-center mt-1 leading-5"> |
| | | <!-- è¿ç¾¤ç³è¯·çº¢ååç¼ï¼ç¾¤ä¸» / 管çåçå°èªå·±ç®¡çç群ä¸è¿ææªå¤çç³è¯·æ¶æ¾ç¤º --> |
| | | <span |
| | | v-if="requestText" |
| | | class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap" |
| | | > |
| | | {{ requestText }} |
| | | </span> |
| | | <!-- @红åæç¤ºï¼atMe ä¼å
äº atAll --> |
| | | <span |
| | | v-if="atText" |
| | | class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap" |
| | | > |
| | | {{ atText }} |
| | | </span> |
| | | <!-- å
ææ°æªè¯»æ¡æ° --> |
| | | <span |
| | | v-if="mutedUnreadText" |
| | | class="flex-shrink-0 mr-1 text-12px text-[var(--ant-color-text-secondary)] whitespace-nowrap" |
| | | > |
| | | {{ mutedUnreadText }} |
| | | </span> |
| | | <!-- 群èæå䏿¡åéè
åç¼ï¼æ lastSenderId + å½åä¼è¯ä¸ä¸æå®æ¶ç®åå --> |
| | | <span |
| | | v-if="showSendName" |
| | | class="conversation-item__sender flex-shrink overflow-hidden text-12px text-[var(--ant-color-text-secondary)] truncate whitespace-nowrap" |
| | | > |
| | | {{ lastSenderDisplayName }}: |
| | | </span> |
| | | <span |
| | | class="flex-1 min-w-0 overflow-hidden text-12px truncate text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | {{ lastContentDisplay }} |
| | | </span> |
| | | <!-- å
ææ°å¾æ --> |
| | | <Icon |
| | | v-if="conversation.silent" |
| | | icon="mdi:bell-off-outline" |
| | | :size="14" |
| | | class="conversation-item__silent flex-shrink-0 ml-1 text-[var(--ant-color-text-disabled)]" |
| | | title="æ¶æ¯å
ææ°" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | /* æ¶æ el-tag åä¼è¯æ¶ active åºè²ååçæ¸åï¼çèµ·æ¥åéªçï¼ï¼ :deep ç©¿é el-tag èªèº«æ ·å¼ */ |
| | | .conversation-item__tag { |
| | | transition: none !important; |
| | | } |
| | | |
| | | /* el-icon çå
¨å± color:var(--color) å¨æè²æ¨¡å¼ä¸ä¼æ¸²ææç½è²ï¼è¿éç¨ :deep + !important éå® */ |
| | | .conversation-item__silent :deep(svg) { |
| | | fill: currentColor !important; |
| | | } |
| | | |
| | | .conversation-item__prefix { |
| | | max-width: 45%; |
| | | } |
| | | |
| | | .conversation-item__sender { |
| | | max-width: 50%; |
| | | } |
| | | </style> |