mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(chat): smooth loading with progressive session hydration
This commit is contained in:
@@ -912,6 +912,10 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getSessions()
|
return chatService.getSessions()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getSessionStatuses', async (_, usernames: string[]) => {
|
||||||
|
return chatService.getSessionStatuses(usernames)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:getExportTabCounts', async () => {
|
ipcMain.handle('chat:getExportTabCounts', async () => {
|
||||||
return chatService.getExportTabCounts()
|
return chatService.getExportTabCounts()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
chat: {
|
chat: {
|
||||||
connect: () => ipcRenderer.invoke('chat:connect'),
|
connect: () => ipcRenderer.invoke('chat:connect'),
|
||||||
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
||||||
|
getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames),
|
||||||
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
||||||
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
||||||
enrichSessionsContactInfo: (usernames: string[]) =>
|
enrichSessionsContactInfo: (usernames: string[]) =>
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ class ChatService {
|
|||||||
private sessionMessageCountHintCache = new Map<string, number>()
|
private sessionMessageCountHintCache = new Map<string, number>()
|
||||||
private sessionMessageCountCacheScope = ''
|
private sessionMessageCountCacheScope = ''
|
||||||
private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000
|
private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000
|
||||||
|
private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>()
|
||||||
|
private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configService = new ConfigService()
|
this.configService = new ConfigService()
|
||||||
@@ -386,7 +388,7 @@ class ChatService {
|
|||||||
return { success: false, error: `会话表异常: ${detail}${tableInfo}${tables}${columns}` }
|
return { success: false, error: `会话表异常: ${detail}${tableInfo}${tables}${columns}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为 ChatSession(先加载缓存,但不等待数据库查询)
|
// 转换为 ChatSession(先加载缓存,但不等待额外状态查询)
|
||||||
const sessions: ChatSession[] = []
|
const sessions: ChatSession[] = []
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const myWxid = this.configService.get('myWxid')
|
const myWxid = this.configService.get('myWxid')
|
||||||
@@ -449,7 +451,7 @@ class ChatService {
|
|||||||
avatarUrl = cached.avatarUrl
|
avatarUrl = cached.avatarUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.push({
|
const nextSession: ChatSession = {
|
||||||
username,
|
username,
|
||||||
type: parseInt(row.type || '0', 10),
|
type: parseInt(row.type || '0', 10),
|
||||||
unreadCount: parseInt(row.unread_count || row.unreadCount || row.unreadcount || '0', 10),
|
unreadCount: parseInt(row.unread_count || row.unreadCount || row.unreadcount || '0', 10),
|
||||||
@@ -463,7 +465,15 @@ class ChatService {
|
|||||||
lastMsgSender: row.last_msg_sender,
|
lastMsgSender: row.last_msg_sender,
|
||||||
lastSenderDisplayName: row.last_sender_display_name,
|
lastSenderDisplayName: row.last_sender_display_name,
|
||||||
selfWxid: myWxid
|
selfWxid: myWxid
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const cachedStatus = this.sessionStatusCache.get(username)
|
||||||
|
if (cachedStatus && now - cachedStatus.updatedAt <= this.sessionStatusCacheTtlMs) {
|
||||||
|
nextSession.isFolded = cachedStatus.isFolded
|
||||||
|
nextSession.isMuted = cachedStatus.isMuted
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.push(nextSession)
|
||||||
|
|
||||||
if (typeof messageCountHint === 'number') {
|
if (typeof messageCountHint === 'number') {
|
||||||
this.sessionMessageCountHintCache.set(username, messageCountHint)
|
this.sessionMessageCountHintCache.set(username, messageCountHint)
|
||||||
@@ -474,23 +484,6 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量拉取 extra_buffer 状态(isFolded/isMuted),不阻塞主流程
|
|
||||||
const allUsernames = sessions.map(s => s.username)
|
|
||||||
try {
|
|
||||||
const statusResult = await wcdbService.getContactStatus(allUsernames)
|
|
||||||
if (statusResult.success && statusResult.map) {
|
|
||||||
for (const s of sessions) {
|
|
||||||
const st = statusResult.map[s.username]
|
|
||||||
if (st) {
|
|
||||||
s.isFolded = st.isFolded
|
|
||||||
s.isMuted = st.isMuted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 状态获取失败不影响会话列表返回
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不等待联系人信息加载,直接返回基础会话列表
|
// 不等待联系人信息加载,直接返回基础会话列表
|
||||||
// 前端可以异步调用 enrichSessionsWithContacts 来补充信息
|
// 前端可以异步调用 enrichSessionsWithContacts 来补充信息
|
||||||
return { success: true, sessions }
|
return { success: true, sessions }
|
||||||
@@ -500,6 +493,46 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSessionStatuses(usernames: string[]): Promise<{
|
||||||
|
success: boolean
|
||||||
|
map?: Record<string, { isFolded?: boolean; isMuted?: boolean }>
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
if (!Array.isArray(usernames) || usernames.length === 0) {
|
||||||
|
return { success: true, map: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectResult = await this.ensureConnected()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { success: false, error: connectResult.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await wcdbService.getContactStatus(usernames)
|
||||||
|
if (!result.success || !result.map) {
|
||||||
|
return { success: false, error: result.error || '获取会话状态失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
for (const username of usernames) {
|
||||||
|
const state = result.map[username]
|
||||||
|
if (!state) continue
|
||||||
|
this.sessionStatusCache.set(username, {
|
||||||
|
isFolded: state.isFolded,
|
||||||
|
isMuted: state.isMuted,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
map: result.map as Record<string, { isFolded?: boolean; isMuted?: boolean }>
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步补充会话列表的联系人信息(公开方法,供前端调用)
|
* 异步补充会话列表的联系人信息(公开方法,供前端调用)
|
||||||
*/
|
*/
|
||||||
@@ -1532,6 +1565,7 @@ class ChatService {
|
|||||||
this.sessionMessageCountCacheScope = scope
|
this.sessionMessageCountCacheScope = scope
|
||||||
this.sessionMessageCountCache.clear()
|
this.sessionMessageCountCache.clear()
|
||||||
this.sessionMessageCountHintCache.clear()
|
this.sessionMessageCountHintCache.clear()
|
||||||
|
this.sessionStatusCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectSessionExportStats(
|
private async collectSessionExportStats(
|
||||||
|
|||||||
@@ -815,6 +815,24 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-sync-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1651,6 +1669,18 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.switching .message-list {
|
||||||
|
opacity: 0.42;
|
||||||
|
transform: scale(0.995);
|
||||||
|
filter: saturate(0.72) blur(1px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.switching .loading-overlay {
|
||||||
|
background: rgba(127, 127, 127, 0.18);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
@@ -1666,7 +1696,7 @@
|
|||||||
background-color: var(--bg-tertiary);
|
background-color: var(--bg-tertiary);
|
||||||
position: relative;
|
position: relative;
|
||||||
-webkit-app-region: no-drag !important;
|
-webkit-app-region: no-drag !important;
|
||||||
transition: opacity 240ms ease, transform 240ms ease;
|
transition: opacity 240ms ease, transform 240ms ease, filter 220ms ease;
|
||||||
|
|
||||||
// 滚动条样式
|
// 滚动条样式
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@@ -4133,7 +4163,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息信息弹窗
|
// 消息信息弹窗
|
||||||
.message-info-overlay {
|
.message-info-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
||||||
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
|
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
|
||||||
const [hasInitialMessages, setHasInitialMessages] = useState(false)
|
const [hasInitialMessages, setHasInitialMessages] = useState(false)
|
||||||
|
const [isSessionSwitching, setIsSessionSwitching] = useState(false)
|
||||||
const [noMessageTable, setNoMessageTable] = useState(false)
|
const [noMessageTable, setNoMessageTable] = useState(false)
|
||||||
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
|
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
|
||||||
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
||||||
@@ -376,6 +377,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const sessionMapRef = useRef<Map<string, ChatSession>>(new Map())
|
const sessionMapRef = useRef<Map<string, ChatSession>>(new Map())
|
||||||
const sessionsRef = useRef<ChatSession[]>([])
|
const sessionsRef = useRef<ChatSession[]>([])
|
||||||
const currentSessionRef = useRef<string | null>(null)
|
const currentSessionRef = useRef<string | null>(null)
|
||||||
|
const pendingSessionLoadRef = useRef<string | null>(null)
|
||||||
const prevSessionRef = useRef<string | null>(null)
|
const prevSessionRef = useRef<string | null>(null)
|
||||||
const isLoadingMessagesRef = useRef(false)
|
const isLoadingMessagesRef = useRef(false)
|
||||||
const isLoadingMoreRef = useRef(false)
|
const isLoadingMoreRef = useRef(false)
|
||||||
@@ -447,10 +449,10 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const result = await window.electronAPI.chat.connect()
|
const result = await window.electronAPI.chat.connect()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setConnected(true)
|
setConnected(true)
|
||||||
await loadSessions()
|
const wxidPromise = window.electronAPI.config.get('myWxid')
|
||||||
await loadMyAvatar()
|
await Promise.all([loadSessions(), loadMyAvatar()])
|
||||||
// 获取 myWxid 用于匹配个人头像
|
// 获取 myWxid 用于匹配个人头像
|
||||||
const wxid = await window.electronAPI.config.get('myWxid')
|
const wxid = await wxidPromise
|
||||||
if (wxid) setMyWxid(wxid as string)
|
if (wxid) setMyWxid(wxid as string)
|
||||||
} else {
|
} else {
|
||||||
setConnectionError(result.error || '连接失败')
|
setConnectionError(result.error || '连接失败')
|
||||||
@@ -467,6 +469,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
senderAvatarLoading.clear()
|
senderAvatarLoading.clear()
|
||||||
preloadImageKeysRef.current.clear()
|
preloadImageKeysRef.current.clear()
|
||||||
lastPreloadSessionRef.current = null
|
lastPreloadSessionRef.current = null
|
||||||
|
pendingSessionLoadRef.current = null
|
||||||
|
setIsSessionSwitching(false)
|
||||||
setSessionDetail(null)
|
setSessionDetail(null)
|
||||||
setCurrentSession(null)
|
setCurrentSession(null)
|
||||||
setSessions([])
|
setSessions([])
|
||||||
@@ -499,6 +503,45 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
currentSessionRef.current = currentSessionId
|
currentSessionRef.current = currentSessionId
|
||||||
}, [currentSessionId])
|
}, [currentSessionId])
|
||||||
|
|
||||||
|
const hydrateSessionStatuses = useCallback(async (sessionList: ChatSession[]) => {
|
||||||
|
const usernames = sessionList.map((s) => s.username).filter(Boolean)
|
||||||
|
if (usernames.length === 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.chat.getSessionStatuses(usernames)
|
||||||
|
if (!result.success || !result.map) return
|
||||||
|
|
||||||
|
const statusMap = result.map
|
||||||
|
const { sessions: latestSessions } = useChatStore.getState()
|
||||||
|
if (!Array.isArray(latestSessions) || latestSessions.length === 0) return
|
||||||
|
|
||||||
|
let hasChanges = false
|
||||||
|
const updatedSessions = latestSessions.map((session) => {
|
||||||
|
const status = statusMap[session.username]
|
||||||
|
if (!status) return session
|
||||||
|
|
||||||
|
const nextIsFolded = status.isFolded ?? session.isFolded
|
||||||
|
const nextIsMuted = status.isMuted ?? session.isMuted
|
||||||
|
if (nextIsFolded === session.isFolded && nextIsMuted === session.isMuted) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanges = true
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
isFolded: nextIsFolded,
|
||||||
|
isMuted: nextIsMuted
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
setSessions(updatedSessions)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('会话状态补齐失败:', e)
|
||||||
|
}
|
||||||
|
}, [setSessions])
|
||||||
|
|
||||||
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
|
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
|
||||||
const loadSessions = async (options?: { silent?: boolean }) => {
|
const loadSessions = async (options?: { silent?: boolean }) => {
|
||||||
if (options?.silent) {
|
if (options?.silent) {
|
||||||
@@ -518,11 +561,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
setSessions(nextSessions)
|
setSessions(nextSessions)
|
||||||
sessionsRef.current = nextSessions
|
sessionsRef.current = nextSessions
|
||||||
|
void hydrateSessionStatuses(nextSessions)
|
||||||
// 立即启动联系人信息加载,不再延迟 500ms
|
// 立即启动联系人信息加载,不再延迟 500ms
|
||||||
void enrichSessionsContactInfo(nextSessions)
|
void enrichSessionsContactInfo(nextSessions)
|
||||||
} else {
|
} else {
|
||||||
console.error('mergeSessions returned non-array:', nextSessions)
|
console.error('mergeSessions returned non-array:', nextSessions)
|
||||||
setSessions(sessionsArray)
|
setSessions(sessionsArray)
|
||||||
|
void hydrateSessionStatuses(sessionsArray)
|
||||||
void enrichSessionsContactInfo(sessionsArray)
|
void enrichSessionsContactInfo(sessionsArray)
|
||||||
}
|
}
|
||||||
} else if (!result.success) {
|
} else if (!result.success) {
|
||||||
@@ -575,8 +620,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 进一步减少批次大小,每批3个,避免DLL调用阻塞
|
// 批量补齐联系人,平衡吞吐和 UI 流畅性
|
||||||
const batchSize = 3
|
const batchSize = 8
|
||||||
let loadedCount = 0
|
let loadedCount = 0
|
||||||
|
|
||||||
for (let i = 0; i < needEnrich.length; i += batchSize) {
|
for (let i = 0; i < needEnrich.length; i += batchSize) {
|
||||||
@@ -585,7 +630,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 等待滚动结束
|
// 等待滚动结束
|
||||||
while (isScrollingRef.current && !enrichCancelledRef.current) {
|
while (isScrollingRef.current && !enrichCancelledRef.current) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200))
|
await new Promise(resolve => setTimeout(resolve, 120))
|
||||||
}
|
}
|
||||||
if (enrichCancelledRef.current) break
|
if (enrichCancelledRef.current) break
|
||||||
}
|
}
|
||||||
@@ -602,11 +647,11 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
if ('requestIdleCallback' in window) {
|
if ('requestIdleCallback' in window) {
|
||||||
window.requestIdleCallback(() => {
|
window.requestIdleCallback(() => {
|
||||||
void loadContactInfoBatch(usernames).then(() => resolve())
|
void loadContactInfoBatch(usernames).then(() => resolve())
|
||||||
}, { timeout: 2000 })
|
}, { timeout: 700 })
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void loadContactInfoBatch(usernames).then(() => resolve())
|
void loadContactInfoBatch(usernames).then(() => resolve())
|
||||||
}, 300)
|
}, 80)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -618,8 +663,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 批次间延迟,给UI更多时间(DLL调用可能阻塞,需要更长的延迟)
|
// 批次间延迟,给UI更多时间(DLL调用可能阻塞,需要更长的延迟)
|
||||||
if (i + batchSize < needEnrich.length && !enrichCancelledRef.current) {
|
if (i + batchSize < needEnrich.length && !enrichCancelledRef.current) {
|
||||||
// 如果不在滚动,可以延迟短一点
|
const delay = isScrollingRef.current ? 260 : 120
|
||||||
const delay = isScrollingRef.current ? 1000 : 800
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay))
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,17 +693,17 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
contactUpdateTimerRef.current = null
|
contactUpdateTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加防抖延迟到500ms,避免在滚动时频繁更新
|
// 使用短防抖,让头像和昵称更快补齐但依然避免频繁重渲染
|
||||||
contactUpdateTimerRef.current = window.setTimeout(() => {
|
contactUpdateTimerRef.current = window.setTimeout(() => {
|
||||||
const updates = contactUpdateQueueRef.current
|
const updates = contactUpdateQueueRef.current
|
||||||
if (updates.size === 0) return
|
if (updates.size === 0) return
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
// 如果距离上次更新太近(小于1秒),继续延迟
|
// 如果距离上次更新太近(小于250ms),继续延迟
|
||||||
if (now - lastUpdateTimeRef.current < 1000) {
|
if (now - lastUpdateTimeRef.current < 250) {
|
||||||
contactUpdateTimerRef.current = window.setTimeout(() => {
|
contactUpdateTimerRef.current = window.setTimeout(() => {
|
||||||
flushContactUpdates()
|
flushContactUpdates()
|
||||||
}, 1000 - (now - lastUpdateTimeRef.current))
|
}, 250 - (now - lastUpdateTimeRef.current))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +740,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
updates.clear()
|
updates.clear()
|
||||||
contactUpdateTimerRef.current = null
|
contactUpdateTimerRef.current = null
|
||||||
}, 500) // 500ms 防抖,减少更新频率
|
}, 120)
|
||||||
}, [setSessions])
|
}, [setSessions])
|
||||||
|
|
||||||
// 加载一批联系人信息并更新会话列表(优化:使用队列批量更新)
|
// 加载一批联系人信息并更新会话列表(优化:使用队列批量更新)
|
||||||
@@ -885,7 +929,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
setLoadingMessages(true)
|
setLoadingMessages(true)
|
||||||
setMessages([])
|
// 切会话时保留旧内容作为过渡,避免大面积闪烁
|
||||||
|
setHasInitialMessages(true)
|
||||||
} else {
|
} else {
|
||||||
setLoadingMore(true)
|
setLoadingMore(true)
|
||||||
}
|
}
|
||||||
@@ -900,6 +945,9 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
hasMore?: boolean;
|
hasMore?: boolean;
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
if (currentSessionRef.current !== sessionId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (result.success && result.messages) {
|
if (result.success && result.messages) {
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
setMessages(result.messages)
|
setMessages(result.messages)
|
||||||
@@ -996,9 +1044,16 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
console.error('加载消息失败:', e)
|
console.error('加载消息失败:', e)
|
||||||
setConnectionError('加载消息失败')
|
setConnectionError('加载消息失败')
|
||||||
setHasMoreMessages(false)
|
setHasMoreMessages(false)
|
||||||
|
if (offset === 0 && currentSessionRef.current === sessionId) {
|
||||||
|
setMessages([])
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMessages(false)
|
setLoadingMessages(false)
|
||||||
setLoadingMore(false)
|
setLoadingMore(false)
|
||||||
|
if (offset === 0 && pendingSessionLoadRef.current === sessionId) {
|
||||||
|
pendingSessionLoadRef.current = null
|
||||||
|
setIsSessionSwitching(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,11 +1097,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (session.username === currentSessionId) return
|
if (session.username === currentSessionId) return
|
||||||
setCurrentSession(session.username)
|
pendingSessionLoadRef.current = session.username
|
||||||
|
setIsSessionSwitching(true)
|
||||||
|
setCurrentSession(session.username, { preserveMessages: true })
|
||||||
setCurrentOffset(0)
|
setCurrentOffset(0)
|
||||||
setJumpStartTime(0)
|
setJumpStartTime(0)
|
||||||
setJumpEndTime(0)
|
setJumpEndTime(0)
|
||||||
loadMessages(session.username, 0, 0, 0)
|
void loadMessages(session.username, 0, 0, 0)
|
||||||
// 重置详情面板
|
// 重置详情面板
|
||||||
setSessionDetail(null)
|
setSessionDetail(null)
|
||||||
if (showDetailPanel) {
|
if (showDetailPanel) {
|
||||||
@@ -1368,6 +1425,10 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
)
|
)
|
||||||
}, [sessions, searchKeyword, foldedView])
|
}, [sessions, searchKeyword, foldedView])
|
||||||
|
|
||||||
|
const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0
|
||||||
|
const shouldShowSessionsSkeleton = isLoadingSessions && !hasSessionRecords
|
||||||
|
const isSessionListSyncing = (isLoadingSessions || isRefreshingSessions) && hasSessionRecords
|
||||||
|
|
||||||
|
|
||||||
// 格式化会话时间(相对时间)- 使用 useMemo 缓存,避免每次渲染都计算
|
// 格式化会话时间(相对时间)- 使用 useMemo 缓存,避免每次渲染都计算
|
||||||
const formatSessionTime = useCallback((timestamp: number): string => {
|
const formatSessionTime = useCallback((timestamp: number): string => {
|
||||||
@@ -2068,6 +2129,12 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
<button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}>
|
<button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}>
|
||||||
<RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} />
|
<RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} />
|
||||||
</button>
|
</button>
|
||||||
|
{isSessionListSyncing && (
|
||||||
|
<div className="session-sync-indicator">
|
||||||
|
<Loader2 size={12} className="spin" />
|
||||||
|
<span>同步中</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 折叠群 header */}
|
{/* 折叠群 header */}
|
||||||
@@ -2093,7 +2160,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ... (previous content) ... */}
|
{/* ... (previous content) ... */}
|
||||||
{isLoadingSessions ? (
|
{shouldShowSessionsSkeleton ? (
|
||||||
<div className="loading-sessions">
|
<div className="loading-sessions">
|
||||||
{[1, 2, 3, 4, 5].map(i => (
|
{[1, 2, 3, 4, 5].map(i => (
|
||||||
<div key={i} className="skeleton-item">
|
<div key={i} className="skeleton-item">
|
||||||
@@ -2311,11 +2378,11 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'}`}>
|
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'} ${isSessionSwitching ? 'switching' : ''}`}>
|
||||||
{isLoadingMessages && !hasInitialMessages && (
|
{isLoadingMessages && (!hasInitialMessages || isSessionSwitching) && (
|
||||||
<div className="loading-messages loading-overlay">
|
<div className="loading-messages loading-overlay">
|
||||||
<Loader2 size={24} />
|
<Loader2 size={24} />
|
||||||
<span>加载消息中...</span>
|
<span>{isSessionSwitching ? '切换会话中...' : '加载消息中...'}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export interface ChatState {
|
|||||||
setConnectionError: (error: string | null) => void
|
setConnectionError: (error: string | null) => void
|
||||||
setSessions: (sessions: ChatSession[]) => void
|
setSessions: (sessions: ChatSession[]) => void
|
||||||
setFilteredSessions: (sessions: ChatSession[]) => void
|
setFilteredSessions: (sessions: ChatSession[]) => void
|
||||||
setCurrentSession: (sessionId: string | null) => void
|
setCurrentSession: (sessionId: string | null, options?: { preserveMessages?: boolean }) => void
|
||||||
setLoadingSessions: (loading: boolean) => void
|
setLoadingSessions: (loading: boolean) => void
|
||||||
setMessages: (messages: Message[]) => void
|
setMessages: (messages: Message[]) => void
|
||||||
appendMessages: (messages: Message[], prepend?: boolean) => void
|
appendMessages: (messages: Message[], prepend?: boolean) => void
|
||||||
@@ -69,12 +69,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
setSessions: (sessions) => set({ sessions, filteredSessions: sessions }),
|
setSessions: (sessions) => set({ sessions, filteredSessions: sessions }),
|
||||||
setFilteredSessions: (sessions) => set({ filteredSessions: sessions }),
|
setFilteredSessions: (sessions) => set({ filteredSessions: sessions }),
|
||||||
|
|
||||||
setCurrentSession: (sessionId) => set({
|
setCurrentSession: (sessionId, options) => set((state) => ({
|
||||||
currentSessionId: sessionId,
|
currentSessionId: sessionId,
|
||||||
messages: [],
|
messages: options?.preserveMessages ? state.messages : [],
|
||||||
hasMoreMessages: true,
|
hasMoreMessages: true,
|
||||||
hasMoreLater: false
|
hasMoreLater: false
|
||||||
}),
|
})),
|
||||||
|
|
||||||
setLoadingSessions: (loading) => set({ isLoadingSessions: loading }),
|
setLoadingSessions: (loading) => set({ isLoadingSessions: loading }),
|
||||||
|
|
||||||
|
|||||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
@@ -74,6 +74,11 @@ export interface ElectronAPI {
|
|||||||
chat: {
|
chat: {
|
||||||
connect: () => Promise<{ success: boolean; error?: string }>
|
connect: () => Promise<{ success: boolean; error?: string }>
|
||||||
getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }>
|
getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }>
|
||||||
|
getSessionStatuses: (usernames: string[]) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
map?: Record<string, { isFolded?: boolean; isMuted?: boolean }>
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getExportTabCounts: () => Promise<{
|
getExportTabCounts: () => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
counts?: {
|
counts?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user