Browse Source
- api: 新增 createConversation / fetchMessages / markConversationRead / recallMessage, 真实路径对齐后端 /conversations 系列;sendChatMessage 真实路径改为 /conversations/messages - hook: useConversation 接入消息列表(fetchMessages)、进入标记已读、长按撤回 - 沟通页:进入会话即标记已读;长按本人文本消息可撤回(本地覆盖为撤回态) - 类型:ChatMessage 增加 recalled;新增 MessagePage / BackendMessage / CreateConversation*master
6 changed files with 370 additions and 41 deletions
@ -1,59 +1,119 @@ |
|||
/** |
|||
* HR 单聊会话详情 hook:拉取会话详情,并提供发送消息能力。 |
|||
* 发送消息后自动刷新会话,把新消息追加到本地列表。 |
|||
* HR 单聊会话详情 hook:拉取会话与消息列表,提供发送、标记已读、撤回能力。 |
|||
* - 会话元信息走 fetchConversation(mock 与真实后端共用); |
|||
* - 消息列表走 fetchMessages(对齐后端 /conversations/{id}/messages 分页接口); |
|||
* - 进入会话即标记已读,自己发的消息支持两分钟内撤回(真实后端校验窗口)。 |
|||
*/ |
|||
import { useCallback, useState } from 'react'; |
|||
import { fetchConversation, sendChatMessage } from '../api'; |
|||
import type { ConversationDetail } from '../types'; |
|||
import { useCallback, useEffect, useState } from 'react'; |
|||
import { |
|||
fetchConversation, |
|||
fetchMessages, |
|||
markConversationRead, |
|||
recallMessage, |
|||
sendChatMessage, |
|||
} from '../api'; |
|||
import type { ChatMessage, ConversationDetail } from '../types'; |
|||
import { useAsyncData } from './useAsyncData'; |
|||
|
|||
export interface ConversationState { |
|||
/** 会话详情 */ |
|||
/** 会话元信息(不含消息列表) */ |
|||
detail: ConversationDetail | null; |
|||
/** 消息列表(时间正序) */ |
|||
messages: ChatMessage[]; |
|||
/** 是否加载中 */ |
|||
loading: boolean; |
|||
/** 错误对象 */ |
|||
error: Error | null; |
|||
/** 刷新数据 */ |
|||
refresh: () => void; |
|||
/** 进入会话时标记已读(真实后端调用 /read,mock 直接置空) */ |
|||
markRead: () => Promise<void>; |
|||
/** 发送消息 */ |
|||
sendMessage: (content: string) => Promise<void>; |
|||
/** 发送中状态 */ |
|||
sending: boolean; |
|||
/** 撤回消息 */ |
|||
recall: (messageId: string) => Promise<void>; |
|||
/** 撤回中状态 */ |
|||
recalling: boolean; |
|||
} |
|||
|
|||
export function useConversation(conversationId: string): ConversationState { |
|||
const [sending, setSending] = useState(false); |
|||
const [recalling, setRecalling] = useState(false); |
|||
const [messages, setMessages] = useState<ChatMessage[]>([]); |
|||
|
|||
const fetcher = useCallback(() => fetchConversation(conversationId), [conversationId]); |
|||
const { data, loading, error, refresh } = useAsyncData(fetcher); |
|||
const metaFetcher = useCallback( |
|||
() => fetchConversation(conversationId), |
|||
[conversationId], |
|||
); |
|||
const { data: detail, loading, error, refresh: refreshMeta } = |
|||
useAsyncData(metaFetcher); |
|||
|
|||
const msgFetcher = useCallback( |
|||
() => fetchMessages(conversationId).then((page) => page.items), |
|||
[conversationId], |
|||
); |
|||
const { |
|||
data: fetchedMessages, |
|||
refresh: refreshMessages, |
|||
} = useAsyncData<ChatMessage[]>(msgFetcher); |
|||
|
|||
// 远端消息列表变更(首次加载 / 下拉刷新)时同步到本地可写副本
|
|||
useEffect(() => { |
|||
if (fetchedMessages) setMessages(fetchedMessages); |
|||
}, [fetchedMessages]); |
|||
|
|||
const markRead = useCallback(async () => { |
|||
if (!conversationId) return; |
|||
try { |
|||
await markConversationRead(conversationId); |
|||
} catch { |
|||
// 标记已读失败不影响会话可读性,静默降级
|
|||
} |
|||
}, [conversationId]); |
|||
|
|||
const sendMessage = useCallback( |
|||
async (content: string) => { |
|||
setSending(true); |
|||
try { |
|||
const newMessages = await sendChatMessage(conversationId, content); |
|||
// mock 阶段:直接把新消息合并到本地数据,避免重新拉取整屏
|
|||
const current = data as ConversationDetail | null; |
|||
if (current) { |
|||
current.messages.push(...newMessages); |
|||
refresh(); |
|||
} else { |
|||
refresh(); |
|||
} |
|||
// mock 阶段:直接把新消息合并到本地列表,避免重新拉取整屏
|
|||
setMessages((prev) => [...prev, ...newMessages]); |
|||
} finally { |
|||
setSending(false); |
|||
} |
|||
}, |
|||
[conversationId, data, refresh], |
|||
[conversationId], |
|||
); |
|||
|
|||
const recall = useCallback( |
|||
async (messageId: string) => { |
|||
setRecalling(true); |
|||
try { |
|||
const recalled = await recallMessage(conversationId, messageId); |
|||
// 用撤回后的消息覆盖本地对应项
|
|||
setMessages((prev) => prev.map((m) => (m.id === messageId ? recalled : m))); |
|||
} finally { |
|||
setRecalling(false); |
|||
} |
|||
}, |
|||
[conversationId], |
|||
); |
|||
|
|||
return { |
|||
detail: (data as ConversationDetail | null) ?? null, |
|||
detail: (detail as ConversationDetail | null) ?? null, |
|||
messages, |
|||
loading, |
|||
error, |
|||
refresh, |
|||
refresh: () => { |
|||
refreshMeta(); |
|||
refreshMessages(); |
|||
}, |
|||
markRead, |
|||
sendMessage, |
|||
sending, |
|||
recall, |
|||
recalling, |
|||
}; |
|||
} |
|||
|
|||
Loading…
Reference in new issue