mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
perf(chat): split session detail into fast and extra loading
This commit is contained in:
@@ -986,6 +986,14 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getSessionDetail(sessionId)
|
return chatService.getSessionDetail(sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getSessionDetailFast', async (_, sessionId: string) => {
|
||||||
|
return chatService.getSessionDetailFast(sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getSessionDetailExtra', async (_, sessionId: string) => {
|
||||||
|
return chatService.getSessionDetailExtra(sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:getExportSessionStats', async (_, sessionIds: string[]) => {
|
ipcMain.handle('chat:getExportSessionStats', async (_, sessionIds: string[]) => {
|
||||||
return chatService.getExportSessionStats(sessionIds)
|
return chatService.getExportSessionStats(sessionIds)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
||||||
close: () => ipcRenderer.invoke('chat:close'),
|
close: () => ipcRenderer.invoke('chat:close'),
|
||||||
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
|
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
|
||||||
|
getSessionDetailFast: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailFast', sessionId),
|
||||||
|
getSessionDetailExtra: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailExtra', sessionId),
|
||||||
getExportSessionStats: (sessionIds: string[]) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds),
|
getExportSessionStats: (sessionIds: string[]) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds),
|
||||||
getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId),
|
getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId),
|
||||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ interface ExportTabCounts {
|
|||||||
former_friend: number
|
former_friend: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionDetailFast {
|
||||||
|
wxid: string
|
||||||
|
displayName: string
|
||||||
|
remark?: string
|
||||||
|
nickName?: string
|
||||||
|
alias?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionDetailExtra {
|
||||||
|
firstMessageTime?: number
|
||||||
|
latestMessageTime?: number
|
||||||
|
messageTables: { dbName: string; tableName: string; count: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionDetail = SessionDetailFast & SessionDetailExtra
|
||||||
|
|
||||||
// 表情包缓存
|
// 表情包缓存
|
||||||
const emojiCache: Map<string, string> = new Map()
|
const emojiCache: Map<string, string> = new Map()
|
||||||
const emojiDownloading: Map<string, Promise<string | null>> = new Map()
|
const emojiDownloading: Map<string, Promise<string | null>> = new Map()
|
||||||
@@ -201,6 +219,10 @@ 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 sessionDetailFastCache = new Map<string, { detail: SessionDetailFast; updatedAt: number }>()
|
||||||
|
private sessionDetailExtraCache = new Map<string, { detail: SessionDetailExtra; updatedAt: number }>()
|
||||||
|
private readonly sessionDetailFastCacheTtlMs = 60 * 1000
|
||||||
|
private readonly sessionDetailExtraCacheTtlMs = 5 * 60 * 1000
|
||||||
private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>()
|
private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>()
|
||||||
private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000
|
private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000
|
||||||
|
|
||||||
@@ -1565,6 +1587,8 @@ class ChatService {
|
|||||||
this.sessionMessageCountCacheScope = scope
|
this.sessionMessageCountCacheScope = scope
|
||||||
this.sessionMessageCountCache.clear()
|
this.sessionMessageCountCache.clear()
|
||||||
this.sessionMessageCountHintCache.clear()
|
this.sessionMessageCountHintCache.clear()
|
||||||
|
this.sessionDetailFastCache.clear()
|
||||||
|
this.sessionDetailExtraCache.clear()
|
||||||
this.sessionStatusCache.clear()
|
this.sessionStatusCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3819,20 +3843,9 @@ class ChatService {
|
|||||||
/**
|
/**
|
||||||
* 获取会话详情信息
|
* 获取会话详情信息
|
||||||
*/
|
*/
|
||||||
async getSessionDetail(sessionId: string): Promise<{
|
async getSessionDetailFast(sessionId: string): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
detail?: {
|
detail?: SessionDetailFast
|
||||||
wxid: string
|
|
||||||
displayName: string
|
|
||||||
remark?: string
|
|
||||||
nickName?: string
|
|
||||||
alias?: string
|
|
||||||
avatarUrl?: string
|
|
||||||
messageCount: number
|
|
||||||
firstMessageTime?: number
|
|
||||||
latestMessageTime?: number
|
|
||||||
messageTables: { dbName: string; tableName: string; count: number }[]
|
|
||||||
}
|
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
@@ -3840,53 +3853,152 @@ class ChatService {
|
|||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
return { success: false, error: connectResult.error || '数据库未连接' }
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
}
|
}
|
||||||
|
this.refreshSessionMessageCountCacheScope()
|
||||||
|
|
||||||
let displayName = sessionId
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
|
if (!normalizedSessionId) {
|
||||||
|
return { success: false, error: '会话ID不能为空' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const cachedDetail = this.sessionDetailFastCache.get(normalizedSessionId)
|
||||||
|
if (cachedDetail && now - cachedDetail.updatedAt <= this.sessionDetailFastCacheTtlMs) {
|
||||||
|
return { success: true, detail: cachedDetail.detail }
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = normalizedSessionId
|
||||||
let remark: string | undefined
|
let remark: string | undefined
|
||||||
let nickName: string | undefined
|
let nickName: string | undefined
|
||||||
let alias: string | undefined
|
let alias: string | undefined
|
||||||
let avatarUrl: string | undefined
|
let avatarUrl: string | undefined
|
||||||
|
const cachedContact = this.avatarCache.get(normalizedSessionId)
|
||||||
const contactResult = await wcdbService.getContact(sessionId)
|
if (cachedContact) {
|
||||||
if (contactResult.success && contactResult.contact) {
|
displayName = cachedContact.displayName || normalizedSessionId
|
||||||
remark = contactResult.contact.remark || undefined
|
avatarUrl = cachedContact.avatarUrl
|
||||||
nickName = contactResult.contact.nickName || undefined
|
|
||||||
alias = contactResult.contact.alias || undefined
|
|
||||||
displayName = remark || nickName || alias || sessionId
|
|
||||||
}
|
|
||||||
const avatarResult = await wcdbService.getAvatarUrls([sessionId])
|
|
||||||
if (avatarResult.success && avatarResult.map) {
|
|
||||||
avatarUrl = avatarResult.map[sessionId]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const countResult = await wcdbService.getMessageCount(sessionId)
|
const [contactResult, avatarResult] = await Promise.allSettled([
|
||||||
const totalMessageCount = countResult.success && countResult.count ? countResult.count : 0
|
wcdbService.getContact(normalizedSessionId),
|
||||||
|
avatarUrl ? Promise.resolve({ success: true, map: { [normalizedSessionId]: avatarUrl } }) : wcdbService.getAvatarUrls([normalizedSessionId])
|
||||||
|
])
|
||||||
|
|
||||||
let firstMessageTime: number | undefined
|
if (contactResult.status === 'fulfilled' && contactResult.value.success && contactResult.value.contact) {
|
||||||
let latestMessageTime: number | undefined
|
remark = contactResult.value.contact.remark || undefined
|
||||||
|
nickName = contactResult.value.contact.nickName || undefined
|
||||||
|
alias = contactResult.value.contact.alias || undefined
|
||||||
|
displayName = remark || nickName || alias || displayName
|
||||||
|
}
|
||||||
|
|
||||||
const earliestCursor = await wcdbService.openMessageCursor(sessionId, 1, true, 0, 0)
|
if (avatarResult.status === 'fulfilled' && avatarResult.value.success && avatarResult.value.map) {
|
||||||
if (earliestCursor.success && earliestCursor.cursor) {
|
avatarUrl = avatarResult.value.map[normalizedSessionId]
|
||||||
const batch = await wcdbService.fetchMessageBatch(earliestCursor.cursor)
|
}
|
||||||
if (batch.success && batch.rows && batch.rows.length > 0) {
|
|
||||||
firstMessageTime = parseInt(batch.rows[0].create_time || '0', 10) || undefined
|
let messageCount: number | undefined
|
||||||
|
const cachedCount = this.sessionMessageCountCache.get(normalizedSessionId)
|
||||||
|
if (cachedCount && now - cachedCount.updatedAt <= this.sessionMessageCountCacheTtlMs) {
|
||||||
|
messageCount = cachedCount.count
|
||||||
|
} else {
|
||||||
|
const hintCount = this.sessionMessageCountHintCache.get(normalizedSessionId)
|
||||||
|
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
|
||||||
|
messageCount = Math.floor(hintCount)
|
||||||
|
this.sessionMessageCountCache.set(normalizedSessionId, {
|
||||||
|
count: messageCount,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
}
|
}
|
||||||
await wcdbService.closeMessageCursor(earliestCursor.cursor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestCursor = await wcdbService.openMessageCursor(sessionId, 1, false, 0, 0)
|
if (!Number.isFinite(messageCount)) {
|
||||||
if (latestCursor.success && latestCursor.cursor) {
|
const countResult = await wcdbService.getMessageCount(normalizedSessionId)
|
||||||
const batch = await wcdbService.fetchMessageBatch(latestCursor.cursor)
|
messageCount = countResult.success && Number.isFinite(countResult.count)
|
||||||
if (batch.success && batch.rows && batch.rows.length > 0) {
|
? Math.max(0, Math.floor(countResult.count || 0))
|
||||||
latestMessageTime = parseInt(batch.rows[0].create_time || '0', 10) || undefined
|
: 0
|
||||||
}
|
this.sessionMessageCountCache.set(normalizedSessionId, {
|
||||||
await wcdbService.closeMessageCursor(latestCursor.cursor)
|
count: messageCount,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detail: SessionDetailFast = {
|
||||||
|
wxid: normalizedSessionId,
|
||||||
|
displayName,
|
||||||
|
remark,
|
||||||
|
nickName,
|
||||||
|
alias,
|
||||||
|
avatarUrl,
|
||||||
|
messageCount: Math.max(0, Math.floor(messageCount || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessionDetailFastCache.set(normalizedSessionId, {
|
||||||
|
detail,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, detail }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ChatService: 获取会话详情快速信息失败:', e)
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBoundaryMessageTime(sessionId: string, ascending: boolean): Promise<number | undefined> {
|
||||||
|
const cursorResult = await wcdbService.openMessageCursor(sessionId, 1, ascending, 0, 0)
|
||||||
|
if (!cursorResult.success || !cursorResult.cursor) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursor = cursorResult.cursor
|
||||||
|
try {
|
||||||
|
const batch = await wcdbService.fetchMessageBatch(cursor)
|
||||||
|
if (!batch.success || !batch.rows || batch.rows.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const ts = parseInt(batch.rows[0].create_time || '0', 10)
|
||||||
|
return Number.isFinite(ts) && ts > 0 ? ts : undefined
|
||||||
|
} finally {
|
||||||
|
await wcdbService.closeMessageCursor(cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionDetailExtra(sessionId: string): Promise<{
|
||||||
|
success: boolean
|
||||||
|
detail?: SessionDetailExtra
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const connectResult = await this.ensureConnected()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
|
}
|
||||||
|
this.refreshSessionMessageCountCacheScope()
|
||||||
|
|
||||||
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
|
if (!normalizedSessionId) {
|
||||||
|
return { success: false, error: '会话ID不能为空' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const cachedDetail = this.sessionDetailExtraCache.get(normalizedSessionId)
|
||||||
|
if (cachedDetail && now - cachedDetail.updatedAt <= this.sessionDetailExtraCacheTtlMs) {
|
||||||
|
return { success: true, detail: cachedDetail.detail }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstMessageTimeResult, latestMessageTimeResult, tableStatsResult] = await Promise.allSettled([
|
||||||
|
this.getBoundaryMessageTime(normalizedSessionId, true),
|
||||||
|
this.getBoundaryMessageTime(normalizedSessionId, false),
|
||||||
|
wcdbService.getMessageTableStats(normalizedSessionId)
|
||||||
|
])
|
||||||
|
|
||||||
|
const firstMessageTime = firstMessageTimeResult.status === 'fulfilled'
|
||||||
|
? firstMessageTimeResult.value
|
||||||
|
: undefined
|
||||||
|
const latestMessageTime = latestMessageTimeResult.status === 'fulfilled'
|
||||||
|
? latestMessageTimeResult.value
|
||||||
|
: undefined
|
||||||
|
|
||||||
const messageTables: { dbName: string; tableName: string; count: number }[] = []
|
const messageTables: { dbName: string; tableName: string; count: number }[] = []
|
||||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
if (tableStatsResult.status === 'fulfilled' && tableStatsResult.value.success && tableStatsResult.value.tables) {
|
||||||
if (tableStats.success && tableStats.tables) {
|
for (const row of tableStatsResult.value.tables) {
|
||||||
for (const row of tableStats.tables) {
|
|
||||||
messageTables.push({
|
messageTables.push({
|
||||||
dbName: basename(row.db_path || ''),
|
dbName: basename(row.db_path || ''),
|
||||||
tableName: row.table_name || '',
|
tableName: row.table_name || '',
|
||||||
@@ -3895,21 +4007,49 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detail: SessionDetailExtra = {
|
||||||
|
firstMessageTime,
|
||||||
|
latestMessageTime,
|
||||||
|
messageTables
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessionDetailExtraCache.set(normalizedSessionId, {
|
||||||
|
detail,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
detail: {
|
detail
|
||||||
wxid: sessionId,
|
|
||||||
displayName,
|
|
||||||
remark,
|
|
||||||
nickName,
|
|
||||||
alias,
|
|
||||||
avatarUrl,
|
|
||||||
messageCount: totalMessageCount,
|
|
||||||
firstMessageTime,
|
|
||||||
latestMessageTime,
|
|
||||||
messageTables
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ChatService: 获取会话详情补充统计失败:', e)
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionDetail(sessionId: string): Promise<{
|
||||||
|
success: boolean
|
||||||
|
detail?: SessionDetail
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const fastResult = await this.getSessionDetailFast(sessionId)
|
||||||
|
if (!fastResult.success || !fastResult.detail) {
|
||||||
|
return { success: false, error: fastResult.error || '获取会话详情失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraResult = await this.getSessionDetailExtra(sessionId)
|
||||||
|
const detail: SessionDetail = {
|
||||||
|
...fastResult.detail,
|
||||||
|
firstMessageTime: extraResult.success ? extraResult.detail?.firstMessageTime : undefined,
|
||||||
|
latestMessageTime: extraResult.success ? extraResult.detail?.latestMessageTime : undefined,
|
||||||
|
messageTables: extraResult.success && extraResult.detail?.messageTables
|
||||||
|
? extraResult.detail.messageTables
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, detail }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 获取会话详情失败:', e)
|
console.error('ChatService: 获取会话详情失败:', e)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: String(e) }
|
||||||
|
|||||||
@@ -2766,6 +2766,14 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-table-placeholder {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.table-item {
|
.table-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const [showDetailPanel, setShowDetailPanel] = useState(false)
|
const [showDetailPanel, setShowDetailPanel] = useState(false)
|
||||||
const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null)
|
const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null)
|
||||||
const [isLoadingDetail, setIsLoadingDetail] = useState(false)
|
const [isLoadingDetail, setIsLoadingDetail] = useState(false)
|
||||||
|
const [isLoadingDetailExtra, setIsLoadingDetailExtra] = useState(false)
|
||||||
const [copiedField, setCopiedField] = useState<string | null>(null)
|
const [copiedField, setCopiedField] = useState<string | null>(null)
|
||||||
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
|
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
|
||||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
||||||
@@ -386,6 +387,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const searchKeywordRef = useRef('')
|
const searchKeywordRef = useRef('')
|
||||||
const preloadImageKeysRef = useRef<Set<string>>(new Set())
|
const preloadImageKeysRef = useRef<Set<string>>(new Set())
|
||||||
const lastPreloadSessionRef = useRef<string | null>(null)
|
const lastPreloadSessionRef = useRef<string | null>(null)
|
||||||
|
const detailRequestSeqRef = useRef(0)
|
||||||
|
|
||||||
// 加载当前用户头像
|
// 加载当前用户头像
|
||||||
const loadMyAvatar = useCallback(async () => {
|
const loadMyAvatar = useCallback(async () => {
|
||||||
@@ -401,25 +403,91 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 加载会话详情
|
// 加载会话详情
|
||||||
const loadSessionDetail = useCallback(async (sessionId: string) => {
|
const loadSessionDetail = useCallback(async (sessionId: string) => {
|
||||||
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
|
if (!normalizedSessionId) return
|
||||||
|
|
||||||
|
const requestSeq = ++detailRequestSeqRef.current
|
||||||
|
const mappedSession = sessionMapRef.current.get(normalizedSessionId) || sessionsRef.current.find((s) => s.username === normalizedSessionId)
|
||||||
|
const hintedCount = typeof mappedSession?.messageCountHint === 'number' && Number.isFinite(mappedSession.messageCountHint) && mappedSession.messageCountHint >= 0
|
||||||
|
? Math.floor(mappedSession.messageCountHint)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
const sameSession = prev?.wxid === normalizedSessionId
|
||||||
|
return {
|
||||||
|
wxid: normalizedSessionId,
|
||||||
|
displayName: mappedSession?.displayName || prev?.displayName || normalizedSessionId,
|
||||||
|
remark: sameSession ? prev?.remark : undefined,
|
||||||
|
nickName: sameSession ? prev?.nickName : undefined,
|
||||||
|
alias: sameSession ? prev?.alias : undefined,
|
||||||
|
avatarUrl: mappedSession?.avatarUrl || (sameSession ? prev?.avatarUrl : undefined),
|
||||||
|
messageCount: hintedCount ?? (sameSession ? prev.messageCount : Number.NaN),
|
||||||
|
firstMessageTime: sameSession ? prev?.firstMessageTime : undefined,
|
||||||
|
latestMessageTime: sameSession ? prev?.latestMessageTime : undefined,
|
||||||
|
messageTables: sameSession && Array.isArray(prev?.messageTables) ? prev.messageTables : []
|
||||||
|
}
|
||||||
|
})
|
||||||
setIsLoadingDetail(true)
|
setIsLoadingDetail(true)
|
||||||
|
setIsLoadingDetailExtra(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.chat.getSessionDetail(sessionId)
|
const result = await window.electronAPI.chat.getSessionDetailFast(normalizedSessionId)
|
||||||
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
if (result.success && result.detail) {
|
if (result.success && result.detail) {
|
||||||
setSessionDetail(result.detail)
|
setSessionDetail((prev) => ({
|
||||||
|
wxid: normalizedSessionId,
|
||||||
|
displayName: result.detail!.displayName || prev?.displayName || normalizedSessionId,
|
||||||
|
remark: result.detail!.remark,
|
||||||
|
nickName: result.detail!.nickName,
|
||||||
|
alias: result.detail!.alias,
|
||||||
|
avatarUrl: result.detail!.avatarUrl || prev?.avatarUrl,
|
||||||
|
messageCount: Number.isFinite(result.detail!.messageCount) ? result.detail!.messageCount : prev?.messageCount ?? Number.NaN,
|
||||||
|
firstMessageTime: prev?.firstMessageTime,
|
||||||
|
latestMessageTime: prev?.latestMessageTime,
|
||||||
|
messageTables: Array.isArray(prev?.messageTables) ? (prev?.messageTables || []) : []
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载会话详情失败:', e)
|
console.error('加载会话详情失败:', e)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingDetail(false)
|
if (requestSeq === detailRequestSeqRef.current) {
|
||||||
|
setIsLoadingDetail(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.chat.getSessionDetailExtra(normalizedSessionId)
|
||||||
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
if (result.success && result.detail) {
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
firstMessageTime: result.detail!.firstMessageTime,
|
||||||
|
latestMessageTime: result.detail!.latestMessageTime,
|
||||||
|
messageTables: Array.isArray(result.detail!.messageTables) ? result.detail!.messageTables : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载会话详情补充统计失败:', e)
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === detailRequestSeqRef.current) {
|
||||||
|
setIsLoadingDetailExtra(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 切换详情面板
|
// 切换详情面板
|
||||||
const toggleDetailPanel = useCallback(() => {
|
const toggleDetailPanel = useCallback(() => {
|
||||||
if (!showDetailPanel && currentSessionId) {
|
if (showDetailPanel) {
|
||||||
loadSessionDetail(currentSessionId)
|
setShowDetailPanel(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setShowDetailPanel(true)
|
||||||
|
if (currentSessionId) {
|
||||||
|
void loadSessionDetail(currentSessionId)
|
||||||
}
|
}
|
||||||
setShowDetailPanel(!showDetailPanel)
|
|
||||||
}, [showDetailPanel, currentSessionId, loadSessionDetail])
|
}, [showDetailPanel, currentSessionId, loadSessionDetail])
|
||||||
|
|
||||||
// 复制字段值到剪贴板
|
// 复制字段值到剪贴板
|
||||||
@@ -1107,7 +1175,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
// 重置详情面板
|
// 重置详情面板
|
||||||
setSessionDetail(null)
|
setSessionDetail(null)
|
||||||
if (showDetailPanel) {
|
if (showDetailPanel) {
|
||||||
loadSessionDetail(session.username)
|
void loadSessionDetail(session.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2475,7 +2543,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isLoadingDetail ? (
|
{isLoadingDetail && !sessionDetail ? (
|
||||||
<div className="detail-loading">
|
<div className="detail-loading">
|
||||||
<Loader2 size={20} className="spin" />
|
<Loader2 size={20} className="spin" />
|
||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
@@ -2530,39 +2598,35 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
<span className="value highlight">
|
<span className="value highlight">
|
||||||
{Number.isFinite(sessionDetail.messageCount)
|
{Number.isFinite(sessionDetail.messageCount)
|
||||||
? sessionDetail.messageCount.toLocaleString()
|
? sessionDetail.messageCount.toLocaleString()
|
||||||
: '—'}
|
: (isLoadingDetail ? '统计中...' : '—')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<Calendar size={14} />
|
||||||
|
<span className="label">首条消息</span>
|
||||||
|
<span className="value">
|
||||||
|
{Number.isFinite(sessionDetail.firstMessageTime)
|
||||||
|
? new Date((sessionDetail.firstMessageTime as number) * 1000).toLocaleDateString('zh-CN')
|
||||||
|
: (isLoadingDetailExtra ? '统计中...' : '—')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<Calendar size={14} />
|
||||||
|
<span className="label">最新消息</span>
|
||||||
|
<span className="value">
|
||||||
|
{Number.isFinite(sessionDetail.latestMessageTime)
|
||||||
|
? new Date((sessionDetail.latestMessageTime as number) * 1000).toLocaleDateString('zh-CN')
|
||||||
|
: (isLoadingDetailExtra ? '统计中...' : '—')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{sessionDetail.firstMessageTime && (
|
|
||||||
<div className="detail-item">
|
|
||||||
<Calendar size={14} />
|
|
||||||
<span className="label">首条消息</span>
|
|
||||||
<span className="value">
|
|
||||||
{Number.isFinite(sessionDetail.firstMessageTime)
|
|
||||||
? new Date(sessionDetail.firstMessageTime * 1000).toLocaleDateString('zh-CN')
|
|
||||||
: '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{sessionDetail.latestMessageTime && (
|
|
||||||
<div className="detail-item">
|
|
||||||
<Calendar size={14} />
|
|
||||||
<span className="label">最新消息</span>
|
|
||||||
<span className="value">
|
|
||||||
{Number.isFinite(sessionDetail.latestMessageTime)
|
|
||||||
? new Date(sessionDetail.latestMessageTime * 1000).toLocaleDateString('zh-CN')
|
|
||||||
: '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Array.isArray(sessionDetail.messageTables) && sessionDetail.messageTables.length > 0 && (
|
<div className="detail-section">
|
||||||
<div className="detail-section">
|
<div className="section-title">
|
||||||
<div className="section-title">
|
<Database size={14} />
|
||||||
<Database size={14} />
|
<span>数据库分布</span>
|
||||||
<span>数据库分布</span>
|
</div>
|
||||||
</div>
|
{Array.isArray(sessionDetail.messageTables) && sessionDetail.messageTables.length > 0 ? (
|
||||||
<div className="table-list">
|
<div className="table-list">
|
||||||
{sessionDetail.messageTables.map((t, i) => (
|
{sessionDetail.messageTables.map((t, i) => (
|
||||||
<div key={i} className="table-item">
|
<div key={i} className="table-item">
|
||||||
@@ -2571,8 +2635,12 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="detail-table-placeholder">
|
||||||
|
{isLoadingDetailExtra ? '统计中...' : '暂无统计数据'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-empty">暂无详情</div>
|
<div className="detail-empty">暂无详情</div>
|
||||||
|
|||||||
22
src/types/electron.d.ts
vendored
22
src/types/electron.d.ts
vendored
@@ -144,6 +144,28 @@ export interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getSessionDetailFast: (sessionId: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
detail?: {
|
||||||
|
wxid: string
|
||||||
|
displayName: string
|
||||||
|
remark?: string
|
||||||
|
nickName?: string
|
||||||
|
alias?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
|
getSessionDetailExtra: (sessionId: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
detail?: {
|
||||||
|
firstMessageTime?: number
|
||||||
|
latestMessageTime?: number
|
||||||
|
messageTables: { dbName: string; tableName: string; count: number }[]
|
||||||
|
}
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getExportSessionStats: (sessionIds: string[]) => Promise<{
|
getExportSessionStats: (sessionIds: string[]) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
data?: Record<string, {
|
data?: Record<string, {
|
||||||
|
|||||||
Reference in New Issue
Block a user