diff --git a/src/views/im/home/components/group/GroupMemberAddDialog.vue b/src/views/im/home/components/group/GroupMemberAddDialog.vue index d40f8a77a..b43904cc5 100644 --- a/src/views/im/home/components/group/GroupMemberAddDialog.vue +++ b/src/views/im/home/components/group/GroupMemberAddDialog.vue @@ -146,4 +146,3 @@ async function handleOk() { @include picker.styles; } - diff --git a/src/views/im/home/pages/conversation/components/message/forward/MessageForwardDialog.vue b/src/views/im/home/pages/conversation/components/message/forward/MessageForwardDialog.vue index d550593e6..b31b36c51 100644 --- a/src/views/im/home/pages/conversation/components/message/forward/MessageForwardDialog.vue +++ b/src/views/im/home/pages/conversation/components/message/forward/MessageForwardDialog.vue @@ -1,22 +1,42 @@ + + + + + {{ headerTitle }} + + + + + @@ -100,7 +120,27 @@ + + + + + + + 取消 + + 创建群聊并发送 + + @@ -109,29 +149,37 @@ import { computed, reactive, ref } from 'vue' import Icon from '@/components/Icon/src/Icon.vue' import { useMessage } from '@/hooks/web/useMessage' +import { createGroup } from '@/api/im/group' import ConversationPickerPanel from '@/views/im/home/components/picker/ConversationPickerPanel.vue' +import FriendPickerPanel from '@/views/im/home/components/picker/FriendPickerPanel.vue' import FacePicker from '../../input/FacePicker.vue' import { useConversationStore } from '@/views/im/home/store/conversationStore' +import { useFriendStore } from '@/views/im/home/store/friendStore' +import { useGroupStore } from '@/views/im/home/store/groupStore' import { useMessageSender } from '@/views/im/home/composables/useMessageSender' import { useMessageMultiSelect } from '@/views/im/home/composables/useMessageMultiSelect' import { + ImConversationType, ImForwardMode, ImMessageType, MERGE_FORWARD_PREVIEW_LINES, type ImForwardModeValue } from '@/views/im/utils/constants' import { getConversationKey, summarizeMessageContent } from '@/views/im/utils/conversation' +import { buildDefaultGroupName } from '@/views/im/utils/group' import { buildMergeMessagePayload, removeQuotePayload, serializeMessage } from '@/views/im/utils/message' -import type { Conversation, Message } from '@/views/im/home/types' +import type { Conversation, FriendLite, Message } from '@/views/im/home/types' defineOptions({ name: 'ImMessageForwardDialog' }) const message = useMessage() const conversationStore = useConversationStore() +const friendStore = useFriendStore() +const groupStore = useGroupStore() const { sendRaw, send } = useMessageSender() const multiSelect = useMessageMultiSelect() @@ -141,7 +189,10 @@ const state = reactive({ sourceConversation: null as Conversation | null }) const visible = ref(false) +/** 当前视图:默认会话选择,「创建聊天」入口切到好友选择 */ +const view = ref<'conversation' | 'contact'>('conversation') const selectedKeys = ref([]) +const selectedFriendIds = ref([]) const leaveMessage = ref('') const sending = ref(false) /** emoji picker 显隐:右侧笑脸按钮切换 */ @@ -157,15 +208,23 @@ defineExpose({ state.mode = opts.mode state.messages = opts.messages state.sourceConversation = opts.sourceConversation + view.value = 'conversation' selectedKeys.value = [] + selectedFriendIds.value = [] leaveMessage.value = '' emojiVisible.value = false + sending.value = false visible.value = true } }) -/** 弹窗标题:按 mode 区分「逐条转发 / 合并转发」 */ -const dialogTitle = computed(() => (state.mode === ImForwardMode.MERGE ? '合并转发' : '逐条转发')) +/** 弹窗标题:会话视图按 mode 区分「逐条 / 合并转发」;好友视图固定为「选择好友」 */ +const headerTitle = computed(() => { + if (view.value === 'contact') { + return '选择好友' + } + return state.mode === ImForwardMode.MERGE ? '合并转发' : '逐条转发' +}) /** 确认按钮文案:单选「发送」、多选「分别发送(n)」 */ const confirmButtonText = computed(() => @@ -177,6 +236,16 @@ const candidateConversations = computed( () => conversationStore.getSortedConversations ) +/** 好友视图候选列表:直接复用 friendStore Lite 视图 */ +const friends = computed(() => friendStore.getActiveFriendsLite) + +/** 切到好友视图:清掉之前在会话视图输入的留言,避免在不可见输入框里把留言静默发到新群 */ +function handleSwitchToContact() { + view.value = 'contact' + leaveMessage.value = '' + emojiVisible.value = false +} + /** 选中 emoji:拼到留言末尾;FacePicker 自身负责关闭面板 */ function handleEmojiSelect(emoji: string) { leaveMessage.value = `${leaveMessage.value}${emoji}` @@ -292,6 +361,75 @@ async function handleSend() { sending.value = false } } + +/** + * 好友视图发送:先建群(同时邀请所选好友)→ 给新群复用 forwardToTarget 转发 → 发留言 → 关弹窗 + * + * 跟会话视图的差别:先要 createGroup 拿到 groupId,之后构造 GROUP 临时 conversation 喂给已有的 forwardToTarget + * (sendRaw 内部会自动 insertMessage 把新群登记进 store,最近转发列表也能正常推) + */ +async function handleCreateGroupAndSend() { + if (selectedFriendIds.value.length === 0) { + return + } + if (state.messages.length === 0) { + message.warning('没有可转发的消息') + return + } + const byId = new Map(friends.value.map((f) => [f.id, f])) + const members = selectedFriendIds.value + .map((id) => byId.get(id)) + .filter((f): f is FriendLite => f != null) + if (members.length === 0) { + return + } + sending.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,省一次 fetchGroups + groupStore.upsertGroup({ + id: group.id, + name: group.name, + avatar: group.avatar, + notice: group.notice, + ownerUserId: group.ownerUserId + }) + // 给新群构造一个临时 conversation 对象给 forwardToTarget 用;sendRaw 内部会自动 insertMessage 登记 + const newConversation: Conversation = { + type: ImConversationType.GROUP, + targetId: group.id, + name: group.name || name, + avatar: group.avatar || '', + unreadCount: 0, + messages: [], + lastContent: '', + lastSendTime: 0 + } + const forwardOk = await forwardToTarget(newConversation) + if (forwardOk) { + const leaveText = leaveMessage.value.trim() + if (leaveText) { + await send(leaveText, { conversation: newConversation }) + } + conversationStore.pushRecentForwardConversationKeys([getConversationKey(newConversation)]) + message.success('已创建群聊并转发') + } else { + message.warning('群已创建,但消息转发失败,请稍后在群里重试') + } + // 统一退多选 + 关弹窗:成功 / 失败都要退源会话的多选态,避免遗留 + if (multiSelect.state.active) { + multiSelect.exit() + } + visible.value = false + } finally { + sending.value = false + } +} -