gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
src/views/im/home/pages/conversation/components/message/message-read-status.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
<script lang="ts" setup>
import type { Message } from '../../../../types'
import { computed, ref } from 'vue'
import { CommonStatusEnum } from '#/packages/constants/src'
import { Popover, TabPane, Tabs } from 'ant-design-vue'
import { getGroupReadUsers as apiGetGroupReadUsers } from '#/api/im/message/group'
import { ImConversationType, ImMessageReceiptStatus } from '../../../../../utils/constants'
import { PagedScroller } from '../../../../components'
import { GroupMember, type GroupMemberLite } from '../../../../components/group'
import { useMessageStore } from '../../../../store/messageStore'
defineOptions({ name: 'ImMessageReadStatus' })
const props = defineProps<{
  // å½“前群编号;供 loadReadUsers ä½œä¸º /im/message/group/get-read-users çš„入参
  groupId: number
  // å½“前群所有成员(外部 MessagePanel.groupMembers ä¼ å…¥ï¼›æ²¡æœ‰å°±ä¼ ç©ºæ•°ç»„,未读列表会变空但不报错)
  groupMembers: GroupMemberLite[]
  message: Message
}>()
const messageStore = useMessageStore()
// popover å¼€å…³ï¼šshow æ—¶æ‹‰å·²è¯»åå•,关闭后保留 readUserIds ç¼“存(重开同一条消息不再请求)
const popVisible = ref(false)
const activeTab = ref<'read' | 'unread'>('read')
// æœåŠ¡ç«¯è¿”å›žçš„"已读这条消息的 userId åˆ—表",未读靠 visibleMembers å‡åŽ»è¿™ä»½å¾—åˆ°
const readUserIds = ref<number[]>([])
/**
 * æ ‡ç­¾æ–‡æ¡ˆï¼š
 * - receiptStatus === DONE:服务端确认全员已读 â†’ ç›´æŽ¥æ˜¾ç¤º"全部已读",readCount ä¸å†ä½¿ç”¨
 * - readCount > 0:显示"N äººå·²è¯»"
 * - å…¶ä»–(readCount = 0 æˆ– undefined,且未到 DONE):显示"未读"
 */
const label = computed(() => {
  if (props.message.receiptStatus === ImMessageReceiptStatus.DONE) {
    return '全部已读'
  }
  const readCount = props.message.readCount || 0
  return readCount > 0 ? `${readCount} äººå·²è¯»` : '未读'
})
/**
 * è¿™æ¡æ¶ˆæ¯"应该被谁看到"的可见成员集合(已读 / æœªè¯»ä¸¤ä¸ª tab å…±ç”¨åŸºåº•)
 *
 * ä¸‰å±‚过滤:
 * 1. å®šå‘群消息:receiverUserIds éžç©ºæ—¶ï¼Œåªç®—名单内的成员。少了这层,未在投递范围里的人
 *    ä¼šè¢«é”™ç®—成"未读"(getGroupReadUsers åŽç«¯å·²æŒ‰å¯è§æ€§è¿‡æ»¤ï¼Œå‰ç«¯å¿…须配套)
 * 2. å‘送者本人:自己发的消息算不算"自己已读"语义模糊,索性两个 tab éƒ½ä¸å±•示
 * 3. å·²é€€ç¾¤ï¼ˆstatus === DISABLE):他读没读已经不关心,UI ä¸å±•示
 */
const visibleMembers = computed<GroupMemberLite[]>(() => {
  const receiverUserIds = props.message.receiverUserIds
  const isDirected = !!receiverUserIds && receiverUserIds.length > 0
  return props.groupMembers.filter(
    (member) =>
      member.status !== CommonStatusEnum.DISABLE &&
      member.userId !== props.message.senderId &&
      (!isDirected || receiverUserIds.includes(member.userId))
  )
})
/** å·²è¯» = å¯è§æˆå‘˜ âˆ© readUserIds */
const readMembers = computed(() =>
  visibleMembers.value.filter((member) => readUserIds.value.includes(member.userId))
)
/** æœªè¯» = å¯è§æˆå‘˜ âˆ’ readUserIds */
const unreadMembers = computed(() =>
  visibleMembers.value.filter((member) => !readUserIds.value.includes(member.userId))
)
/**
 * æ‹‰å–已读用户 id åˆ—表
 *
 * è§¦å‘:el-popover @show(按需懒加载,避免每条群消息都预拉)
 * è·³è¿‡ï¼šæœ¬åœ°å ä½æ¶ˆæ¯ï¼ˆid = 0,还没拿到服务端 id),后端没法按 messageId æŸ¥
 * å¤±è´¥ï¼šä»…控制台告警,readUserIds ä¿æŒç©ºæ•°ç»„ â†’ label èµ° readCount å…œåº•,不阻塞 UI
 *
 * æ‹‰åˆ°åå•后顺手把 readCount / receiptStatus å›žå†™åˆ° messageStore,让 popover å¤–面的
 * label ä¹Ÿè·Ÿç€èµ°æœ€æ–°æ•°ï¼šç¦»çº¿ / æ¼æ”¶ RECEIPT äº‹ä»¶æ—¶æœ¬åœ° readCount ä¼šåæ—§ï¼Œå¼¹å±‚里看到"已读 5"
 * ä½†å¤–面仍是"未读"或旧人数;这里以服务端返回为准矫正回去
 */
async function loadReadUsers() {
  if (!props.message.id) {
    return
  }
  try {
    const userIds = await apiGetGroupReadUsers({
      groupId: props.groupId,
      messageId: props.message.id
    })
    readUserIds.value = userIds || []
    const readCount = readUserIds.value.length
    // å…¨å¯è§æˆå‘˜éƒ½å·²è¯» â†’ æ›´æ–°ä¸º DONE,让外面 label ç›´æŽ¥å‘½ä¸­ã€Œå…¨éƒ¨å·²è¯»ã€åˆ†æ”¯ï¼›
    // å¦åˆ™åªæ›´æ–° readCount,receiptStatus ç»´æŒä¸å˜ï¼ˆPENDING)
    const allRead = readCount > 0 && readCount >= visibleMembers.value.length
    messageStore.applyMessageReadReceipt({
      conversationType: ImConversationType.GROUP,
      targetId: props.groupId,
      groupMessageId: props.message.id,
      readCount,
      receiptStatus: allRead ? ImMessageReceiptStatus.DONE : undefined
    })
  } catch (error) {
    console.error('[IM] æ‹‰å–群已读列表失败:', error)
  }
}
</script>
<template>
  <!--
    ç¾¤æ¶ˆæ¯å·²è¯»çŠ¶æ€
    - æ ‡ç­¾å½¢æ€ï¼šå±•示「N å·²è¯»ã€æˆ–「全部已读」;点击弹出 popover ç”¨ tab åˆ—出已读 / æœªè¯»æˆå‘˜
    - æ¸²æŸ“条件:仅在群聊、自己发送、receiptStatus !== NO_RECEIPT æ—¶æŒ‚出(由 MessageItem æŠŠå…³ï¼‰
    - å·²è¯»åå•按需拉:popover è§¦å‘ show æ‰æ‰“ /im/message/group/get-read-users,避免每条群消息都预拉
    - æœªè¯»åå• = å½“前群成员 âˆ’ è‡ªå·± âˆ’ å·²é€€ç¾¤ âˆ’ å·²è¯»é›†åˆï¼ˆå‰ç«¯èšåˆï¼ŒåŽç«¯åªå›žå·²è¯» userId)
  -->
  <Popover
    v-model:open="popVisible"
    placement="left"
    trigger="click"
    :overlay-style="{ width: '320px' }"
    @open-change="(open) => open && loadReadUsers()"
  >
    <span
      class="text-12px whitespace-nowrap cursor-pointer text-[var(--ant-color-text-secondary)] hover:text-[#409eff]"
    >
      {{ label }}
    </span>
    <template #content>
      <Tabs v-model:active-key="activeTab" centered>
        <TabPane :tab="`已读(${readMembers.length})`" key="read">
          <PagedScroller :items="readMembers" :page-size="20" item-key="userId" class="h-75">
            <template #default="{ item }">
              <GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
            </template>
          </PagedScroller>
          <div
            v-if="readMembers.length === 0"
            class="py-5 text-12px text-center text-[var(--ant-color-text-disabled)]"
          >
            {{ groupMembers.length === 0 ? '群成员未加载' : '暂无已读' }}
          </div>
        </TabPane>
        <TabPane :tab="`未读(${unreadMembers.length})`" key="unread">
          <PagedScroller :items="unreadMembers" :page-size="20" item-key="userId" class="h-75">
            <template #default="{ item }">
              <GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
            </template>
          </PagedScroller>
          <div
            v-if="unreadMembers.length === 0"
            class="py-5 text-12px text-center text-[var(--ant-color-text-disabled)]"
          >
            {{ groupMembers.length === 0 ? '群成员未加载' : '全部已读' }}
          </div>
        </TabPane>
      </Tabs>
    </template>
  </Popover>
</template>