gaoluyang
2 天以前 b64a0deae5b5d33f9e20671a68936b27f0b9b00b
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
<script lang="ts" setup>
import type { GroupMemberLite } from '../../../../components/group'
import type { Conversation, GroupLite } from '../../../../types'
 
import { computed, ref, watch } from 'vue'
 
import { confirm } from '#/packages/effects/common-ui/src'
import { CommonStatusEnum } from '#/packages/constants/src'
import { IconifyIcon as Icon } from '#/packages/icons/src'
 
import { Button, Drawer, Input, message, Popover, Switch } from 'ant-design-vue'
 
import {
  dissolveGroup,
  muteAll,
  updateGroup
} from '#/api/im/group'
import { quitGroup, updateGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
import { toGroupCardTarget } from '#/views/im/utils/message'
import { isGroupQuit } from '#/views/im/utils/user'
 
import { GroupAdminSetDialog } from '../../../../components/group'
import { GroupMemberAddDialog } from '../../../../components/group'
import { GroupMemberGrid } from '../../../../components/group'
import { GroupMemberRemoveDialog } from '../../../../components/group'
import { GroupOwnerTransferDialog } from '../../../../components/group'
import { GroupRequestListDialog } from '../../../../components/group'
import { RecommendCardDialog } from '../../../../components/user'
import { useConversationStore } from '../../../../store/conversationStore'
import { useGroupStore } from '../../../../store/groupStore'
 
defineOptions({ name: 'ImConversationGroupSide' })
 
// 大群默认只展示前 N 个成员(4×4 宫格 − 2 个瓦片预留给"添加 / 移出"按钮)
 
const props = withDefaults(
  defineProps<{
    conversation?: Conversation | null // 当前会话:用于读 / 切免打扰、置顶状态
    group?: GroupLite & { groupRemark?: string; notice?: string; remarkNickName?: string; } // 当前群信息(可空:无激活群会话时)
    members?: GroupMemberLite[]
    modelValue?: boolean // 抽屉是否打开(v-model)
  }>(),
  {
    conversation: null,
    group: undefined,
    modelValue: false,
    members: () => []
  }
)
 
const emit = defineEmits<{
  openHistory: [] // 点击 "查找聊天内容" 行 → 父组件打开 MessageHistory 弹窗
  reload: [friendIds?: number[]] // 邀请 / 移除 / 修改群资料后,父组件重新拉群数据;邀请新成员场景透出 friendIds 让上层可选精准刷新
  'update:modelValue': [value: boolean]
}>()
 
const MEMBER_PREVIEW_COUNT = 14
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
 
const visible = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v)
})
 
// ==================== 角色 / 成员展示 ====================
 
const searchText = ref('')
const showAllMembers = ref(false)
 
const inviteDialogRef = ref<InstanceType<typeof GroupMemberAddDialog>>() // 邀请好友入群弹窗 ref:handleOpenInvite 调 open({ groupId }) 打开
/** 打开邀请好友入群弹窗 */
function handleOpenInvite() {
  if (!props.group?.id) {
    return
  }
  inviteDialogRef.value?.open({ groupId: props.group.id })
}
 
const myId = computed(() => getCurrentUserId())
/** 历史退群群:禁所有群操作入口(邀请 / 移出 / 改资料 / 禁言 / 审批 / 退出等),只保留展示;props.group 是 GroupLite 无 joinStatus,回 store 取全量 */
const isQuitGroup = computed(() => {
  const id = props.group?.id
  return id != null && isGroupQuit(groupStore.getGroup(id))
})
const isOwner = computed(
  () => !isQuitGroup.value && props.group != null && props.group.ownerId === myId.value
)
/** 当前用户在群里的角色(来自 props.members 的 me 行);用于判定是否可移出他人 */
const myRole = computed(() => props.members.find((m) => m.userId === myId.value)?.role)
/** 群主或管理员:在抽屉里有 "移出群成员" 入口;历史退群群一律视为无权限 */
const isOwnerOrAdmin = computed(
  () =>
    !isQuitGroup.value &&
    (myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN)
)
 
/** 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定) */
const visibleMembers = computed(() => {
  return props.members
    .filter(
      (member) =>
        member.status !== CommonStatusEnum.DISABLE &&
        (member.showName || '').includes(searchText.value)
    )
    .toSorted((a, b) => {
      const roleA = a.role ?? ImGroupMemberRole.NORMAL
      const roleB = b.role ?? ImGroupMemberRole.NORMAL
      return roleA === roleB ? a.userId - b.userId : roleA - roleB
    })
})
 
/** 折叠规则:搜索 / 已展开 时不折叠,其余只取前 N 个 */
const moreMembersHidden = computed(
  () =>
    !searchText.value && !showAllMembers.value && visibleMembers.value.length > MEMBER_PREVIEW_COUNT
)
const displayMembers = computed(() =>
  moreMembersHidden.value
    ? visibleMembers.value.slice(0, MEMBER_PREVIEW_COUNT)
    : visibleMembers.value
)
 
// 抽屉关闭时清掉成员区临时态(搜索关键字、查看更多展开)
watch(visible, (v) => {
  if (!v) {
    searchText.value = ''
    showAllMembers.value = false
  }
})
 
// ==================== 群信息编辑 ====================
 
const namePopoverVisible = ref(false)
const noticePopoverVisible = ref(false)
const remarkPopoverVisible = ref(false)
const groupRemarkPopoverVisible = ref(false)
const editName = ref('')
const editNotice = ref('')
const editRemark = ref('')
const editGroupRemark = ref('')
 
// popover 弹出时把当前值灌进编辑态,避免上次未保存的脏值
watch(namePopoverVisible, (v) => {
  if (v) editName.value = props.group?.name || ''
})
watch(noticePopoverVisible, (v) => {
  if (v) editNotice.value = props.group?.notice || ''
})
watch(remarkPopoverVisible, (v) => {
  if (v) editRemark.value = props.group?.remarkNickName || ''
})
watch(groupRemarkPopoverVisible, (v) => {
  if (v) editGroupRemark.value = props.group?.groupRemark || ''
})
 
// 抽屉关闭时清掉所有 popover,避免下次打开仍弹着
watch(visible, (v) => {
  if (!v) {
    namePopoverVisible.value = false
    noticePopoverVisible.value = false
    remarkPopoverVisible.value = false
    groupRemarkPopoverVisible.value = false
  }
})
 
/** 群主:保存群名(走 /im/group/update);trim 后空字符串拒提交,与 saveGroupRemark 行为对齐 */
async function saveName() {
  if (!props.group) {
    return
  }
  const trimmed = editName.value.trim()
  if (!trimmed) {
    message.warning('群名称不能为空')
    return
  }
  await updateGroup({ id: props.group.id, name: trimmed })
  namePopoverVisible.value = false
  message.success('保存成功')
  emit('reload')
}
 
/** 群主:保存群公告 */
async function saveNotice() {
  if (!props.group) {
    return
  }
  await updateGroup({ id: props.group.id, notice: editNotice.value })
  noticePopoverVisible.value = false
  message.success('保存成功')
  emit('reload')
}
 
/** 群主:切换「进群审批」开关;开启后所有「申请」「邀请」路径都需群主 / 管理员同意 */
async function handleJoinApprovalChange(value: boolean | number | string) {
  if (!props.group) {
    return
  }
  await updateGroup({ id: props.group.id, joinApproval: !!value })
  message.success('保存成功')
  emit('reload')
}
 
/** 任何成员:保存群备注(仅自己可见,会替换会话列表 / 顶部群名展示) */
async function saveGroupRemark() {
  if (!props.group) {
    return
  }
  await updateGroupMember({
    groupId: props.group.id,
    groupRemark: editGroupRemark.value.trim()
  })
  groupRemarkPopoverVisible.value = false
  message.success('保存成功')
  emit('reload')
}
 
/** 任何成员:保存自己在群里的昵称(走 /im/group-member/update) */
async function saveRemark() {
  if (!props.group) {
    return
  }
  await updateGroupMember({
    groupId: props.group.id,
    displayUserName: editRemark.value
  })
  remarkPopoverVisible.value = false
  message.success('保存成功')
  emit('reload')
}
 
// ==================== 开关切换 ====================
 
/**
 * 消息免打扰:本地 conversationStore 立即切;后端 /silent 异步同步,失败回滚本地
 *
 * 与 ConversationItem 右键菜单的"消息免打扰"语义一致;区别仅在 UI 入口
 */
function onMutedChange(value: boolean | number | string) {
  if (!props.conversation) {
    return
  }
  const next = !!value
  const { type, targetId } = props.conversation
  conversationStore.setConversationSilent(type, targetId, next)
  groupStore.setGroupSilent(targetId, next).catch((error) => {
    console.error('[IM ConversationGroupSide] setGroupSilent 失败', { targetId }, error)
    conversationStore.setConversationSilent(type, targetId, !next)
  })
}
 
/** 置顶聊天:纯本地 conversationStore 排序态(无后端字段) */
function onTopChange(value: boolean | number | string) {
  if (!props.conversation) {
    return
  }
  conversationStore.setConversationTop(props.conversation.type, props.conversation.targetId, !!value)
}
 
// ==================== 全群禁言 ====================
 
/** 当前群是否全群禁言 */
const currentMutedAll = computed(() => {
  if (!props.group) {
    return false
  }
  return groupStore.getGroup(props.group.id)?.mutedAll ?? false
})
 
/** 全群禁言开关切换 */
async function onMuteAllChange(value: boolean | number | string) {
  if (!props.group) {
    return
  }
  const newValue = !!value
  await muteAll({ id: props.group.id, mutedAll: newValue })
  message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
  emit('reload')
}
 
// ==================== 进群审批 ====================
 
const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>() // 进群申请列表弹窗 ref:handleOpenRequestList 调 open({ groupId }) 触发
 
/** 打开当前群的进群申请列表 */
function handleOpenRequestList() {
  if (!props.group?.id) {
    return
  }
  requestListDialogRef.value?.open({ groupId: props.group.id })
}
 
// ==================== 分享群名片 ====================
 
const recommendCardDialogRef = ref<InstanceType<typeof RecommendCardDialog>>() // 分享群名片弹窗 ref:handleShareGroupCard 调用 open({ target }) 打开
 
/** 分享群名片:把当前群作为名片消息推荐给其他会话 */
function handleShareGroupCard() {
  const target = toGroupCardTarget(props.group)
  if (!target) {
    return
  }
  recommendCardDialogRef.value?.open({ target })
}
 
// ==================== 退出群聊 ====================
 
/** 退出群聊(普通成员入口;群主退出走"解散群"是另一条路径,这里不处理) */
async function handleQuit() {
  if (!props.group) {
    return
  }
  // 二次确认
  try {
    await confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出')
  } catch {
    return
  }
  const groupId = props.group.id
  await quitGroup(groupId)
  // 本地立即响应:先把 self.member 置 DISABLE(让 GroupInfo 等 isMember 收敛),再清会话 + 群 store
  if (myId.value) {
    groupStore.updateGroupMemberStatus(groupId, myId.value, CommonStatusEnum.DISABLE)
  }
  conversationStore.removeConversation(ImConversationType.GROUP, groupId)
  groupStore.removeGroup(groupId)
  message.success('已退出群聊')
  visible.value = false
}
 
/** 解散群聊(仅群主入口) */
async function handleDissolve() {
  if (!props.group) {
    return
  }
  try {
    await confirm('解散后所有成员将被移出,且无法恢复,确认解散吗?', '确认解散')
  } catch {
    return
  }
  const groupId = props.group.id
  await dissolveGroup(groupId)
  conversationStore.removeConversation(ImConversationType.GROUP, groupId)
  groupStore.removeGroup(groupId)
  message.success('群聊已解散')
  visible.value = false
}
 
// ==================== 群主操作 ====================
// 移除群成员(群主 / 管理员可见)+ 设置群管理员(仅群主)+ 群主管理权转让(仅群主)
 
const removeDialogRef = ref<InstanceType<typeof GroupMemberRemoveDialog>>() // 移除群成员弹窗 ref
const adminSetDialogRef = ref<InstanceType<typeof GroupAdminSetDialog>>() // 设置群管理员弹窗 ref
const ownerTransferDialogRef = ref<InstanceType<typeof GroupOwnerTransferDialog>>() // 转让群主弹窗 ref
 
// ---------- 移除群成员 ----------
 
/** 打开移除群成员弹窗:始终隐藏群主;管理员视角额外隐藏其它管理员(管理员不能移出管理员) */
function handleOpenRemove() {
  if (!props.group?.id) {
    return
  }
  const hideIds: number[] = []
  if (props.group.ownerId) {
    hideIds.push(props.group.ownerId)
  }
  if (myRole.value === ImGroupMemberRole.ADMIN) {
    props.members
      .filter((m) => m.role === ImGroupMemberRole.ADMIN)
      .forEach((m) => hideIds.push(m.userId))
  }
  removeDialogRef.value?.open({
    groupId: props.group.id,
    members: props.members,
    hideIds
  })
}
 
// ---------- 设置群管理员 ----------
 
/** 打开设置群管理员弹窗:当前管理员默认勾选;群主从候选里隐藏 */
function handleOpenAdminSet() {
  if (!props.group?.id) {
    return
  }
  // 过滤已退群成员,避免 maxSize 名额被隐藏成员占用导致无法新增管理员
  const currentAdminIds = props.members
    .filter(
      (member) =>
        member.role === ImGroupMemberRole.ADMIN && member.status !== CommonStatusEnum.DISABLE
    )
    .map((member) => member.userId)
  const hideIds = props.group.ownerId ? [props.group.ownerId] : []
  adminSetDialogRef.value?.open({
    groupId: props.group.id,
    members: props.members,
    currentAdminIds,
    hideIds
  })
}
 
// ---------- 群主管理权转让 ----------
 
/** 打开转让群主弹窗:当前用户从候选里隐藏(不能转给自己) */
function handleOpenTransferOwner() {
  if (!props.group?.id) {
    return
  }
  ownerTransferDialogRef.value?.open({
    groupId: props.group.id,
    members: props.members,
    hideIds: [myId.value]
  })
}
</script>
 
<template>
  <!-- 聊天面板右侧群信息抽屉:成员宫格 + 群信息 + 开关 + 退出群聊,整体对齐微信 PC -->
  <Drawer
    v-model:open="visible"
    :closable="false"
    placement="right"
    width="380px"
    root-class-name="im-conversation-group-side__modal"
  >
    <div v-if="group" class="flex flex-col h-full bg-[var(--ant-color-bg-container)]">
      <!-- 上部:可滚动内容区 -->
      <div class="flex-1 overflow-y-auto bg-[var(--ant-color-fill-secondary)]">
        <!-- ==================== 群成员区 ==================== -->
        <div class="px-4 pt-4 pb-[10px] bg-[var(--ant-color-bg-container)]">
          <Input v-model:value="searchText" placeholder="搜索群成员" allow-clear>
            <template #prefix>
              <Icon
                icon="ant-design:search-outlined"
                class="text-[var(--ant-color-text-placeholder)]"
              />
            </template>
          </Input>
 
          <div class="flex flex-wrap gap-x-1 gap-y-[14px] mt-[14px]">
            <GroupMemberGrid
              v-for="member in displayMembers"
              :key="member.userId"
              :member="member"
              :size="50"
              clickable
              :group-name="group.name"
            />
 
            <!-- 添加(任何成员都能邀请;历史退群群隐藏) -->
            <div
              v-if="!isQuitGroup"
              class="im-conversation-group-side__tile-wrap flex flex-col items-center w-[66px] cursor-pointer"
              title="邀请好友入群"
              @click="handleOpenInvite"
            >
              <div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-tertiary)] border border-dashed border-[var(--ant-color-border)] rounded-md transition-colors duration-200">
                <Icon icon="ant-design:plus-outlined" />
              </div>
              <div class="mt-1.5 text-12px leading-[1.5] text-[var(--ant-color-text)] text-center">添加</div>
            </div>
 
            <!-- 移出(群主或管理员;管理员只能移出普通成员,由后端校验) -->
            <div
              v-if="isOwnerOrAdmin"
              class="im-conversation-group-side__tile-wrap flex flex-col items-center w-[66px] cursor-pointer"
              title="移出群成员"
              @click="handleOpenRemove"
            >
              <div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-tertiary)] border border-dashed border-[var(--ant-color-border)] rounded-md transition-colors duration-200">
                <Icon icon="ant-design:minus-outlined" />
              </div>
              <div class="mt-1.5 text-12px leading-[1.5] text-[var(--ant-color-text)] text-center">移出</div>
            </div>
          </div>
 
          <!-- 大群折叠:默认只展示前 N 个,点 "查看更多" 全展开(搜索时不折叠) -->
          <div
            v-if="moreMembersHidden"
            class="flex items-center justify-center gap-1 mt-[10px] pt-1.5 pb-0.5 text-12px text-[var(--ant-color-text-secondary)] cursor-pointer transition-colors duration-150 hover:text-[var(--ant-color-primary)]"
            @click="showAllMembers = true"
          >
            查看更多
            <Icon icon="ant-design:down-outlined" :size="10" />
          </div>
        </div>
 
        <div class="flex-shrink-0 h-[10px]"></div>
 
        <!-- ==================== 群信息 ==================== -->
        <!-- label 在上、value 在下,纵向堆叠(对齐微信 PC 设计);只有 "群公告" 因为内容长加 > chevron -->
        <div class="bg-[var(--ant-color-bg-container)]">
          <!-- 群聊名称(群主可改) -->
          <Popover
            v-if="isOwner"
            v-model:open="namePopoverVisible"
            trigger="click"
            placement="leftTop"
            :overlay-style="{ width: '280px' }"
          >
            <div
              class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群聊名称</span>
              <span class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] truncate">{{ group.name }}</span>
            </div>
            <template #content>
              <div class="flex flex-col gap-2">
                <Input v-model:value="editName" :maxlength="20" placeholder="请输入群聊名称" />
                <div class="flex justify-end gap-2">
                  <Button size="small" @click="namePopoverVisible = false">取消</Button>
                  <Button size="small" type="primary" @click="saveName">保存</Button>
                </div>
              </div>
            </template>
          </Popover>
          <div
            v-else
            class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 transition-colors duration-150"
          >
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群聊名称</span>
            <span class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] truncate">{{ group.name }}</span>
          </div>
 
          <!-- 群公告(群主可改):内容可能很长,加 > chevron 表示可展开编辑 -->
          <Popover
            v-if="isOwner"
            v-model:open="noticePopoverVisible"
            trigger="click"
            placement="leftTop"
            :overlay-style="{ width: '320px' }"
          >
            <div
              class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            >
              <div class="flex items-center justify-between gap-2">
                <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群公告</span>
                <Icon
                  icon="ant-design:right-outlined"
                  :size="11"
                  class="text-[var(--ant-color-text-placeholder)]"
                />
              </div>
              <span
                v-if="group.notice"
                class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] line-clamp-2"
              >
                {{ group.notice }}
              </span>
              <span v-else class="text-13px text-[var(--ant-color-text-placeholder)] leading-[1.6]">未设置</span>
            </div>
            <template #content>
              <div class="flex flex-col gap-2">
                <Input.TextArea
                  v-model:value="editNotice"
                  :rows="4"
                  :maxlength="1024"
                  show-count
                  placeholder="请输入群公告"
                />
                <div class="flex justify-end gap-2">
                  <Button size="small" @click="noticePopoverVisible = false">取消</Button>
                  <Button size="small" type="primary" @click="saveNotice">保存</Button>
                </div>
              </div>
            </template>
          </Popover>
          <div
            v-else
            class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 transition-colors duration-150"
          >
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群公告</span>
            <span
              v-if="group.notice"
              class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] line-clamp-2"
            >
              {{ group.notice }}
            </span>
            <span v-else class="text-13px text-[var(--ant-color-text-placeholder)] leading-[1.6]">未设置</span>
          </div>
 
          <!-- 备注(仅自己可见;保存后会替换会话列表 / 顶部群名展示);历史退群群隐藏:改备注走 updateGroupMember,已退群会被后端拒 -->
          <Popover
            v-if="!isQuitGroup"
            v-model:open="groupRemarkPopoverVisible"
            trigger="click"
            placement="leftTop"
            :overlay-style="{ width: '280px' }"
          >
            <div
              class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">备注</span>
              <span
                v-if="group.groupRemark"
                class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] line-clamp-2"
              >
                {{ group.groupRemark }}
              </span>
              <span v-else class="text-13px text-[var(--ant-color-text-placeholder)] leading-[1.6]">
                群聊的备注仅自己可见
              </span>
            </div>
            <template #content>
              <div class="flex flex-col gap-2">
                <Input.TextArea
                  v-model:value="editGroupRemark"
                  :rows="3"
                  :maxlength="64"
                  show-count
                  placeholder="仅自己可见"
                />
                <div class="flex justify-end gap-2">
                  <Button size="small" @click="groupRemarkPopoverVisible = false">取消</Button>
                  <Button size="small" type="primary" @click="saveGroupRemark">保存</Button>
                </div>
              </div>
            </template>
          </Popover>
 
          <!-- 我在本群的昵称(任何成员都能改自己的);历史退群群隐藏:走 updateGroupMember,已退群会被后端拒 -->
          <Popover
            v-if="!isQuitGroup"
            v-model:open="remarkPopoverVisible"
            trigger="click"
            placement="leftTop"
            :overlay-style="{ width: '280px' }"
          >
            <div
              class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">我在本群的昵称</span>
              <span
                v-if="group.remarkNickName"
                class="text-13px text-[var(--ant-color-text)] break-all leading-[1.6] truncate"
              >
                {{ group.remarkNickName }}
              </span>
              <span v-else class="text-13px text-[var(--ant-color-text-placeholder)] leading-[1.6]">点击设置</span>
            </div>
            <template #content>
              <div class="flex flex-col gap-2">
                <Input v-model:value="editRemark" :maxlength="20" placeholder="请输入本群昵称" />
                <div class="flex justify-end gap-2">
                  <Button size="small" @click="remarkPopoverVisible = false">取消</Button>
                  <Button size="small" type="primary" @click="saveRemark">保存</Button>
                </div>
              </div>
            </template>
          </Popover>
        </div>
 
        <div class="flex-shrink-0 h-[10px]"></div>
 
        <!-- ==================== 查找聊天内容 ==================== -->
        <!-- 点击 → 父组件打开 MessageHistory 弹窗 -->
 
        <div class="flex-shrink-0 h-[10px]"></div>
 
        <div class="bg-[var(--ant-color-bg-container)]">
          <div
            class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            @click="emit('openHistory')"
          >
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">查找聊天内容</span>
            <Icon
              icon="ant-design:right-outlined"
              :size="11"
              class="text-[var(--ant-color-text-placeholder)]"
            />
          </div>
          <!-- 分享群名片:弹 RecommendCardDialog,把当前群作为名片消息推荐给其他会话 -->
          <div
            v-if="group"
            class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
            @click="handleShareGroupCard"
          >
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">分享群名片</span>
            <Icon
              icon="ant-design:right-outlined"
              :size="11"
              class="text-[var(--ant-color-text-placeholder)]"
            />
          </div>
        </div>
 
        <div class="flex-shrink-0 h-[10px]"></div>
 
        <!-- ==================== 开关项 ==================== -->
        <div class="bg-[var(--ant-color-bg-container)]">
          <div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">消息免打扰</span>
            <Switch :checked="!!conversation?.silent" @change="onMutedChange" />
          </div>
          <div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">置顶聊天</span>
            <Switch :checked="!!conversation?.top" @change="onTopChange" />
          </div>
          <!-- 全群禁言:仅群主或管理员可操作 -->
          <div v-if="isOwnerOrAdmin" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">全群禁言</span>
            <Switch :checked="!!currentMutedAll" @change="onMuteAllChange" />
          </div>
        </div>
 
        <!-- ==================== 进群审批 ==================== -->
        <!-- 单独一段:群主开关 + 紧跟「- 进群申请」子项;与微信群管理布局对齐 -->
        <template v-if="isOwner || (isOwnerOrAdmin && !!group.joinApproval)">
          <div class="flex-shrink-0 h-[10px]"></div>
          <div class="bg-[var(--ant-color-bg-container)]">
            <!-- 进群审批:仅群主可操作;开启后普通成员的「申请」「邀请」路径都需群主 / 管理员同意;群主 / 管理员邀请直进 -->
            <div v-if="isOwner" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">进群需要群主 / 群管理确认</span>
              <Switch :checked="!!group.joinApproval" @change="handleJoinApprovalChange" />
            </div>
            <!-- 进群申请子项:仅当开启审批 + 当前用户是 owner / admin 时出现;点击进列表 dialog -->
            <div
              v-if="isOwnerOrAdmin && !!group.joinApproval"
              class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
              @click="handleOpenRequestList"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">- 进群申请</span>
              <Icon
                icon="ant-design:right-outlined"
                :size="11"
                class="text-[var(--ant-color-text-placeholder)]"
              />
            </div>
          </div>
        </template>
 
        <!-- ==================== 群主操作 ==================== -->
        <!-- 仅群主可见,含管理员设置 + 群主管理权转让 -->
        <template v-if="isOwner">
          <div class="flex-shrink-0 h-[10px]"></div>
          <div class="bg-[var(--ant-color-bg-container)]">
            <div
              class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
              @click="handleOpenAdminSet"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群管理员</span>
              <Icon
                icon="ant-design:right-outlined"
                :size="11"
                class="text-[var(--ant-color-text-placeholder)]"
              />
            </div>
            <div
              class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
              @click="handleOpenTransferOwner"
            >
              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">群主管理权转让</span>
              <Icon
                icon="ant-design:right-outlined"
                :size="11"
                class="text-[var(--ant-color-text-placeholder)]"
              />
            </div>
          </div>
        </template>
      </div>
 
      <!-- ==================== 底部:退出 / 解散群聊(历史退群群隐藏,已退群无需再退) ==================== -->
      <div
        v-if="!isQuitGroup"
        class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--ant-color-bg-container)] border-t border-t-solid border-[var(--im-border-color-lighter)]"
      >
        <!-- 群主:解散群聊 -->
        <Button
          v-if="isOwner"
          class="w-full !h-9 text-14px"
          danger
          @click="handleDissolve"
        >
          解散群聊
        </Button>
        <!-- 非群主:退出群聊 -->
        <Button
          v-else
          class="w-full !h-9 text-14px"
          danger
          @click="handleQuit"
        >
          退出群聊
        </Button>
      </div>
    </div>
 
    <!-- ==================== 子对话框 ==================== -->
    <!-- 邀请新成员 / 选成员移除 -->
    <GroupMemberAddDialog ref="inviteDialogRef" @reload="(ids) => $emit('reload', ids)" />
    <GroupMemberRemoveDialog ref="removeDialogRef" @reload="$emit('reload')" />
 
    <!-- 群主操作:管理员设置(一个弹窗合并增 / 删,提交时 diff)+ 群主管理权转让 -->
    <GroupAdminSetDialog ref="adminSetDialogRef" @reload="$emit('reload')" />
    <GroupOwnerTransferDialog ref="ownerTransferDialogRef" @reload="$emit('reload')" />
 
    <!-- 进群申请列表(仅当开启审批 + 当前用户是 owner / admin 时入口可见) -->
    <GroupRequestListDialog ref="requestListDialogRef" />
 
    <!-- 分享群名片:把当前群作为名片消息推荐给其他会话 -->
    <RecommendCardDialog ref="recommendCardDialogRef" />
  </Drawer>
</template>
 
<style scoped>
/* 「添加 / 移出」瓦片:hover 时联动内部 icon-tile 走主色,跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-group-side__tile-wrap:hover .im-conversation-group-side__icon-tile {
  color: var(--ant-color-primary);
  border-color: var(--ant-color-primary);
  background-color: var(--ant-color-primary-bg);
}
 
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) {
  fill: currentColor !important;
}
 
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
.im-conversation-group-side__row + .im-conversation-group-side__row {
  border-top: 1px solid var(--im-border-color-lighter);
}
</style>
 
<!-- AntD Drawer 被传送出当前 scoped 边界,这里靠 root-class-name 压掉默认 padding -->
<style>
.im-conversation-group-side__modal .ant-drawer-body {
  padding: 0;
}
</style>