feat(im): MessageForwardDialog 接入「创建聊天」分支 + 收尾打磨

- MessageForwardDialog 加 view: 'conversation' | 'contact' 切换:
  - 模板按 view 切 ConversationPickerPanel / FriendPickerPanel;dialog header 用 slot 渲染「← 返回」
  - handleSwitchToContact 切 view + 清留言(避免不可见输入框留言被静默发出)
  - handleCreateGroupAndSend 复用 forwardToTarget(newConversation),merge / single 都按 mode 自动跑
  - 成功 / 失败统一末尾退多选 + 关弹窗,避免源会话遗留多选态
- 顺手清掉 GroupMemberAddDialog / MessageForwardDialog 末尾多余空行

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
im
YunaiV 2026-05-08 16:16:01 +08:00
parent 312df4c73d
commit dfd5b39a17
2 changed files with 144 additions and 8 deletions

View File

@ -146,4 +146,3 @@ async function handleOk() {
@include picker.styles;
}
</style>

View File

@ -1,22 +1,42 @@
<template>
<!--
转发消息逐条 / 合并选目标会话 + 留言后批量发送
- dialog 壳本组件持有选择 UI 委托 ConversationPickerPanel
- dialog 壳本组件持有选择 UI 委托 ConversationPickerPanel / FriendPickerPanel
- view='conversation'选已有会话发送默认视图
- view='contact'创建聊天入口进入选好友建群再转发业务壳层切视图
- footer slot 塞预览卡合并 / 逐条不同视觉+ 留言 + 提交按钮
- 对外接口沿用ref + open({ mode, messages, sourceConversation })
-->
<el-dialog
v-model="visible"
:title="dialogTitle"
width="720px"
:close-on-click-modal="false"
class="im-picker-dialog"
class="im-picker-dialog im-forward-dialog"
>
<template #header>
<div class="flex gap-2 items-center">
<Icon
v-if="view === 'contact'"
icon="ant-design:arrow-left-outlined"
:size="16"
class="cursor-pointer im-forward-dialog__back"
@click="view = 'conversation'"
/>
<span class="text-base text-[var(--el-text-color-primary)]">
{{ headerTitle }}
</span>
</div>
</template>
<div class="h-[480px]">
<!-- 会话视图选已有会话转发 -->
<ConversationPickerPanel
v-if="view === 'conversation'"
v-model:selected-keys="selectedKeys"
:conversations="candidateConversations"
:recent-forward-conversation-keys="conversationStore.recentForwardConversationKeys"
:show-create-chat="true"
@create-chat="handleSwitchToContact"
@remove-recent="conversationStore.removeRecentForwardConversationKey"
>
<template #footer>
@ -100,7 +120,27 @@
</div>
</template>
</ConversationPickerPanel>
<!-- 好友视图选好友建群后转发 -->
<FriendPickerPanel
v-else
v-model:selected-ids="selectedFriendIds"
:friends="friends"
/>
</div>
<!-- 好友视图的 dialog footer建群并转发 -->
<template v-if="view === 'contact'" #footer>
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:loading="sending"
:disabled="selectedFriendIds.length === 0"
@click="handleCreateGroupAndSend"
>
创建群聊并发送
</el-button>
</template>
</el-dialog>
</template>
@ -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<string[]>([])
const selectedFriendIds = ref<number[]>([])
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<Conversation[]>(
() => conversationStore.getSortedConversations
)
/** 好友视图候选列表:直接复用 friendStore Lite 视图 */
const friends = computed<FriendLite[]>(() => 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
}
}
</script>
<style scoped lang="scss">
@ -301,4 +439,3 @@ async function handleSend() {
@include picker.styles;
}
</style>