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
<script lang="ts" setup>
import type { FriendLite } from '../../types'
 
import { computed, ref } from 'vue'
 
import { Button, message, Modal } from 'ant-design-vue'
 
import { createGroup } from '#/api/im/group'
 
import { buildDefaultGroupName } from '../../../utils/group'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import { FriendPickerPanel } from '../picker'
 
defineOptions({ name: 'ImGroupCreateDialog' })
 
const emit = defineEmits<{
  /** 创建成功,携带新群编号;父侧通常用来跳转到新群会话 */
  created: [groupId: number]
}>()
 
const friendStore = useFriendStore()
const groupStore = useGroupStore()
 
const visible = ref(false)
const submitting = ref(false)
const lockedIds = ref<number[]>([])
const selectedIds = ref<number[]>([])
 
defineExpose({
  /** 打开发起群聊弹窗:reset → 灌参 → visible=true */
  open(opts?: { lockedIds?: number[] }) {
    lockedIds.value = opts?.lockedIds ? [...opts.lockedIds] : []
    selectedIds.value = []
    submitting.value = false
    visible.value = true
  }
})
 
/** 全量好友:直接复用 friendStore Lite 视图(带拼音字段供分桶用) */
const friends = computed<FriendLite[]>(() => friendStore.getActiveFriendLiteList)
 
/** 完成按钮可点:至少有 1 个非 locked 勾选(locked 是入口锁定项,不算"用户主动选择") */
const canSubmit = computed(() => selectedIds.value.length > 0)
 
/** 拿到所有要进群的好友(locked + selected);建群默认群名按这批人生成 */
function resolveMembersToInvite(): FriendLite[] {
  const seen = new Set<number>()
  const result: FriendLite[] = []
  const byId = new Map(friends.value.map((f) => [f.id, f]))
  for (const id of lockedIds.value) {
    if (seen.has(id)) {
      continue
    }
    const friend = byId.get(id)
    if (friend) {
      seen.add(id)
      result.push(friend)
    }
  }
  for (const id of selectedIds.value) {
    if (seen.has(id)) {
      continue
    }
    const friend = byId.get(id)
    if (friend) {
      seen.add(id)
      result.push(friend)
    }
  }
  return result
}
 
/** 创建群聊:建群(同时邀请初始成员)→ upsert groupStore → emit created 让父页跳转新会话 */
async function handleOk() {
  const members = resolveMembersToInvite()
  if (members.length === 0) {
    return
  }
  submitting.value = true
  try {
    const memberUserIds = members.map((m) => m.id)
    const name = buildDefaultGroupName(members)
    const group = await createGroup({ name, memberUserIds, joinApproval: false })
    if (!group?.id) {
      throw new Error('创建群失败:未返回群编号')
    }
    // 直接 upsert 进 groupStore,省一次 fetchGroupList —— 服务端返回 VO 已经够建会话了
    groupStore.upsertGroup({
      id: group.id,
      name: group.name,
      avatar: group.avatar,
      notice: group.notice,
      ownerUserId: group.ownerUserId
    })
    message.success('群聊创建成功')
    emit('created', group.id)
    visible.value = false
  } finally {
    submitting.value = false
  }
}
</script>
 
<template>
  <!--
    发起群聊:选好友 → 默认按所选成员名生成群名 → createGroup
    - dialog 壳本组件持有;选择 UI 委托 FriendPickerPanel
    - lockedIds 由调用方通过 open({ lockedIds }) 传入;私聊侧 +建群锁定对方
    - 不再要求先输入群名 / 不再展示「进群审批」开关,对齐微信 PC
    - 对外接口:ref + open({ lockedIds }) + emit created(groupId)
  -->
  <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"
        :locked-ids="lockedIds"
      />
    </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>