| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FriendLite, FriendRequest, Group, GroupLite, User } from '../../types' |
| | | |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | |
| | | import { confirm } from '#/packages/effects/common-ui/src' |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { Input, message } from 'ant-design-vue' |
| | | |
| | | import { ImConversationType } from '../../../utils/constants' |
| | | import { StorageKeys } from '../../../utils/db' |
| | | import { getFriendDisplayName, getGroupDisplayName, isGroupQuit } from '../../../utils/user' |
| | | import { ResizableAside } from '../../components' |
| | | import { UserInfo } from '../../components/user' |
| | | import { useConversationStore } from '../../store/conversationStore' |
| | | import { useFriendStore } from '../../store/friendStore' |
| | | import { useGroupStore } from '../../store/groupStore' |
| | | import FriendList from './friend-list.vue' |
| | | import FriendRequestDetail from './friend-request-detail.vue' |
| | | import FriendRequestList from './friend-request-list.vue' |
| | | import GroupDetail from './group-detail.vue' |
| | | import GroupList from './group-list.vue' |
| | | |
| | | defineOptions({ name: 'ImContactPage' }) |
| | | |
| | | const router = useRouter() |
| | | const conversationStore = useConversationStore() |
| | | const friendStore = useFriendStore() |
| | | const groupStore = useGroupStore() |
| | | |
| | | /** ç¨ type å¤å«é䏿¯å¥½å / 群è / 好åç³è¯· */ |
| | | type Selection = |
| | | | { friend: FriendLite; type: 'friend'; } |
| | | | { group: GroupLite; type: 'group'; } |
| | | | { request: FriendRequest; type: 'request'; } |
| | | |
| | | const selection = ref<null | Selection>(null) |
| | | const keyword = ref('') |
| | | |
| | | /** éä¸ç³è¯·è¯¦æ
ï¼è¯¦æ
ç¨ store éçææ°å¯æ¬ï¼åæ / æç»åç¶æä¼åï¼ */ |
| | | const currentRequest = computed<FriendRequest>(() => { |
| | | const req = selection.value?.type === 'request' ? selection.value.request : null |
| | | if (!req) { |
| | | return {} as FriendRequest |
| | | } |
| | | return friendStore.getFriendRequest(req.id) || req |
| | | }) |
| | | |
| | | /** æç¸å
³çç³è¯·å表ï¼ç¨ friendStore éç宿¶å¯æ¬ï¼ä¾¿äºéç¥å°è¾¾åèªå¨å·æ°ï¼ */ |
| | | const friendRequests = computed<FriendRequest[]>(() => friendStore.friendRequests) |
| | | |
| | | /** 好åå表çå±ç¤ºå¿«ç
§ï¼é带å端ç®å¥½çæ¼é³ï¼ç» FriendList å忝忡¶ / æ¼é³æç´¢ */ |
| | | const friends = computed<FriendLite[]>(() => friendStore.getActiveFriendLiteList) |
| | | |
| | | const groups = computed<GroupLite[]>(() => |
| | | // é讯å½åªå±ç¤ºå½åä»å¨ç¾¤çï¼å·²é群åå²ç¾¤åªçå¨ store é便¶æ¯å±ç¤ºç¾¤å / 头åï¼ä¸è¿éè®¯å½ |
| | | groupStore.groups |
| | | .filter((group: Group) => !isGroupQuit(group)) |
| | | .map((group: Group) => ({ |
| | | id: group.id, |
| | | name: group.name, |
| | | showGroupName: getGroupDisplayName(group), // ä¼å
ç¨ç¾¤å¤æ³¨ groupRemarkï¼æ²¡è®¾ç½®æ¶åè½å°å群åï¼é¿å
ç¹"è¿å
¥ç¾¤è"æ¶æå·²åæ¥ç夿³¨ä¼è¯åå·ååå |
| | | showImage: group.avatar, |
| | | showImageThumb: group.avatar, |
| | | memberCount: group.memberCount |
| | | })) |
| | | ) |
| | | |
| | | /** |
| | | * store å表å忶忥 selection æçå¯¹è±¡å¯æ¬ï¼å¯¹ç«¯æ¨é / 跨端å¨ä½æ¹ store åï¼å³ä¾§è¯¦æ
è½è·ä¸ï¼ |
| | | * - å½ä¸åæ¿æ¢ä¸ºææ°å¼ç¨ï¼èµæ / 夿³¨ / ç³è¯·ç¶æåæ´ç«å»åæ |
| | | * - æ¾ä¸å°ï¼å é¤ / æç» / å·²éè¿çè®©è®°å½æ¶å¤±ï¼åç½® null æ¶èµ·è¯¦æ
|
| | | */ |
| | | watch( |
| | | friends, |
| | | (list) => { |
| | | const selected = selection.value |
| | | if (selected?.type !== 'friend') { |
| | | return |
| | | } |
| | | const fresh = list.find((friend) => friend.id === selected.friend.id) |
| | | if (!fresh) { |
| | | selection.value = null |
| | | } else if (fresh !== selected.friend) { |
| | | selection.value = { type: 'friend', friend: fresh } |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | watch( |
| | | groups, |
| | | (list) => { |
| | | const selected = selection.value |
| | | if (selected?.type !== 'group') { |
| | | return |
| | | } |
| | | const fresh = list.find((group) => group.id === selected.group.id) |
| | | if (!fresh) { |
| | | selection.value = null |
| | | } else if (fresh !== selected.group) { |
| | | selection.value = { type: 'group', group: fresh } |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | watch( |
| | | friendRequests, |
| | | (list) => { |
| | | const selected = selection.value |
| | | if (selected?.type !== 'request') { |
| | | return |
| | | } |
| | | const fresh = list.find((request) => request.id === selected.request.id) |
| | | if (!fresh) { |
| | | selection.value = null |
| | | } else if (fresh !== selected.request) { |
| | | selection.value = { type: 'request', request: fresh } |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | const friendUser = computed<null | User>(() => { |
| | | if (selection.value?.type !== 'friend') { |
| | | return null |
| | | } |
| | | const friend = selection.value.friend |
| | | return { |
| | | id: friend.id, |
| | | nickname: friend.nickname, |
| | | avatar: friend.avatar |
| | | } |
| | | }) |
| | | |
| | | onMounted(async () => { |
| | | await Promise.all([ |
| | | friendStore.fetchFriendList(), |
| | | friendStore.fetchFriendRequestList(), |
| | | groupStore.fetchGroupList() |
| | | ]) |
| | | }) |
| | | |
| | | /** éä¸å¥½å â åå°å¥½å详æ
*/ |
| | | function handleSelectFriend(friend: FriendLite) { |
| | | selection.value = { type: 'friend', friend } |
| | | } |
| | | |
| | | /** éä¸ç¾¤è â åå°ç¾¤è¯¦æ
*/ |
| | | function handleSelectGroup(group: GroupLite) { |
| | | selection.value = { type: 'group', group } |
| | | } |
| | | |
| | | /** éä¸å¥½åç³è¯· â åå°ãæ°çæåã详æ
*/ |
| | | function handleSelectRequest(request: FriendRequest) { |
| | | selection.value = { type: 'request', request } |
| | | } |
| | | |
| | | /** ç³è¯·è¯¦æ
éç¹ãåæ¶æ¯ãï¼ç´æ¥è¿ä¸å¯¹ç«¯çç§èä¼è¯ */ |
| | | function handleChatPeer(peerUserId: number) { |
| | | const friend = friendStore.getFriend(peerUserId) |
| | | const conversationName = friend ? getFriendDisplayName(friend) : String(peerUserId) |
| | | conversationStore.openConversation( |
| | | peerUserId, |
| | | ImConversationType.PRIVATE, |
| | | conversationName, |
| | | friend?.avatar || '', |
| | | { silent: !!friend?.silent } |
| | | ) |
| | | router.push({ name: 'ImHomeConversation' }) |
| | | } |
| | | |
| | | /** è¿å
¥ä¸è¯¥å¥½åçç§èä¼è¯ */ |
| | | function handleChatFriend(friend: FriendLite) { |
| | | // ä» friendStore 忥夿³¨ + å
ææ°ï¼é¿å
æ°å»ºä¼è¯ç¨è¿ææ°æ® |
| | | const entry = friendStore.getFriend(friend.id) |
| | | const conversationName = entry ? getFriendDisplayName(entry) : friend.nickname |
| | | conversationStore.openConversation( |
| | | friend.id, |
| | | ImConversationType.PRIVATE, |
| | | conversationName, |
| | | friend.avatar || '', |
| | | { silent: !!entry?.silent } |
| | | ) |
| | | router.push({ name: 'ImHomeConversation' }) |
| | | } |
| | | |
| | | /** è¿å
¥è¯¥ç¾¤ç群èä¼è¯ */ |
| | | function handleChatGroup(group: GroupLite) { |
| | | const entry = groupStore.getGroup(group.id) |
| | | conversationStore.openConversation( |
| | | group.id, |
| | | ImConversationType.GROUP, |
| | | group.showGroupName || group.name || '', |
| | | group.showImage || group.showImageThumb || '', |
| | | { silent: !!entry?.silent } |
| | | ) |
| | | router.push({ name: 'ImHomeConversation' }) |
| | | } |
| | | |
| | | /** å é¤å¥½åï¼äºæ¬¡ç¡®è®¤ â store è½åº â æ¸
空å½åéä¸ */ |
| | | async function handleDeleteFriend(friend: FriendLite) { |
| | | try { |
| | | await confirm(`ç¡®å®å é¤å¥½åã${friend.nickname}ãåï¼`, 'å é¤è系人') |
| | | // friendStore.deleteFriend å
é¨å·²ç»çº§èæ¸
ç对åºç§èä¼è¯ |
| | | await friendStore.deleteFriend(friend.id) |
| | | if (selection.value?.type === 'friend' && selection.value.friend.id === friend.id) { |
| | | selection.value = null |
| | | } |
| | | message.success('å·²å é¤å¥½å') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 夿³¨å·²ä¿åï¼UserInfo å
é¨å·²ç»èµ°å® friendStore è½åº + æç¤ºï¼æ¬ä¾§åªè´è´£åæ¥ selection æçæ§ FriendLite 坿¬ */ |
| | | function onRemarkSaved(displayName: string) { |
| | | if (selection.value?.type === 'friend') { |
| | | selection.value.friend.displayName = displayName |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | éè®¯å½ Tab æ´é¡µï¼åè微信 PC é讯å½ï¼ |
| | | - å·¦ï¼æç´¢ + GroupList / FriendList 两个æå åç»ï¼å¥½åæåæ¯åæ¡¶ï¼ |
| | | - å³ï¼éä¸é¡¹ç详æ
ï¼å¥½åèµ°å
±äº« <UserInfo>ï¼relation=friendï¼ï¼ç¾¤èèµ° GroupDetail |
| | | - æ¬é¡µä»
åï¼éä¸åå + æ°æ®æºè½¬æ¢ + è·¨ç»ä»¶äºä»¶è½ store |
| | | --> |
| | | <div class="flex flex-1 h-full min-w-0 bg-[var(--ant-color-bg-container)]"> |
| | | <ResizableAside :default-width="260" :storage-key="StorageKeys.localStorage.asideWidth"> |
| | | <!-- é¡¶é¨ï¼ä»
æç´¢æ¡ï¼h-14 䏿¶æ¯ Tab é¡¶é¨å¯¹é½ï¼é¿å
åæ¢æ¶æç´¢æ¡ä¸ä¸æå¨ --> |
| | | <div |
| | | class="flex flex-shrink-0 items-center h-14 px-4 border-b border-b-solid border-[var(--im-border-color-lighter)]" |
| | | > |
| | | <Input v-model:value="keyword" placeholder="æç´¢" allow-clear class="flex-1"> |
| | | <template #prefix> |
| | | <Icon icon="ant-design:search-outlined" /> |
| | | </template> |
| | | </Input> |
| | | </div> |
| | | |
| | | <!-- å表主ä½ï¼æ FriendRequestList / GroupList / FriendList ä¸ä¸ªåç»ä»¶ï¼åèªç®¡çæå + è¿æ»¤ï¼æ¬é¡µåªéä¼ é䏿 --> |
| | | <!-- overflow-y-autoï¼èç³»äººå¤æ¶æ¬åè¡¨å¯æ»å¨ï¼ç¶ ResizableAside 䏿便»å¨ï¼ï¼å¯¹é½ Vue3 ç el-scrollbar --> |
| | | <div class="flex-1 overflow-y-auto"> |
| | | <FriendRequestList |
| | | :requests="friendRequests" |
| | | :active-id="selection?.type === 'request' ? selection.request.id : undefined" |
| | | @select="handleSelectRequest" |
| | | /> |
| | | <GroupList |
| | | :groups="groups" |
| | | :keyword="keyword" |
| | | :active-id="selection?.type === 'group' ? selection.group.id : undefined" |
| | | @select="handleSelectGroup" |
| | | /> |
| | | <FriendList |
| | | :friends="friends" |
| | | :keyword="keyword" |
| | | :active-id="selection?.type === 'friend' ? selection.friend.id : undefined" |
| | | @select="handleSelectFriend" |
| | | @chat="handleChatFriend" |
| | | @delete="handleDeleteFriend" |
| | | /> |
| | | </div> |
| | | </ResizableAside> |
| | | |
| | | <!-- å³ä¾§è¯¦æ
åº --> |
| | | <div class="flex-1 min-w-0"> |
| | | <!-- 空æï¼å ä½å¾æ + æç¤ºææ¡ --> |
| | | <div |
| | | v-if="!selection" |
| | | class="flex flex-col items-center justify-center h-full gap-3 text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | <Icon |
| | | icon="ant-design:contacts-outlined" |
| | | :size="64" |
| | | class="text-[var(--ant-color-text-placeholder)]" |
| | | /> |
| | | <span class="text-sm">å¨å·¦ä¾§éæ©å¥½åæç¾¤èæ¥ç详æ
</span> |
| | | </div> |
| | | <!-- 好å详æ
--> |
| | | <div v-else-if="selection.type === 'friend'" class="flex justify-center pt-12 px-6"> |
| | | <div class="w-full max-w-[320px]"> |
| | | <UserInfo |
| | | :user="friendUser" |
| | | :display-name="selection.friend.displayName || ''" |
| | | relation="friend" |
| | | @chat="handleChatFriend(selection.friend)" |
| | | @deleted="selection = null" |
| | | @saved="onRemarkSaved" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <!-- 群详æ
--> |
| | | <GroupDetail |
| | | v-else-if="selection.type === 'group'" |
| | | :group="selection.group" |
| | | @chat="handleChatGroup" |
| | | /> |
| | | <!-- æ°çæå - ç³è¯·è¯¦æ
--> |
| | | <FriendRequestDetail |
| | | v-else-if="selection.type === 'request'" |
| | | :request="currentRequest" |
| | | @chat="handleChatPeer" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |