| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FriendLite } from '../../types' |
| | | |
| | | import { computed, ref } from 'vue' |
| | | |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { Input, message } from 'ant-design-vue' |
| | | |
| | | import { PagedScroller } from '..' |
| | | import { useFriendBuckets } from '../../composables/useFriendBuckets' |
| | | import { useSelectedItems } from '../../composables/useSelectedItems' |
| | | import { UserAvatar } from '../user' |
| | | |
| | | defineOptions({ name: 'ImFriendPickerPanel' }) |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | /** ç¦ç¨ idï¼å表éå±ç¤ºç½®ç°ãä¸å¯å¾éãä¸è®¡å
¥å·²éæ°ï¼å
¸åï¼é请å
¥ç¾¤æ¶å·²å¨ç¾¤æåï¼ */ |
| | | disabledIds?: number[] |
| | | /** å
¨é好åå表 */ |
| | | friends: FriendLite[] |
| | | /** éè idï¼ä¸å±ç¤ºï¼hide > locked > disabledï¼ */ |
| | | hideIds?: number[] |
| | | /** éå® idï¼é»è®¤å¾éãä¸å¯åæ¶ã计å
¥å·²éæ°ï¼å
¸åï¼ç§èä¾§ +建群éå®å¯¹æ¹ï¼ */ |
| | | lockedIds?: number[] |
| | | /** 已鿰ä¸éï¼ä¸ä¼ æ <=0 æ¶ä¸é */ |
| | | maxSize?: number |
| | | /** å·²é好å idï¼v-modelï¼ï¼ææ°ç»é¡ºåºå³ä¸ºç¹å»é¡ºåº */ |
| | | selectedIds: number[] |
| | | }>(), |
| | | { |
| | | lockedIds: () => [], |
| | | disabledIds: () => [], |
| | | hideIds: () => [], |
| | | maxSize: 0 |
| | | } |
| | | ) |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:selectedIds': [value: number[]] |
| | | }>() |
| | | |
| | | const keyword = ref('') |
| | | |
| | | /** id â friend æ å°ï¼å·²é忥 / 䏿å¤å®å
±ç¨ï¼é¿å
æ¯æ¬¡ O(N) æ« */ |
| | | const byId = computed(() => { |
| | | const map = new Map<number, FriendLite>() |
| | | for (const friend of props.friends) { |
| | | map.set(friend.id, friend) |
| | | } |
| | | return map |
| | | }) |
| | | |
| | | /** 䏿 id éåï¼æ¯æ¬¡è¿æ»¤å¤ç¨ */ |
| | | const hideSet = computed(() => new Set(props.hideIds)) |
| | | const lockedSet = computed(() => new Set(props.lockedIds)) |
| | | const disabledSet = computed(() => new Set(props.disabledIds)) |
| | | const selectedSet = computed(() => new Set(props.selectedIds)) |
| | | |
| | | /** åé好åï¼åé¤ hideIdsï¼hide ä¼å
级æé«ï¼ */ |
| | | const candidates = computed(() => |
| | | props.friends.filter((friend) => !hideSet.value.has(friend.id)) |
| | | ) |
| | | |
| | | /** å§æ useFriendBucketsï¼æç´¢è§åå¤ç¨ï¼å·¦ä¾§åè¡¨ææ»å¨å页渲æ */ |
| | | const { filtered } = useFriendBuckets(candidates, keyword) |
| | | |
| | | /** 已鿰 + å·²é好åå表ï¼ä¸æä¼å
级 + é¡ºåºæ¼æ¥ç± useSelectedItems ç»ä¸æ¿æ
*/ |
| | | const { selectedCount, selectedItems: selectedFriends } = useSelectedItems<FriendLite>( |
| | | () => props.selectedIds, |
| | | () => props.lockedIds, |
| | | () => props.disabledIds, |
| | | () => props.hideIds, |
| | | byId |
| | | ) |
| | | |
| | | /** æ¯å¦è¢«éå® */ |
| | | function isLocked(friend: FriendLite): boolean { |
| | | return lockedSet.value.has(friend.id) |
| | | } |
| | | |
| | | /** æ¯å¦è¢«ç¦ç¨ï¼locked / hide 已被åç½®è¿æ»¤ï¼å©ä¸çæç® disabled */ |
| | | function isDisabled(friend: FriendLite): boolean { |
| | | return !lockedSet.value.has(friend.id) && disabledSet.value.has(friend.id) |
| | | } |
| | | |
| | | /** æ¯å¦éä¸ï¼locked è§ä¸ºæ°¸è¿éä¸ */ |
| | | function isSelected(friend: FriendLite): boolean { |
| | | return selectedSet.value.has(friend.id) |
| | | } |
| | | |
| | | /** åå½¢å¾éæç¤ºå¨ç classï¼éä¸ / éå®èµ°ç»¿åºï¼ç¦ç¨ç°åºï¼æªé空å¿å */ |
| | | function getCheckClass(friend: FriendLite): string { |
| | | if (isLocked(friend) || isSelected(friend)) { |
| | | return 'bg-[#07c160] border border-solid border-[#07c160]' |
| | | } |
| | | if (isDisabled(friend)) { |
| | | return 'bg-[var(--ant-color-fill)] border border-solid border-[var(--ant-color-border)]' |
| | | } |
| | | return 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]' |
| | | } |
| | | |
| | | /** 忢é䏿ï¼locked / disabled ä¸ååºï¼å³æ Ã ç§»é¤ / è¡ click é½èµ°è¿é */ |
| | | function handleToggle(friend: FriendLite) { |
| | | if (isLocked(friend) || isDisabled(friend)) { |
| | | return |
| | | } |
| | | const next = [...props.selectedIds] |
| | | const index = next.indexOf(friend.id) |
| | | if (index === -1) { |
| | | if (props.maxSize > 0 && selectedCount.value >= props.maxSize) { |
| | | message.error(`æå¤éæ© ${props.maxSize} ä½å¥½å`) |
| | | return |
| | | } |
| | | next.push(friend.id) |
| | | } else { |
| | | next.splice(index, 1) |
| | | } |
| | | emit('update:selectedIds', next) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | 好å鿩颿¿ï¼ç¨äºãæ°å»ºç¾¤è / é请好å / æ¨èæ¶å建è天ãç好åéæ©åºæ¯ |
| | | - å·¦ï¼æç´¢ + 好åå表ï¼åå½¢å¾éï¼ |
| | | - å³ï¼å·²éæ°æ é¢ + å·²é好ååè¡¨ï¼æç¹å»é¡ºåºï¼ |
| | | - Panel ä¸å¸¦ el-dialog 壳ï¼dialog ç±ä¸å¡å£³ææ |
| | | - 䏿è¯ä¹ï¼hide > locked > disabledï¼è¯¦è§ contractï¼ |
| | | --> |
| | | <div class="flex h-full"> |
| | | <!-- å·¦æ --> |
| | | <div |
| | | class="flex flex-col flex-1 min-w-0 border-r border-r-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-fill-secondary)]" |
| | | > |
| | | <!-- æç´¢æ¡ --> |
| | | <div class="flex-shrink-0 px-3 py-2"> |
| | | <Input v-model:value="keyword" placeholder="æç´¢å¥½å" allow-clear> |
| | | <template #prefix> |
| | | <Icon icon="ant-design:search-outlined" /> |
| | | </template> |
| | | </Input> |
| | | </div> |
| | | |
| | | <div class="flex-1 min-h-0"> |
| | | <PagedScroller v-if="filtered.length > 0" :items="filtered" :page-size="30" item-key="id"> |
| | | <template #default="{ item }"> |
| | | <div |
| | | :key="(item as FriendLite).id" |
| | | class="flex gap-2.5 items-center px-3 py-2 cursor-pointer hover:bg-[var(--ant-color-fill)]" |
| | | :class="{ |
| | | 'opacity-60 cursor-not-allowed hover:bg-transparent': isDisabled(item as FriendLite) |
| | | }" |
| | | @click="handleToggle(item as FriendLite)" |
| | | > |
| | | <!-- åå½¢å¾éæç¤ºå¨ï¼æªéç°è²ç©ºå¿åï¼éä¸å®å¿å¾®ä¿¡ç»¿ + ç½å¯¹å¾ï¼éå® / ç¦ç¨èµ°ç°åº --> |
| | | <span |
| | | class="flex flex-shrink-0 justify-center items-center w-5 h-5 rounded-full transition-colors" |
| | | :class="getCheckClass(item as FriendLite)" |
| | | > |
| | | <Icon |
| | | v-if="isSelected(item as FriendLite) || isLocked(item as FriendLite)" |
| | | icon="ant-design:check-outlined" |
| | | :size="12" |
| | | color="#fff" |
| | | /> |
| | | </span> |
| | | <UserAvatar |
| | | :id="(item as FriendLite).id" |
| | | :url="(item as FriendLite).avatar" |
| | | :name="(item as FriendLite).nickname" |
| | | :size="36" |
| | | :clickable="false" |
| | | /> |
| | | <!-- è¡å
ååï¼å¤æ³¨ä¼å
ï¼å表éä¸éå¤å±ç¤ºæµç§° --> |
| | | <span |
| | | class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]" |
| | | > |
| | | {{ (item as FriendLite).displayName || (item as FriendLite).nickname }} |
| | | </span> |
| | | </div> |
| | | </template> |
| | | </PagedScroller> |
| | | <div v-else class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"> |
| | | {{ keyword ? '没æå¹é
ç好å' : 'ææ å¥½å' }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 峿 --> |
| | | <div class="flex flex-col flex-1 min-w-0"> |
| | | <!-- æ é¢ï¼å·²éæ°ï¼é«åº¦å¯¹é½å·¦ä¾§ input defaultï¼32pxï¼ï¼ä¿è¯ä¸¤ä¾§ç¬¬ä¸é¡¹èµ·ç¹åæ°´å¹³ --> |
| | | <div |
| | | class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b border-b-solid text-13px text-[var(--ant-color-text-secondary)] border-[var(--ant-color-border-secondary)]" |
| | | > |
| | | 已鿩 {{ selectedCount }} 个好å |
| | | </div> |
| | | |
| | | <!-- å·²éé¢è§ï¼æ selectedIds + lockedIds æ°ç»é¡ºåºï¼ç¹å»é¡ºåºï¼å±ç¤º --> |
| | | <div class="flex-1"> |
| | | <div |
| | | v-for="friend in selectedFriends" |
| | | :key="friend.id" |
| | | class="flex gap-2.5 items-center px-4 py-2" |
| | | > |
| | | <UserAvatar |
| | | :id="friend.id" |
| | | :url="friend.avatar" |
| | | :name="friend.nickname" |
| | | :size="36" |
| | | :clickable="false" |
| | | /> |
| | | <span |
| | | class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]" |
| | | > |
| | | {{ friend.displayName || friend.nickname }} |
| | | </span> |
| | | <!-- éå®é¡¹ä¸æ¸²æ Ãï¼é¿å
误以为å¯ç§»é¤ --> |
| | | <Icon |
| | | v-if="!isLocked(friend)" |
| | | icon="ant-design:close-outlined" |
| | | :size="14" |
| | | class="flex-shrink-0 cursor-pointer transition-colors text-[var(--ant-color-text-placeholder)] hover:text-[var(--ant-color-error)]" |
| | | @click="handleToggle(friend)" |
| | | /> |
| | | </div> |
| | | <div |
| | | v-if="selectedFriends.length === 0" |
| | | class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]" |
| | | > |
| | | 请ä»å·¦ä¾§éæ©å¥½å |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ä¸å¡å£³å¡é¢å¤å
容çä½ç½®ï¼FriendPickerPanel 主æµåºæ¯ä¸éè¦ footer --> |
| | | <div |
| | | v-if="$slots.footer" |
| | | class="flex-shrink-0 border-t border-t-solid border-[var(--ant-color-border-secondary)]" |
| | | > |
| | | <slot name="footer"></slot> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |