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 { useCallback, useEffect, useState } from 'react'; |
||||
import { fetchConversation, sendChatMessage } from '../api'; |
import { |
||||
import type { ConversationDetail } from '../types'; |
fetchConversation, |
||||
|
fetchMessages, |
||||
|
markConversationRead, |
||||
|
recallMessage, |
||||
|
sendChatMessage, |
||||
|
} from '../api'; |
||||
|
import type { ChatMessage, ConversationDetail } from '../types'; |
||||
import { useAsyncData } from './useAsyncData'; |
import { useAsyncData } from './useAsyncData'; |
||||
|
|
||||
export interface ConversationState { |
export interface ConversationState { |
||||
/** 会话详情 */ |
/** 会话元信息(不含消息列表) */ |
||||
detail: ConversationDetail | null; |
detail: ConversationDetail | null; |
||||
|
/** 消息列表(时间正序) */ |
||||
|
messages: ChatMessage[]; |
||||
/** 是否加载中 */ |
/** 是否加载中 */ |
||||
loading: boolean; |
loading: boolean; |
||||
/** 错误对象 */ |
/** 错误对象 */ |
||||
error: Error | null; |
error: Error | null; |
||||
/** 刷新数据 */ |
/** 刷新数据 */ |
||||
refresh: () => void; |
refresh: () => void; |
||||
|
/** 进入会话时标记已读(真实后端调用 /read,mock 直接置空) */ |
||||
|
markRead: () => Promise<void>; |
||||
/** 发送消息 */ |
/** 发送消息 */ |
||||
sendMessage: (content: string) => Promise<void>; |
sendMessage: (content: string) => Promise<void>; |
||||
/** 发送中状态 */ |
/** 发送中状态 */ |
||||
sending: boolean; |
sending: boolean; |
||||
|
/** 撤回消息 */ |
||||
|
recall: (messageId: string) => Promise<void>; |
||||
|
/** 撤回中状态 */ |
||||
|
recalling: boolean; |
||||
} |
} |
||||
|
|
||||
export function useConversation(conversationId: string): ConversationState { |
export function useConversation(conversationId: string): ConversationState { |
||||
const [sending, setSending] = useState(false); |
const [sending, setSending] = useState(false); |
||||
|
const [recalling, setRecalling] = useState(false); |
||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]); |
||||
|
|
||||
const fetcher = useCallback(() => fetchConversation(conversationId), [conversationId]); |
const metaFetcher = useCallback( |
||||
const { data, loading, error, refresh } = useAsyncData(fetcher); |
() => 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( |
const sendMessage = useCallback( |
||||
async (content: string) => { |
async (content: string) => { |
||||
setSending(true); |
setSending(true); |
||||
try { |
try { |
||||
const newMessages = await sendChatMessage(conversationId, content); |
const newMessages = await sendChatMessage(conversationId, content); |
||||
// mock 阶段:直接把新消息合并到本地数据,避免重新拉取整屏
|
// mock 阶段:直接把新消息合并到本地列表,避免重新拉取整屏
|
||||
const current = data as ConversationDetail | null; |
setMessages((prev) => [...prev, ...newMessages]); |
||||
if (current) { |
|
||||
current.messages.push(...newMessages); |
|
||||
refresh(); |
|
||||
} else { |
|
||||
refresh(); |
|
||||
} |
|
||||
} finally { |
} finally { |
||||
setSending(false); |
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 { |
return { |
||||
detail: (data as ConversationDetail | null) ?? null, |
detail: (detail as ConversationDetail | null) ?? null, |
||||
|
messages, |
||||
loading, |
loading, |
||||
error, |
error, |
||||
refresh, |
refresh: () => { |
||||
|
refreshMeta(); |
||||
|
refreshMessages(); |
||||
|
}, |
||||
|
markRead, |
||||
sendMessage, |
sendMessage, |
||||
sending, |
sending, |
||||
|
recall, |
||||
|
recalling, |
||||
}; |
}; |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue