| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { Message } from '../../../../types' |
| | | |
| | | import { computed, ref, watch } from 'vue' |
| | | |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { Button, message } from 'ant-design-vue' |
| | | |
| | | import { unpinGroupMessage as apiUnpinGroupMessage } from '#/api/im/group' |
| | | import { getCurrentUserId } from '#/views/im/utils/auth' |
| | | import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants' |
| | | import { resolveConversationLastContent } from '#/views/im/utils/conversation' |
| | | import { getSenderDisplayName, isGroupQuit } from '#/views/im/utils/user' |
| | | |
| | | import { useGroupStore } from '../../../../store/groupStore' |
| | | |
| | | defineOptions({ name: 'ImGroupPinnedMessage' }) |
| | | |
| | | const props = defineProps<{ |
| | | /** å½å群ç¼å·ï¼èªè¡ä» groupStore æ¿å®æ´ Groupï¼è·éååºå¼ï¼ */ |
| | | groupId: number |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | /** ç¹å»ç½®é¡¶æ¶æ¯ â ç¶çº§ MessagePanel æ»å¨å®ä½å°åæ¶æ¯ä½ç½® */ |
| | | locate: [messageId: number] |
| | | }>() |
| | | |
| | | const groupStore = useGroupStore() |
| | | |
| | | /** å½å群ï¼å« pinnedMessagesï¼ */ |
| | | const group = computed(() => groupStore.getGroup(props.groupId)) |
| | | |
| | | const expanded = ref(false) |
| | | const removingId = ref<null | number>(null) |
| | | |
| | | // å群æ¶éç½®å±å¼ / ç§»é¤ä¸ç¶æï¼æ¬å° ref ä¸è·é groupIdï¼å¦åä¸ä¸ç¾¤"å±å¼"æ"ç§»é¤ä¸"ä¼å¸¦å°æ°ç¾¤ |
| | | watch( |
| | | () => props.groupId, |
| | | () => { |
| | | expanded.value = false |
| | | removingId.value = null |
| | | } |
| | | ) |
| | | |
| | | /** å½åç¾¤ç½®é¡¶æ¶æ¯å表ï¼ç´æ¥èµ° group.valueï¼è·éååºå¼ï¼ */ |
| | | const pinnedMessages = computed<Message[]>(() => group.value?.pinnedMessages ?? []) |
| | | |
| | | /** é¡¶é¨è¶åå±ç¤ºçææ°ä¸æ¡ï¼å³å表æå䏿¡ï¼pin 顺åºè¿½å ï¼ */ |
| | | const latest = computed<Message | null>( |
| | | () => pinnedMessages.value[pinnedMessages.value.length - 1] ?? null |
| | | ) |
| | | |
| | | /** å½åç¨æ·æ¯å¦ç¾¤ä¸» / 管çåï¼å³å®æ¯å¦æ¾ç¤ºãç§»é¤ãå
¥å£ï¼ */ |
| | | const canManage = computed(() => { |
| | | // åå²éç¾¤ç¾¤ï¼æ¬å°ç¼åæ®çæ¶ä¹ä¸ç»ãç§»é¤ãå
¥å£ |
| | | if (isGroupQuit(group.value)) { |
| | | return false |
| | | } |
| | | const myId = getCurrentUserId() |
| | | const role = group.value?.members?.find((m) => m.userId === myId)?.role |
| | | return role === ImGroupMemberRole.OWNER || role === ImGroupMemberRole.ADMIN |
| | | }) |
| | | |
| | | /** é¡¶é¨è¶åç¹å»ï¼åæ¡ç´æ¥è·³è½¬åæ¶æ¯ä½ç½®ï¼å¤æ¡åæ¢å±å¼ / æå */ |
| | | function handleTopClick() { |
| | | if (!latest.value) { |
| | | return |
| | | } |
| | | if (pinnedMessages.value.length === 1) { |
| | | handleLocate(latest.value) |
| | | return |
| | | } |
| | | expanded.value = !expanded.value |
| | | } |
| | | |
| | | /** ç¹å»ç½®é¡¶æ¶æ¯è¡ â 触å跳转 + æ¶èµ·å¼¹åºå± */ |
| | | function handleLocate(message: Message) { |
| | | if (!message.id) { |
| | | return |
| | | } |
| | | emit('locate', message.id) |
| | | expanded.value = false |
| | | } |
| | | |
| | | /** ç½®é¡¶æ¶æ¯åé人æ¾ç¤ºå */ |
| | | function getSenderName(message: Message): string { |
| | | return group.value |
| | | ? getSenderDisplayName(message.senderId, ImConversationType.GROUP, group.value.id) |
| | | : '' |
| | | } |
| | | |
| | | /** ç½®é¡¶æ¶æ¯é¢è§ææ¬ï¼å¤ç¨ä¼è¯æå䏿¡æè¦é»è¾ï¼[å¾ç] / [æä»¶] / ææ¬çï¼ */ |
| | | function getPreview(message: Message): string { |
| | | return group.value |
| | | ? resolveConversationLastContent(message, ImConversationType.GROUP, group.value.id) |
| | | : '' |
| | | } |
| | | |
| | | /** ç§»é¤ç½®é¡¶ï¼è°å端 APIï¼loading æé´ç¦æ¢éå¤ç¹ï¼å端广æ GROUP_MESSAGE_UNPIN ç± dispatcher èªå¨åæ¥æ¬å° */ |
| | | async function handleRemove(pinnedMessage: Message) { |
| | | if (!group.value || !pinnedMessage.id || removingId.value !== null) { |
| | | return |
| | | } |
| | | removingId.value = pinnedMessage.id |
| | | try { |
| | | await apiUnpinGroupMessage({ id: group.value.id, messageId: pinnedMessage.id }) |
| | | message.success('å·²åæ¶ç½®é¡¶') |
| | | } finally { |
| | | removingId.value = null |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- 群èç½®é¡¶æ¶æ¯ï¼ä»
群è + æç½®é¡¶æ¶æ¾ç¤ºï¼æ¬æå¨ç¾¤è头é¨ä¸æ¹å·¦ä¸è§ï¼ä¸å æ´è¡ï¼å¯¹é½å¾®ä¿¡ PCï¼ --> |
| | | <div |
| | | v-if="latest" |
| | | class="im-group-pinned-message relative flex flex-shrink-0 flex-col items-start px-4 pt-1.5 pb-2 bg-[var(--ant-color-fill-secondary)]" |
| | | > |
| | | <!-- é¡¶é¨è¶åï¼åæ¡ç¹å»è·³è½¬ï¼å¤æ¡æå ç¹å»å±å¼ï¼å¤æ¡å±å¼ç¹å»æå --> |
| | | <div |
| | | class="flex items-center gap-1.5 w-[360px] px-3 py-1.5 rounded-[10px] text-13px text-[var(--ant-color-text)] bg-[var(--ant-color-bg-container)] shadow-[0_1px_2px_rgba(0,0,0,0.04)] cursor-pointer hover:bg-[var(--ant-color-fill-tertiary)]" |
| | | @click="handleTopClick" |
| | | > |
| | | <Icon |
| | | icon="ant-design:pushpin-outlined" |
| | | :size="14" |
| | | class="flex-shrink-0 text-[var(--ant-color-warning)]" |
| | | /> |
| | | <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)]">{{ getSenderName(latest) }}ï¼</span> |
| | | <span class="flex-1 min-w-0 truncate">{{ getPreview(latest) }}</span> |
| | | <!-- åæ¡ï¼ç§»é¤æé®ï¼å¤æ¡æå ï¼å
± N æ¡ï¼å¤æ¡å±å¼ï¼æ¶èµ·ç®å¤´ --> |
| | | <Button |
| | | v-if="pinnedMessages.length === 1 && canManage" |
| | | type="link" |
| | | size="small" |
| | | :loading="removingId === latest.id" |
| | | class="flex-shrink-0 !h-auto !p-0 text-13px" |
| | | @click.stop="handleRemove(latest)" |
| | | > |
| | | ç§»é¤ |
| | | </Button> |
| | | <template v-else-if="pinnedMessages.length > 1"> |
| | | <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)] text-12px"> |
| | | å
± {{ pinnedMessages.length }} æ¡ |
| | | </span> |
| | | <Icon |
| | | :icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" |
| | | :size="11" |
| | | class="flex-shrink-0 text-[var(--ant-color-text-placeholder)]" |
| | | /> |
| | | </template> |
| | | </div> |
| | | |
| | | <!-- 夿¡å±å¼ï¼æµ
è²é¢æ¿å
è£¹å®æ´åè¡¨ï¼æ¯æ¡ç¬ç«è¶åï¼ç¹å»è·³è½¬å°å¯¹åºæ¶æ¯ä½ç½® --> |
| | | <div |
| | | v-if="pinnedMessages.length > 1 && expanded" |
| | | class="im-group-pinned-message__list absolute top-full left-1.5 z-10 flex flex-col gap-2.5 w-[380px] p-3 rounded-xl bg-[var(--ant-color-bg-container)] shadow-[0_6px_16px_rgba(0,0,0,0.12)]" |
| | | style="margin-top: -1px" |
| | | > |
| | | <div |
| | | v-for="msg in pinnedMessages" |
| | | :key="msg.id" |
| | | class="flex items-center gap-1.5 w-full px-3 py-1.5 rounded-[10px] text-13px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-secondary)] cursor-pointer hover:bg-[var(--ant-color-bg-container)]" |
| | | @click="handleLocate(msg)" |
| | | > |
| | | <Icon |
| | | icon="ant-design:pushpin-outlined" |
| | | :size="14" |
| | | class="flex-shrink-0 text-[var(--ant-color-warning)]" |
| | | /> |
| | | <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)]"> |
| | | {{ getSenderName(msg) }}ï¼ |
| | | </span> |
| | | <span class="flex-1 min-w-0 truncate">{{ getPreview(msg) }}</span> |
| | | <Button |
| | | v-if="canManage" |
| | | type="link" |
| | | size="small" |
| | | :loading="removingId === msg.id" |
| | | class="flex-shrink-0 !h-auto !p-0 text-13px" |
| | | @click.stop="handleRemove(msg)" |
| | | > |
| | | ç§»é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | /* å¼¹åºå±æä¸çä¸è§ç®å¤´ï¼èµ° ::before + 4 è¾¹ border é
è²ç»ï¼é¢è²è·å¼¹åºå± background ä¸è´ */ |
| | | .im-group-pinned-message__list::before { |
| | | content: ''; |
| | | position: absolute; |
| | | top: -8px; |
| | | left: 184px; |
| | | width: 0; |
| | | height: 0; |
| | | border-left: 8px solid transparent; |
| | | border-right: 8px solid transparent; |
| | | border-bottom: 8px solid var(--ant-color-bg-container); |
| | | filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.04)); |
| | | } |
| | | </style> |