| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, ref, useTemplateRef, watch } from 'vue' |
| | | |
| | | import { CommonStatusEnum } from '#/packages/constants/src' |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { getCurrentUserId } from '#/views/im/utils/auth' |
| | | import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '#/views/im/utils/constants' |
| | | |
| | | import { GroupMember, type GroupMemberLite } from '../../../../components/group' |
| | | |
| | | defineOptions({ name: 'ImMentionPicker' }) |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | canAtAll?: boolean // å½åç¨æ·æ¯å¦è½ @ å
¨åï¼ç¾¤ä¸» / 管çåï¼ï¼ç¶ç»ä»¶æè§è²ç®å¥½ä¼ å
¥ |
| | | members: GroupMemberLite[] // å½å群çæåå表 |
| | | // æµ®å±ä½ç½®ï¼x æ¨ªåæ + top / bottom äºéä¸ï¼bottom é宿¶ picker 䏿²¿è´´ @ 䏿¹ï¼ |
| | | position?: { bottom?: number; top?: number; x: number; } |
| | | searchText?: string // @ åè¾å
¥çè¿æ»¤ææ¬ |
| | | visible: boolean // æ¯å¦æ¾ç¤º |
| | | }>(), |
| | | { |
| | | searchText: '', |
| | | position: () => ({ x: 0, bottom: 0 }) |
| | | } |
| | | ) |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [member: GroupMemberLite] |
| | | 'update:visible': [value: boolean] |
| | | }>() |
| | | |
| | | const scrollRef = useTemplateRef<HTMLDivElement>('scrollRef') |
| | | const activeIdx = ref(0) |
| | | |
| | | /** å½åç»å½ç¨æ· idï¼æååè¡¨è¿æ»¤æèªå·±ï¼ */ |
| | | const selfUserId = computed(() => getCurrentUserId()) |
| | | |
| | | /** |
| | | * èæ"ææäºº"项ï¼ç¾¤ä¸» / 管çåï¼canAtAll=trueï¼+ å
³é®åå½ä¸"ææäºº"åç¼æ¶åå¨ |
| | | * |
| | | * MessageInput èµ° token data-id æ¶é atUserIdsï¼ä¸ä¾èµææ¡åç¬¦ä¸²ï¼ |
| | | * è¿éç userId / ææ¡é½ä» im/utils/constants åï¼é¿å
æ£è½ |
| | | */ |
| | | const allItem = computed<GroupMemberLite | null>(() => { |
| | | if (!props.canAtAll) { |
| | | return null |
| | | } |
| | | if (!IM_AT_ALL_NICKNAME.startsWith(props.searchText)) { |
| | | return null |
| | | } |
| | | // @ææäºº æ¯ä¸ªä¼ªæåï¼nickname ç» IM_AT_ALL_NICKNAME 让头å :name è¡ä¸ºå¯¹é½æ®éæå |
| | | return { |
| | | userId: IM_AT_ALL_USER_ID, |
| | | showName: IM_AT_ALL_NICKNAME, |
| | | nickname: IM_AT_ALL_NICKNAME |
| | | } |
| | | }) |
| | | |
| | | /** çæåï¼è¿æ»¤èªå·± / é群 / ä¸å¹é
å
³é®åï¼ä¸æªææ°éï¼æµ®å± max-height + el-scrollbar ææ»å¨ */ |
| | | const memberItems = computed<GroupMemberLite[]>(() => |
| | | props.members.filter( |
| | | (member) => |
| | | member.userId !== selfUserId.value && |
| | | member.status !== CommonStatusEnum.DISABLE && |
| | | !!member.showName && |
| | | member.showName.startsWith(props.searchText) |
| | | ) |
| | | ) |
| | | |
| | | /** é®ç导èªä¸ pickActive èµ°çæå¹³å表ï¼allItem å¨åãmemberItems å¨å */ |
| | | const showMembers = computed<GroupMemberLite[]>(() => { |
| | | return allItem.value ? [allItem.value, ...memberItems.value] : memberItems.value |
| | | }) |
| | | |
| | | /** åéå表ååï¼ç¨æ·è¾å
¥å
³é®è¯å¨è¿æ»¤ï¼â éç½®é«äº®å°é¦é¡¹ + æ»åé¡¶ */ |
| | | watch(showMembers, (list) => { |
| | | activeIdx.value = list.length > 0 ? 0 : -1 |
| | | scrollToTop() |
| | | }) |
| | | |
| | | /** æµ®å±éæ°æå¼ â éç½®é«äº® + æ»åé¡¶ï¼é¿å
䏿¬¡çä¸é´ç¶ææ®çï¼ */ |
| | | watch( |
| | | () => props.visible, |
| | | (v) => { |
| | | if (v) { |
| | | activeIdx.value = showMembers.value.length > 0 ? 0 : -1 |
| | | scrollToTop() |
| | | } |
| | | } |
| | | ) |
| | | |
| | | /** el-scrollbar 没æ´é² scrollToï¼ç´æ¥æ¿å
é¨ wrap è° scrollTop */ |
| | | function scrollToTop() { |
| | | const scrollWrap = scrollRef.value |
| | | if (scrollWrap) { |
| | | scrollWrap.scrollTop = 0 |
| | | } |
| | | } |
| | | |
| | | /** é®çä¸ä¸å¯¼èªæ¶æé«äº®é¡¹æ»å°å¯è§åºï¼è¶
åºåºè¾¹ä¸æ¨ãè¶
åºé¡¶è¾¹ä¸æï¼å¦åä¸å¨ */ |
| | | function scrollToActive() { |
| | | const scrollWrap = scrollRef.value |
| | | if (!scrollWrap) { |
| | | return |
| | | } |
| | | const itemHeight = 40 |
| | | const activeOffsetTop = activeIdx.value * itemHeight |
| | | if (activeOffsetTop + itemHeight > scrollWrap.scrollTop + scrollWrap.clientHeight) { |
| | | scrollWrap.scrollTop = activeOffsetTop + itemHeight - scrollWrap.clientHeight |
| | | } else if (activeOffsetTop < scrollWrap.scrollTop) { |
| | | scrollWrap.scrollTop = activeOffsetTop |
| | | } |
| | | } |
| | | |
| | | /** éä¸ä¸é¡¹ï¼emit ç» MessageInput è½ tokenï¼åæ¶å
³ææµ®å± */ |
| | | function handleSelect(member: GroupMemberLite) { |
| | | emit('select', member) |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | // æ´é²ç»ç¶ç»ä»¶çé®çå¯¼èªæ¹æ³ |
| | | defineExpose({ |
| | | moveUp() { |
| | | if (activeIdx.value > 0) { |
| | | activeIdx.value-- |
| | | scrollToActive() |
| | | } |
| | | }, |
| | | moveDown() { |
| | | if (activeIdx.value < showMembers.value.length - 1) { |
| | | activeIdx.value++ |
| | | scrollToActive() |
| | | } |
| | | }, |
| | | pickActive() { |
| | | const member = showMembers.value[activeIdx.value] |
| | | if (activeIdx.value >= 0 && member) { |
| | | handleSelect(member) |
| | | } |
| | | }, |
| | | hasCandidates: () => showMembers.value.length > 0 |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | @ æåéæ©æµ®å±ï¼å¯¹é½å¾®ä¿¡ PCï¼ææäººå¨é¡¶ + 群æååç» + åºé¨ä¸è§æéï¼ |
| | | - ç¶ç»ä»¶éè¿ v-model:visible æ§å¶æ¾éï¼searchText è¿æ»¤ |
| | | - ç¶ç»ä»¶éè¿ ref è° moveUp / moveDown / pickActive å®ç°é®çä¸ä¸ + Enter éä¸ |
| | | - @select åå°è¢«éä¸çæå |
| | | --> |
| | | <div |
| | | v-show="visible && showMembers.length > 0" |
| | | class="message-input__mention-picker !fixed z-100 w-50 rounded-md bg-[var(--ant-color-bg-container)] shadow-[0_4px_16px_rgba(0,0,0,0.12)]" |
| | | :style="{ |
| | | left: `${position.x }px`, |
| | | top: position.top != null ? `${position.top }px` : 'auto', |
| | | bottom: position.bottom != null ? `${position.bottom }px` : 'auto' |
| | | }" |
| | | > |
| | | <div ref="scrollRef" style="max-height: 300px; overflow-y: auto"> |
| | | <!-- ææäººï¼èæé¡¹ï¼ä»
群主å¯éï¼èè²æ¹å + 群ä½å¾æ ï¼è·ä¸é¢çæå头ååºå --> |
| | | <div |
| | | v-if="allItem" |
| | | class="flex items-center gap-2.5 px-[5px] h-10 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]" |
| | | :class="{ 'bg-[#e1eaf7] dark:bg-[var(--ant-color-primary-bg)]': activeIdx === 0 }" |
| | | @click.stop="handleSelect(allItem)" |
| | | > |
| | | <div |
| | | class="flex items-center justify-center w-[30px] h-[30px] rounded text-white bg-[var(--ant-color-primary)] flex-shrink-0" |
| | | > |
| | | <Icon icon="ep:user-filled" :size="18" /> |
| | | </div> |
| | | <span class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]"> |
| | | {{ allItem.showName }} |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 群æå section headerï¼åªæ"ææäºº + çæå"两边é½å¨æ¶æåºç°ï¼é¿å
å䏿¥æºæ¶æ¾å¾å¤ä½ --> |
| | | <div |
| | | v-if="allItem && memberItems.length > 0" |
| | | class="px-2 pt-2 pb-1 text-12px text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | 群æå |
| | | </div> |
| | | |
| | | <!-- çæåè¡ --> |
| | | <GroupMember |
| | | v-for="(member, idx) in memberItems" |
| | | :key="member.userId" |
| | | :member="member" |
| | | :height="40" |
| | | :active="activeIdx === (allItem ? idx + 1 : idx)" |
| | | :clickable="false" |
| | | @click.stop="handleSelect(member)" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- ä¸è§æéï¼picker å¨ @ 䏿¹ï¼position.top éå®ï¼â ç®å¤´æä¸è´´é¡¶ï¼picker å¨ @ 䏿¹ï¼position.bottom éå®ï¼â ç®å¤´æä¸è´´åº --> |
| | | <div |
| | | class="absolute left-4 w-3 h-3 rotate-45 bg-[var(--ant-color-bg-container)]" |
| | | :class="position.top != null ? '-top-1.5' : '-bottom-1.5'" |
| | | ></div> |
| | | </div> |
| | | </template> |