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
parent
312df4c73d
commit
dfd5b39a17
|
|
@ -146,4 +146,3 @@ async function handleOk() {
|
|||
@include picker.styles;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue