gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
src/views/im/home/components/group/group-member-add-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,167 @@
<script lang="ts" setup>
import type { GroupMemberLite } from './group-member.vue'
import { computed, ref } from 'vue'
import { CommonStatusEnum } from '#/packages/constants/src'
import { Button, message, Modal } from 'ant-design-vue'
import { inviteGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { GROUP_MAX_MEMBER } from '#/views/im/utils/config'
import { ImGroupMemberRole } from '#/views/im/utils/constants'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import { FriendPickerPanel } from '../picker'
defineOptions({ name: 'ImGroupMemberAddDialog' })
const emit = defineEmits<{
  /** é‚€è¯·æˆåŠŸï¼Œæºå¸¦è¢«é‚€è¯·çš„å¥½å‹ id åˆ—表;父侧通常用来 reload ç¾¤æˆå‘˜ */
  reload: [friendIds: number[]]
}>()
const friendStore = useFriendStore()
const groupStore = useGroupStore()
const visible = ref(false)
const submitting = ref(false)
const groupId = ref(0)
const selectedIds = ref<number[]>([])
defineExpose({
  /** æ‰“开添加群成员弹窗:reset â†’ çŒå‚ â†’ visible=true */
  open(opts: { groupId: number }) {
    groupId.value = opts.groupId
    selectedIds.value = []
    submitting.value = false
    visible.value = true
  }
})
/** å½“前群成员列表:从 groupStore çŽ°å–ï¼Œé¿å…éš groupId å˜åŒ–时父侧 prop æ›´æ–°å»¶è¿Ÿ */
const members = computed<GroupMemberLite[]>(() => {
  const group = groupStore.getGroup(groupId.value)
  return (group?.members || []).map((member) => ({
    userId: member.userId,
    nickname: member.nickname,
    showName: member.displayUserName || member.nickname,
    avatar: member.avatar,
    status: member.status,
    role: member.role
  }))
})
/** å…¨é‡å¥½å‹ï¼šç›´æŽ¥å¤ç”¨ friendStore Lite è§†å›¾ */
const friends = computed(() => friendStore.getActiveFriendLiteList)
/** å·²åœ¨ç¾¤é‡Œçš„好友 id:传给 Panel çš„ disabledIds ç½®ç° + ä¸è®¡å…¥å·²é€‰ */
const disabledIds = computed<number[]>(() =>
  members.value
    .filter((member) => member.status !== CommonStatusEnum.DISABLE)
    .map((member) => member.userId)
)
/** æ˜¯å¦èµ°å®¡æ‰¹åˆ†æ”¯ï¼šç¾¤å¼€å¯ joinApproval + å½“前用户是普通成员(群主 / ç®¡ç†å‘˜é‚€è¯·ç›´è¿›ï¼‰ */
const willGoApproval = computed(() => {
  const group = groupStore.getGroup(groupId.value)
  if (!group?.joinApproval) {
    return false
  }
  const myId = getCurrentUserId()
  if (!myId) {
    return false
  }
  // ç¾¤ä¸»ç›´åˆ¤ï¼Œé¿å¼€ members å¼‚步加载的窗口;admin ä»ä¾èµ– members
  if (group.ownerUserId === myId) {
    return false
  }
  // members æœªåˆ°ä½æ—¶æ— æ³•判定 admin,保守按非审批处理,宁可漏报「等待审批」也不误报给真实管理员
  const myRole = members.value.find((member) => member.userId === myId)?.role
  if (myRole == null) {
    return false
  }
  return myRole !== ImGroupMemberRole.ADMIN
})
/** å½“前群已启用成员数(DISABLE å³é€€ç¾¤ / è¢«è¸¢ä¸è®¡å…¥ï¼‰ï¼Œç”¨äºŽä¸Šé™åˆ¤å®š */
const activeMemberCount = computed(
  () => members.value.filter((member) => member.status !== CommonStatusEnum.DISABLE).length
)
/** é‚€è¯·åŽç¾¤æ€»äººæ•°è‹¥è¶… GROUP_MAX_MEMBER,前端先拦;activeMemberCount + selectedIds.length å³é‚€è¯·åŽçš„æˆå‘˜æ•° */
const willExceedLimit = computed(
  () => activeMemberCount.value + selectedIds.value.length > GROUP_MAX_MEMBER
)
/** æ·»åŠ æŒ‰é’®å¯ç‚¹ï¼šè‡³å°‘æœ‰ 1 ä¸ªæ–°é‚€è¯·çš„好友 + ä¸è¶…群人数上限 */
const canSubmit = computed(() => selectedIds.value.length > 0 && !willExceedLimit.value)
/** é‚€è¯·å…¥ç¾¤ï¼šè°ƒ /im/group/invite,成功后 emit reload è®©çˆ¶ä¾§åˆ·æ–°ç¾¤æˆå‘˜ */
async function handleOk() {
  if (!groupId.value) {
    return
  }
  const memberUserIds = [...selectedIds.value]
  if (memberUserIds.length === 0) {
    return
  }
  // ç¾¤äººæ•°ä¸Šé™å†—余防御:与 canSubmit é‡å¤åˆ¤ä¸€æ¬¡ï¼Œé˜²æ­¢çŠ¶æ€åœ¨ await é—´éš™å˜åŒ–或调用方绕过按钮直接调
  if (activeMemberCount.value + memberUserIds.length > GROUP_MAX_MEMBER) {
    message.warning(`群成员上限为 ${GROUP_MAX_MEMBER} äºº`)
    return
  }
  submitting.value = true
  try {
    await inviteGroupMember({ groupId: groupId.value, memberUserIds })
    // å®¡æ‰¹åˆ†æ”¯ï¼šåŽç«¯ä»…落审批记录,未入群
    message.success(willGoApproval.value ? '邀请已发起,等待群主 / ç®¡ç†å‘˜å®¡æ‰¹' : '邀请成功')
    emit('reload', memberUserIds)
    visible.value = false
  } finally {
    submitting.value = false
  }
}
</script>
<template>
  <!--
    æ·»åŠ ç¾¤æˆå‘˜ï¼šé€‰å¥½å‹é‚€è¯·å…¥ç¾¤ï¼ˆå·²åœ¨ç¾¤æˆå‘˜ç½®ç°ã€ä¸è®¡å…¥å·²é€‰ï¼‰
    - dialog å£³æœ¬ç»„件持有;选择 UI å§”托 FriendPickerPanel
    - å·²åœ¨ç¾¤æˆå‘˜é€šè¿‡ disabledIds ä¼ å…¥ï¼Œä¸å†èµ° checked+disabled çš„"已勾选灰态"
    - å¯¹å¤–接口:ref + open({ groupId }) + emit reload(friendIds)
  -->
  <Modal
    v-model:open="visible"
    title="添加群成员"
    width="720px"
    :mask-closable="false"
    class="im-picker-dialog"
  >
    <div class="h-[480px]">
      <FriendPickerPanel
        v-model:selected-ids="selectedIds"
        :friends="friends"
        :disabled-ids="disabledIds"
      />
    </div>
    <template #footer>
      <Button @click="visible = false">取消</Button>
      <Button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleOk">
        æ·»åŠ 
      </Button>
    </template>
  </Modal>
</template>
<style scoped lang="scss">
@use '../picker/picker-dialog' as picker;
/* :deep ç©¿é€ el-dialog å†…部类;复用 picker å…¬å…± mixin */
.im-picker-dialog {
  @include picker.styles;
}
</style>