gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<script lang="ts" setup>
import type { Friend, GroupLite, GroupMember } from '../../types'
import type { GroupMemberLite } from './group-member.vue'
 
import { computed, ref, watch } from 'vue'
 
import { CommonStatusEnum } from '#/packages/constants/src'
 
import { Button } from 'ant-design-vue'
 
import { getCurrentUserId } from '#/views/im/utils/auth'
 
import { getMemberDisplayName, isGroupQuit } from '../../../utils/user'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import GroupAvatar from './group-avatar.vue'
import GroupMemberGrid from './group-member-grid.vue'
 
defineOptions({ name: 'ImGroupInfo' })
 
const props = defineProps<{
  group: GroupLite
}>()
 
const emit = defineEmits<{
  /** stranger 点「加入群聊」;父级负责弹申请理由 + 调 applyJoinGroup */
  apply: [group: GroupLite]
  /** member 点「进入群聊」;父级负责切会话 + 关浮层 */
  chat: [group: GroupLite]
}>()
 
const groupStore = useGroupStore()
const friendStore = useFriendStore()
 
const members = ref<GroupMemberLite[]>([])
 
/**
 * 是否已加群:基于"自己确实在成员列表里"判断
 * - 缓存未命中:直接 false(陌生群)
 * - 命中且 members 已拉:精准查 self.userId 在不在
 * - 命中但 members 未拉:fetchGroupList 接口语义即「我加入的群」,命中视为 member(拉成员后会自动收敛)
 */
const isMember = computed(() => {
  if (!props.group?.id) {
    return false
  }
  const cached = groupStore.getGroup(props.group.id)
  if (!cached) {
    return false
  }
  // 历史退群群:直接判 false,避免成员未加载时误显示「进入群聊」
  if (isGroupQuit(cached)) {
    return false
  }
  if (cached.membersLoaded && cached.members) {
    const myId = getCurrentUserId()
    return cached.members.some((m) => m.userId === myId && m.status === CommonStatusEnum.ENABLE)
  }
  return true
})
/** 历史退群群:只读,动作区两个按钮都不渲染(既不「进入群聊」也不「加入群聊」) */
const isQuitGroup = computed(() => {
  const id = props.group?.id
  return id != null && isGroupQuit(groupStore.getGroup(id))
})
/** 是否未加群:有 id、非成员、且非历史退群群;只有真·陌生人才给「加入群聊」 */
const isStranger = computed(() => !!props.group?.id && !isMember.value && !isQuitGroup.value)
 
/** 成员数文案:member 优先用本地拉到的列表长度,stranger 用 props.group.memberCount 卡片快照 */
const memberCountText = computed(() => {
  const count = isMember.value
    ? props.group.memberCount || members.value.length
    : props.group.memberCount
  return count ? `${count} 位成员` : ''
})
 
/** member 切群 / 首挂:拉成员;竞态用 group.id 比对丢弃陈旧响应避免上一条群成员错位 */
watch(
  () => [props.group?.id, isMember.value] as const,
  async ([id, member]) => {
    members.value = []
    if (!id || !member) {
      return
    }
    const list = await groupStore.fetchGroupMemberList(id, true)
    if (props.group?.id !== id) {
      return
    }
    members.value = list.map((m) => convertGroupMemberLite(m, friendStore.getFriend(m.userId)))
  },
  { immediate: true }
)
 
/** 群成员 → 列表项 */
function convertGroupMemberLite(member: GroupMember, friend: Friend | undefined): GroupMemberLite {
  return {
    userId: member.userId,
    showName: getMemberDisplayName(member, friend),
    nickname: member.nickname,
    avatar: member.avatar,
    status: member.status,
    role: member.role
  }
}
</script>
 
<template>
  <!--
    群信息内容组件(与 UserInfo 对位)
    - 头像 + 群名 + 成员数 + 成员宫格 + 动作区,纯展示 + 抛事件,业务由父级承接
    - relation 走 groupStore 缓存推导:命中 = member(已加群),否则 = stranger(未加群),无 id = readonly
    - 成员宫格仅 member 时拉取(陌生群拉不到,所有信息走 props.group 卡片快照)
  -->
  <div class="flex justify-center">
    <div class="w-full max-w-[320px] flex flex-col gap-3 items-center">
      <GroupAvatar
        :group-id="group.id"
        :url="group.showImage || group.showImageThumb"
        :name="group.showGroupName || group.name"
        :size="72"
        :previewable="isMember"
      />
      <div
        class="w-full text-lg font-semibold leading-snug text-[var(--ant-color-text)] truncate text-center"
      >
        {{ group.showGroupName || group.name }}
      </div>
      <div v-if="memberCountText" class="text-13px text-[var(--ant-color-text-secondary)]">
        {{ memberCountText }}
      </div>
 
      <!-- 成员宫格:仅 member 渲染(陌生群拉不到成员) -->
      <div
        v-if="isMember && members.length > 0"
        class="flex flex-wrap gap-2 justify-center w-full pt-2"
      >
        <GroupMemberGrid
          v-for="member in members"
          :key="member.userId"
          :member="member"
          :group-name="group.name"
        />
      </div>
 
      <!-- 动作区:member 进入群聊 / stranger 加入群聊 / readonly 不渲染 -->
      <div v-if="isMember" class="mt-4">
        <Button type="primary" @click="emit('chat', group)">进入群聊</Button>
      </div>
      <div v-else-if="isStranger" class="mt-4">
        <Button type="primary" @click="emit('apply', group)">加入群聊</Button>
      </div>
    </div>
  </div>
</template>