9 分钟以前 543433f64d491fe133b7a23363ff0ccacc3da07e
src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,207 @@
<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>