gaoluyang
10 分钟以前 51538d466085b6544c35fcf9b9d11de572d74ada
src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,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>