| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { SystemUserApi } from '#/api/system/user' |
| | | |
| | | import { computed, ref } from 'vue' |
| | | |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | import { useUserStore } from '#/packages/stores/src' |
| | | |
| | | import { Button, Input, message, Modal, Spin } from 'ant-design-vue' |
| | | |
| | | import { getSimpleUserListByNickname } from '#/api/system/user' |
| | | import { getCurrentUserId } from '#/views/im/utils/auth' |
| | | |
| | | import { ImFriendAddSource } from '../../../utils/constants' |
| | | import { getGenderColor, getGenderIcon } from '../../../utils/user' |
| | | import { useFriendStore } from '../../store/friendStore' |
| | | import { UserAvatar } from '../user' |
| | | |
| | | defineOptions({ name: 'ImFriendAddDialog' }) |
| | | |
| | | const visible = ref(false) // å¼¹çªæ¯å¦å¯è§ |
| | | const presetUser = ref<null | SystemUserApi.UserSimple>(null) // é¢å¡«ç®æ ç¨æ·ï¼é null æ¶è·³è¿æç´¢æ¥éª¤ï¼ç´æ¥è¿å
¥ç³è¯·è¡¨åï¼ç¾¤æåå 好å / åçå 好åçåºæ¯ï¼ |
| | | const addSource = ref<number>(ImFriendAddSource.SEARCH) // æ·»å æ¥æºï¼åè§ ImFriendAddSourceEnumï¼é»è®¤ SEARCH |
| | | const addSourceExtra = ref<string>('') // æ¥æºé带信æ¯ï¼addSource=ImFriendAddSource.GROUP æ¶ä¼ 群åï¼è¯æ¯æ¼ä¸ºãææ¯ XX 群ç YYã |
| | | |
| | | defineExpose({ |
| | | /** æå¼å 好åå¼¹çªï¼reset â çå â visible=trueï¼ä¸ä¼ opts èµ°æç´¢æ¨¡å¼ */ |
| | | open(opts?: { addSource?: number; addSourceExtra?: string; presetUser?: null | SystemUserApi.UserSimple; }) { |
| | | presetUser.value = opts?.presetUser ?? null |
| | | addSource.value = opts?.addSource ?? ImFriendAddSource.SEARCH |
| | | addSourceExtra.value = opts?.addSourceExtra ?? '' |
| | | resetAll() |
| | | visible.value = true |
| | | } |
| | | }) |
| | | |
| | | const friendStore = useFriendStore() |
| | | const userStore = useUserStore() |
| | | |
| | | /** å½åç»å½ç¨æ·ç¼å·ï¼ç¨ computed å
ä¸å±ï¼åè´¦å·åé wsCache éåï¼é¿å
顶屿±å¼å¨ keep-alive å®ä¾éæææ§ id */ |
| | | const currentUserId = computed(() => getCurrentUserId()) |
| | | |
| | | /** æç´¢ç»æè¿æ»¤æèªå·±ï¼ç¨ v-if èé v-showï¼é¿å
DOM å ä½ + å¤´åæ æè¯·æ± */ |
| | | const visibleUsers = computed(() => |
| | | users.value.filter((user) => user.id !== currentUserId.value) |
| | | ) |
| | | const keyword = ref('') // æç´¢å
³é®å |
| | | const users = ref<SystemUserApi.UserSimple[]>([]) // æç´¢ç»æ |
| | | const searched = ref(false) // æ¯å¦å·²æç´¢ |
| | | const loading = ref(false) // æç´¢å è½½ä¸ |
| | | const step = ref<'apply' | 'search'>('search') // å½åæ¥éª¤ï¼search=æç´¢å表ï¼apply=ç³è¯·è¡¨å |
| | | const targetUser = ref<null | SystemUserApi.UserSimple>(null) // ç³è¯·ç®æ ç¨æ· |
| | | const applyContent = ref('') // ç³è¯·çç±ï¼é»è®¤å¡«ãææ¯ ${å½åæµç§°}ãï¼å¯¹é½å¾®ä¿¡äº¤äºï¼ |
| | | const displayName = ref('') // å¯¹æ¥æ¶æ¹ç夿³¨ï¼ä»
èªå·±å¯è§ï¼ |
| | | const submitting = ref(false) // æäº¤ä¸ |
| | | |
| | | /** å¼¹çªæ é¢éæ¥éª¤åæ¢ */ |
| | | const dialogTitle = computed(() => (step.value === 'apply' ? 'ç³è¯·æ·»å æå' : 'æ·»å 好å')) |
| | | |
| | | /** æ¯å¦é¢å¡«æ¨¡å¼ï¼presetUser ä¸ä¸ºç©º â è·³è¿æç´¢ï¼å
³éå³éæ¯ï¼æ ãåæ¶è¿åæç´¢ãæé®ï¼ */ |
| | | const presetMode = computed(() => !!presetUser.value) |
| | | |
| | | function resetAll() { |
| | | keyword.value = '' |
| | | users.value = [] |
| | | searched.value = false |
| | | // é¢å¡«æ¨¡å¼ï¼ç´æ¥è¿ç³è¯·è¡¨åï¼targetUser åèª presetUserï¼ç³è¯·çç±æ addSource åºåè¯æ¯ |
| | | if (presetUser.value) { |
| | | targetUser.value = presetUser.value |
| | | applyContent.value = buildPresetApplyContent() |
| | | displayName.value = '' |
| | | step.value = 'apply' |
| | | return |
| | | } |
| | | // éé¢å¡«æ¨¡å¼ï¼é»è®¤è¿æç´¢æ¥éª¤ |
| | | step.value = 'search' |
| | | targetUser.value = null |
| | | applyContent.value = '' |
| | | displayName.value = '' |
| | | } |
| | | |
| | | /** é¢å¡«æ¨¡å¼ä¸çç³è¯·çç±è¯æ¯ï¼ç¾¤èãææ¯"XX 群"ç YYãï¼å
¶å®ãææ¯ YYã */ |
| | | function buildPresetApplyContent(): string { |
| | | const myNickname = userStore.userInfo?.nickname || '' |
| | | if (!myNickname) { |
| | | return '' |
| | | } |
| | | // 群èåºæ¯æ¼å¸¦ç¾¤åçè¯æ¯ï¼å
¶å®åºæ¯é»è®¤ãææ¯ YYã |
| | | const groupExtra = addSource.value === ImFriendAddSource.GROUP ? addSourceExtra.value : '' |
| | | return groupExtra ? `ææ¯"${groupExtra}"ç${myNickname}` : `ææ¯${myNickname}` |
| | | } |
| | | |
| | | /** ææµç§°æç´¢ç¨æ·ï¼ç©ºå
³é®åç´æ¥æ¸
ç©ºç»æ */ |
| | | async function handleSearch() { |
| | | searched.value = true |
| | | if (!keyword.value.trim()) { |
| | | users.value = [] |
| | | return |
| | | } |
| | | loading.value = true |
| | | try { |
| | | users.value = (await getSimpleUserListByNickname(keyword.value.trim())) || [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** è¿å
¥ç³è¯·æ¥éª¤ï¼é¢å¡«ç³è¯·çç±ãææ¯ ${å½åç¨æ·æµç§°}ãï¼å¯¹é½å¾®ä¿¡äº¤äºï¼ */ |
| | | function enterApply(user: SystemUserApi.UserSimple) { |
| | | targetUser.value = user |
| | | const myNickname = userStore.userInfo?.nickname || '' |
| | | applyContent.value = myNickname ? `ææ¯${myNickname}` : '' |
| | | displayName.value = '' |
| | | step.value = 'apply' |
| | | } |
| | | |
| | | /** åæ¶ç³è¯·ï¼åå°æç´¢æ¥éª¤ */ |
| | | function backToSearch() { |
| | | step.value = 'search' |
| | | targetUser.value = null |
| | | } |
| | | |
| | | /** æäº¤å¥½åç³è¯·ï¼è¿å requestId èµ°ãçå¾
éªè¯ãï¼è¿å null 表示å端å½ä¸ãåå好åéé»éå¯ã忝ï¼å·²ç´æ¥æä¸ºå¥½å */ |
| | | async function handleSubmitApply() { |
| | | if (!targetUser.value) { |
| | | return |
| | | } |
| | | // 颿 ¡éªï¼ä¸è½å èªå·±ï¼æç´¢åè¡¨å·²è¿æ»¤ï¼è¿éå
åº presetUser / åçå
¥å£çåºæ¯ï¼ |
| | | if (targetUser.value.id === currentUserId.value) { |
| | | message.warning('ä¸è½æ·»å èªå·±ä¸ºå¥½å') |
| | | return |
| | | } |
| | | submitting.value = true |
| | | try { |
| | | const requestId = await friendStore.applyFriendRequest({ |
| | | toUserId: targetUser.value.id, |
| | | applyContent: applyContent.value.trim() || undefined, |
| | | displayName: displayName.value.trim() || undefined, |
| | | addSource: addSource.value |
| | | }) |
| | | // silent 忝ï¼å·²æ¯åå好å被éé»éå¯ï¼ï¼ä¸»å¨ fetchFriendInfo å
¥åºï¼ä¸ä¾èµ WS FRIEND_ADD æ¨éï¼é¿å
ä¸¢æ¨æ¶å表çä¸å° |
| | | if (requestId === null) { |
| | | await friendStore.fetchFriendInfo(targetUser.value.id) |
| | | } |
| | | message.success(requestId ? 'ç³è¯·å·²åéï¼çå¾
对æ¹éªè¯' : '已添å 为好å') |
| | | visible.value = false |
| | | } catch { |
| | | // ä¸å¡é误ï¼å·²æ¯å¥½å / è¢«å¯¹æ¹æé» / ç¨æ·è¢«ç¦ç¨ çï¼ï¼å
¨å±æ¦æªå¨å·²å¼¹é误æç¤ºï¼æ¬å°å
³å¼¹çªé¿å
èç¶æåç |
| | | visible.value = false |
| | | } finally { |
| | | submitting.value = false |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | æ·»å 好åå¯¹è¯æ¡ï¼å屿µç¨ï¼ |
| | | - 第ä¸å± searchï¼ææµç§°æç´¢ç¨æ·å表 |
| | | - 第äºå± applyï¼éä¸ç¨æ·åå±å¼ãç³è¯·æ·»å æåã表åï¼ç³è¯·çç± + 夿³¨ï¼ |
| | | --> |
| | | <Modal |
| | | v-model:open="visible" |
| | | :title="dialogTitle" |
| | | width="480px" |
| | | :mask-closable="false" |
| | | :footer="step === 'apply' ? undefined : null" |
| | | > |
| | | <!-- 第ä¸å±ï¼æç´¢ + ç¨æ·å表 --> |
| | | <template v-if="step === 'search'"> |
| | | <Input |
| | | v-model:value="keyword" |
| | | placeholder="è¾å
¥æµç§°å车æç´¢ï¼æå¤å±ç¤º 20 æ¡ï¼" |
| | | allow-clear |
| | | @keyup.enter="handleSearch" |
| | | > |
| | | <template #suffix> |
| | | <Icon icon="ant-design:search-outlined" class="cursor-pointer" @click="handleSearch" /> |
| | | </template> |
| | | </Input> |
| | | |
| | | <Spin :spinning="loading" wrapper-class-name="w-full"> |
| | | <div class="h-[400px] mt-2.5"> |
| | | <div |
| | | v-if="visibleUsers.length === 0" |
| | | class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]" |
| | | > |
| | | {{ searched ? '没ææå°ç¨æ·' : 'è¾å
¥å
³é®ååå车å¼å§æç´¢' }} |
| | | </div> |
| | | <div |
| | | v-for="user in visibleUsers" |
| | | :key="user.id" |
| | | class="flex gap-3 items-center px-2 py-2.5 border-b border-b-solid border-[var(--ant-color-border-secondary)]" |
| | | > |
| | | <UserAvatar |
| | | :id="user.id" |
| | | :url="user.avatar" |
| | | :name="user.nickname" |
| | | :size="42" |
| | | :clickable="false" |
| | | /> |
| | | <div class="flex-1 min-w-0 overflow-hidden"> |
| | | <!-- æµç§° + æ§å«å¾æ --> |
| | | <div |
| | | class="flex items-center gap-1 text-sm font-semibold text-[var(--ant-color-text)]" |
| | | > |
| | | <span class="truncate">{{ user.nickname }}</span> |
| | | <Icon |
| | | v-if="getGenderIcon(user.sex)" |
| | | :icon="getGenderIcon(user.sex)" |
| | | :size="14" |
| | | :color="getGenderColor(user.sex)" |
| | | class="flex-shrink-0" |
| | | /> |
| | | </div> |
| | | <!-- é¨é¨ --> |
| | | <div |
| | | v-if="user.deptName" |
| | | class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | {{ user.deptName }} |
| | | </div> |
| | | </div> |
| | | <!-- å·²æ¯å¥½åæ¾ç¤ºã已添å ãï¼å¦åæ¾ç¤ºãæ·»å ãï¼ç¹å»è¿å
¥ apply æ¥éª¤ï¼ --> |
| | | <Button |
| | | v-if="!friendStore.isActiveFriend(user.id)" |
| | | type="primary" |
| | | size="small" |
| | | @click="enterApply(user)" |
| | | > |
| | | æ·»å |
| | | </Button> |
| | | <Button v-else size="small" disabled>已添å </Button> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </template> |
| | | |
| | | <!-- 第äºå±ï¼ç³è¯·è¡¨åï¼å¯¹é½å¾®ä¿¡ãç³è¯·æ·»å æåãï¼ --> |
| | | <template v-if="step === 'apply' && targetUser"> |
| | | <!-- éä¸çç¨æ·å¡ç --> |
| | | <div |
| | | class="flex gap-3 items-center px-2 py-3 mb-4 rounded-md bg-[var(--ant-color-fill-secondary)]" |
| | | > |
| | | <UserAvatar |
| | | :id="targetUser.id" |
| | | :url="targetUser.avatar" |
| | | :name="targetUser.nickname" |
| | | :size="42" |
| | | :clickable="false" |
| | | /> |
| | | <div class="flex-1 min-w-0 overflow-hidden"> |
| | | <div class="text-sm font-semibold text-[var(--ant-color-text)] truncate"> |
| | | {{ targetUser.nickname }} |
| | | </div> |
| | | <div |
| | | v-if="targetUser.deptName" |
| | | class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | {{ targetUser.deptName }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="text-13px text-[var(--ant-color-text-secondary)] mb-1.5">åéæ·»å æåç³è¯·</div> |
| | | <Input.TextArea |
| | | v-model:value="applyContent" |
| | | :rows="3" |
| | | :maxlength="255" |
| | | show-count |
| | | placeholder="请填åç³è¯·çç±" |
| | | /> |
| | | |
| | | <div class="text-13px text-[var(--ant-color-text-secondary)] mt-3 mb-1.5">夿³¨</div> |
| | | <Input |
| | | v-model:value="displayName" |
| | | :maxlength="16" |
| | | placeholder="ç»å¯¹æ¹èµ·ä¸ªå¤æ³¨ï¼ä»
èªå·±å¯è§ï¼å¯ä¸å¡«ï¼" |
| | | /> |
| | | </template> |
| | | |
| | | <!-- ä»
å¨ apply æ¥éª¤æ¾ç¤º footer æä½æé®ï¼slot å¿
é¡»æ¯ el-dialog ç´æ¥åèç¹ï¼ --> |
| | | <template v-if="step === 'apply'" #footer> |
| | | <!-- é¢å¡«æ¨¡å¼æ æç´¢æ¥éª¤ï¼ãåæ¶ãç´æ¥å
³éå¼¹çª --> |
| | | <Button @click="presetMode ? (visible = false) : backToSearch()">åæ¶</Button> |
| | | <Button type="primary" :loading="submitting" @click="handleSubmitApply"> ç¡®å® </Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |