mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(chat): smooth standalone session window loading
This commit is contained in:
@@ -1783,6 +1783,30 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.standalone-phase-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 82%, transparent);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-chat-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -205,8 +205,12 @@ interface ChatPageProps {
|
||||
standaloneSessionWindow?: boolean
|
||||
initialSessionId?: string | null
|
||||
standaloneSource?: string | null
|
||||
standaloneInitialDisplayName?: string | null
|
||||
standaloneInitialAvatarUrl?: string | null
|
||||
standaloneInitialContactType?: string | null
|
||||
}
|
||||
|
||||
type StandaloneLoadStage = 'idle' | 'connecting' | 'loading' | 'ready'
|
||||
|
||||
interface SessionDetail {
|
||||
wxid: string
|
||||
@@ -409,9 +413,19 @@ const SessionItem = React.memo(function SessionItem({
|
||||
|
||||
|
||||
function ChatPage(props: ChatPageProps) {
|
||||
const { standaloneSessionWindow = false, initialSessionId = null, standaloneSource = null } = props
|
||||
const {
|
||||
standaloneSessionWindow = false,
|
||||
initialSessionId = null,
|
||||
standaloneSource = null,
|
||||
standaloneInitialDisplayName = null,
|
||||
standaloneInitialAvatarUrl = null,
|
||||
standaloneInitialContactType = null
|
||||
} = props
|
||||
const normalizedInitialSessionId = useMemo(() => String(initialSessionId || '').trim(), [initialSessionId])
|
||||
const normalizedStandaloneSource = useMemo(() => String(standaloneSource || '').trim().toLowerCase(), [standaloneSource])
|
||||
const normalizedStandaloneInitialDisplayName = useMemo(() => String(standaloneInitialDisplayName || '').trim(), [standaloneInitialDisplayName])
|
||||
const normalizedStandaloneInitialAvatarUrl = useMemo(() => String(standaloneInitialAvatarUrl || '').trim(), [standaloneInitialAvatarUrl])
|
||||
const normalizedStandaloneInitialContactType = useMemo(() => String(standaloneInitialContactType || '').trim().toLowerCase(), [standaloneInitialContactType])
|
||||
const shouldHideStandaloneDetailButton = standaloneSessionWindow && normalizedStandaloneSource === 'export'
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -496,7 +510,12 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [hasInitialMessages, setHasInitialMessages] = useState(false)
|
||||
const [isSessionSwitching, setIsSessionSwitching] = useState(false)
|
||||
const [noMessageTable, setNoMessageTable] = useState(false)
|
||||
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
|
||||
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(normalizedStandaloneInitialDisplayName || null)
|
||||
const [fallbackAvatarUrl, setFallbackAvatarUrl] = useState<string | null>(normalizedStandaloneInitialAvatarUrl || null)
|
||||
const [standaloneLoadStage, setStandaloneLoadStage] = useState<StandaloneLoadStage>(
|
||||
standaloneSessionWindow && normalizedInitialSessionId ? 'connecting' : 'idle'
|
||||
)
|
||||
const [standaloneInitialLoadRequested, setStandaloneInitialLoadRequested] = useState(false)
|
||||
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
||||
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
|
||||
const [inProgressExportSessionIds, setInProgressExportSessionIds] = useState<Set<string>>(new Set())
|
||||
@@ -2411,9 +2430,9 @@ function ChatPage(props: ChatPageProps) {
|
||||
}, [appendMessages, getMessageKey])
|
||||
|
||||
// 选择会话
|
||||
const selectSessionById = useCallback((sessionId: string) => {
|
||||
const selectSessionById = useCallback((sessionId: string, options: { force?: boolean } = {}) => {
|
||||
const normalizedSessionId = String(sessionId || '').trim()
|
||||
if (!normalizedSessionId || normalizedSessionId === currentSessionId) return
|
||||
if (!normalizedSessionId || (!options.force && normalizedSessionId === currentSessionId)) return
|
||||
const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1
|
||||
sessionSwitchRequestSeqRef.current = switchRequestSeq
|
||||
|
||||
@@ -2737,7 +2756,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
}, [currentSessionId, messages.length, isLoadingMessages])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
|
||||
if (currentSessionId && isConnected && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
|
||||
if (pendingSessionLoadRef.current === currentSessionId) return
|
||||
if (initialLoadRequestedSessionRef.current === currentSessionId) return
|
||||
initialLoadRequestedSessionRef.current = currentSessionId
|
||||
@@ -2748,7 +2767,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
forceInitialLimit: 30
|
||||
})
|
||||
}
|
||||
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])
|
||||
}, [currentSessionId, isConnected, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -2909,7 +2928,21 @@ function ChatPage(props: ChatPageProps) {
|
||||
// 获取当前会话信息(从通讯录跳转时可能不在 sessions 列表中,构造 fallback)
|
||||
const currentSession = (() => {
|
||||
const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined
|
||||
if (found || !currentSessionId) return found
|
||||
if (found) {
|
||||
if (
|
||||
standaloneSessionWindow &&
|
||||
normalizedInitialSessionId &&
|
||||
found.username === normalizedInitialSessionId
|
||||
) {
|
||||
return {
|
||||
...found,
|
||||
displayName: found.displayName || fallbackDisplayName || found.username,
|
||||
avatarUrl: found.avatarUrl || fallbackAvatarUrl || undefined
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
if (!currentSessionId) return found
|
||||
return {
|
||||
username: currentSessionId,
|
||||
type: 0,
|
||||
@@ -2919,6 +2952,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
lastTimestamp: 0,
|
||||
lastMsgType: 0,
|
||||
displayName: fallbackDisplayName || currentSessionId,
|
||||
avatarUrl: fallbackAvatarUrl || undefined,
|
||||
} as ChatSession
|
||||
})()
|
||||
const filteredGroupPanelMembers = useMemo(() => {
|
||||
@@ -2938,33 +2972,121 @@ function ChatPage(props: ChatPageProps) {
|
||||
}, [groupMemberSearchKeyword, groupPanelMembers])
|
||||
const isCurrentSessionExporting = Boolean(currentSessionId && inProgressExportSessionIds.has(currentSessionId))
|
||||
const isExportActionBusy = isCurrentSessionExporting || isPreparingExportDialog
|
||||
const isCurrentSessionGroup = Boolean(
|
||||
currentSession && (
|
||||
isGroupChatSession(currentSession.username) ||
|
||||
(
|
||||
standaloneSessionWindow &&
|
||||
currentSession.username === normalizedInitialSessionId &&
|
||||
normalizedStandaloneInitialContactType === 'group'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!standaloneSessionWindow) return
|
||||
setStandaloneInitialLoadRequested(false)
|
||||
setStandaloneLoadStage(normalizedInitialSessionId ? 'connecting' : 'idle')
|
||||
setFallbackDisplayName(normalizedStandaloneInitialDisplayName || null)
|
||||
setFallbackAvatarUrl(normalizedStandaloneInitialAvatarUrl || null)
|
||||
}, [
|
||||
standaloneSessionWindow,
|
||||
normalizedInitialSessionId,
|
||||
normalizedStandaloneInitialDisplayName,
|
||||
normalizedStandaloneInitialAvatarUrl
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!standaloneSessionWindow) return
|
||||
if (!normalizedInitialSessionId) return
|
||||
|
||||
if (normalizedStandaloneInitialDisplayName) {
|
||||
setFallbackDisplayName(normalizedStandaloneInitialDisplayName)
|
||||
}
|
||||
if (normalizedStandaloneInitialAvatarUrl) {
|
||||
setFallbackAvatarUrl(normalizedStandaloneInitialAvatarUrl)
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
setCurrentSession(normalizedInitialSessionId, { preserveMessages: false })
|
||||
}
|
||||
if (!isConnected || isConnecting) {
|
||||
setStandaloneLoadStage('connecting')
|
||||
}
|
||||
}, [
|
||||
standaloneSessionWindow,
|
||||
normalizedInitialSessionId,
|
||||
normalizedStandaloneInitialDisplayName,
|
||||
normalizedStandaloneInitialAvatarUrl,
|
||||
currentSessionId,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
setCurrentSession
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!standaloneSessionWindow) return
|
||||
if (!normalizedInitialSessionId) return
|
||||
if (!isConnected || isConnecting) return
|
||||
if (currentSessionId === normalizedInitialSessionId) return
|
||||
selectSessionById(normalizedInitialSessionId)
|
||||
if (currentSessionId === normalizedInitialSessionId && standaloneInitialLoadRequested) return
|
||||
setStandaloneInitialLoadRequested(true)
|
||||
setStandaloneLoadStage('loading')
|
||||
selectSessionById(normalizedInitialSessionId, {
|
||||
force: currentSessionId === normalizedInitialSessionId
|
||||
})
|
||||
}, [
|
||||
standaloneSessionWindow,
|
||||
normalizedInitialSessionId,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
currentSessionId,
|
||||
standaloneInitialLoadRequested,
|
||||
selectSessionById
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!standaloneSessionWindow || !normalizedInitialSessionId) return
|
||||
if (!isConnected || isConnecting) {
|
||||
setStandaloneLoadStage('connecting')
|
||||
return
|
||||
}
|
||||
if (!standaloneInitialLoadRequested) {
|
||||
setStandaloneLoadStage('loading')
|
||||
return
|
||||
}
|
||||
if (currentSessionId !== normalizedInitialSessionId) {
|
||||
setStandaloneLoadStage('loading')
|
||||
return
|
||||
}
|
||||
if (isLoadingMessages || isSessionSwitching) {
|
||||
setStandaloneLoadStage('loading')
|
||||
return
|
||||
}
|
||||
setStandaloneLoadStage('ready')
|
||||
}, [
|
||||
standaloneSessionWindow,
|
||||
normalizedInitialSessionId,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
standaloneInitialLoadRequested,
|
||||
currentSessionId,
|
||||
isLoadingMessages,
|
||||
isSessionSwitching
|
||||
])
|
||||
|
||||
// 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称
|
||||
useEffect(() => {
|
||||
if (!currentSessionId) return
|
||||
const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined
|
||||
if (found) {
|
||||
setFallbackDisplayName(null)
|
||||
if (found.displayName) setFallbackDisplayName(found.displayName)
|
||||
if (found.avatarUrl) setFallbackAvatarUrl(found.avatarUrl)
|
||||
return
|
||||
}
|
||||
loadContactInfoBatch([currentSessionId]).then(() => {
|
||||
const cached = senderAvatarCache.get(currentSessionId)
|
||||
if (cached?.displayName) setFallbackDisplayName(cached.displayName)
|
||||
if (cached?.avatarUrl) setFallbackAvatarUrl(cached.avatarUrl)
|
||||
})
|
||||
}, [currentSessionId, sessions])
|
||||
|
||||
@@ -3741,16 +3863,16 @@ function ChatPage(props: ChatPageProps) {
|
||||
src={currentSession.avatarUrl}
|
||||
name={currentSession.displayName || currentSession.username}
|
||||
size={40}
|
||||
className={isGroupChatSession(currentSession.username) ? 'group session-avatar' : 'session-avatar'}
|
||||
className={isCurrentSessionGroup ? 'group session-avatar' : 'session-avatar'}
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h3>{currentSession.displayName || currentSession.username}</h3>
|
||||
{isGroupChatSession(currentSession.username) && (
|
||||
{isCurrentSessionGroup && (
|
||||
<div className="header-subtitle">群聊</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
{!standaloneSessionWindow && isGroupChatSession(currentSession.username) && (
|
||||
{!standaloneSessionWindow && isCurrentSessionGroup && (
|
||||
<button
|
||||
className="icon-btn group-analytics-btn"
|
||||
onClick={handleGroupAnalytics}
|
||||
@@ -3759,7 +3881,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
<BarChart3 size={18} />
|
||||
</button>
|
||||
)}
|
||||
{isGroupChatSession(currentSession.username) && (
|
||||
{isCurrentSessionGroup && (
|
||||
<button
|
||||
className={`icon-btn group-members-btn ${showGroupMembersPanel ? 'active' : ''}`}
|
||||
onClick={toggleGroupMembersPanel}
|
||||
@@ -3886,6 +4008,13 @@ function ChatPage(props: ChatPageProps) {
|
||||
)}
|
||||
|
||||
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'} ${isSessionSwitching ? 'switching' : ''}`}>
|
||||
{standaloneSessionWindow && standaloneLoadStage !== 'ready' && (
|
||||
<div className="standalone-phase-overlay" role="status" aria-live="polite">
|
||||
<Loader2 size={22} className="spin" />
|
||||
<span>{standaloneLoadStage === 'connecting' ? '正在建立连接...' : '正在加载最近消息...'}</span>
|
||||
{connectionError && <small>{connectionError}</small>}
|
||||
</div>
|
||||
)}
|
||||
{isLoadingMessages && (!hasInitialMessages || isSessionSwitching) && (
|
||||
<div className="loading-messages loading-overlay">
|
||||
<Loader2 size={24} />
|
||||
@@ -3942,7 +4071,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
session={currentSession}
|
||||
showTime={!showDateDivider && showTime}
|
||||
myAvatarUrl={myAvatarUrl}
|
||||
isGroupChat={isGroupChatSession(currentSession.username)}
|
||||
isGroupChat={isCurrentSessionGroup}
|
||||
onRequireModelDownload={handleRequireModelDownload}
|
||||
onContextMenu={handleContextMenu}
|
||||
isSelectionMode={isSelectionMode}
|
||||
@@ -3974,7 +4103,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
</div>
|
||||
|
||||
{/* 群成员面板 */}
|
||||
{showGroupMembersPanel && isGroupChatSession(currentSession.username) && (
|
||||
{showGroupMembersPanel && isCurrentSessionGroup && (
|
||||
<div className="detail-panel group-members-panel">
|
||||
<div className="detail-header">
|
||||
<h4>群成员</h4>
|
||||
|
||||
Reference in New Issue
Block a user