diff --git a/electron/main.ts b/electron/main.ts index 4bb3c27..872b0ff 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -100,9 +100,38 @@ interface ExportTaskControlState { stopRequested: boolean } +type AnnualReportYearsLoadStrategy = 'cache' | 'native' | 'hybrid' +type AnnualReportYearsLoadPhase = 'cache' | 'native' | 'scan' | 'done' + +interface AnnualReportYearsProgressPayload { + years?: number[] + done: boolean + error?: string + canceled?: boolean + strategy?: AnnualReportYearsLoadStrategy + phase?: AnnualReportYearsLoadPhase + statusText?: string + nativeElapsedMs?: number + scanElapsedMs?: number + totalElapsedMs?: number + switched?: boolean + nativeTimedOut?: boolean +} + +interface AnnualReportYearsTaskState { + cacheKey: string + canceled: boolean + done: boolean + snapshot: AnnualReportYearsProgressPayload + updatedAt: number +} + const exportTaskControlMap = new Map() const pendingExportTaskControlMap = new Map() -const annualReportYearsLoadTasks = new Map() +const annualReportYearsLoadTasks = new Map() +const annualReportYearsTaskByCacheKey = new Map() +const annualReportYearsSnapshotCache = new Map() +const annualReportYearsSnapshotTtlMs = 10 * 60 * 1000 const getTaskControlState = (taskId?: string): ExportTaskControlState | null => { const normalized = typeof taskId === 'string' ? taskId.trim() : '' @@ -122,8 +151,65 @@ const createTaskControlState = (taskId?: string): string | null => { return normalized } +const normalizeAnnualReportYearsSnapshot = (snapshot: AnnualReportYearsProgressPayload): AnnualReportYearsProgressPayload => { + const years = Array.isArray(snapshot.years) ? [...snapshot.years] : [] + return { ...snapshot, years } +} + +const buildAnnualReportYearsCacheKey = (dbPath: string, wxid: string): string => { + return `${String(dbPath || '').trim()}\u0001${String(wxid || '').trim()}` +} + +const pruneAnnualReportYearsSnapshotCache = (): void => { + const now = Date.now() + for (const [cacheKey, entry] of annualReportYearsSnapshotCache.entries()) { + if (now - entry.updatedAt > annualReportYearsSnapshotTtlMs) { + annualReportYearsSnapshotCache.delete(cacheKey) + } + } +} + +const persistAnnualReportYearsSnapshot = ( + cacheKey: string, + taskId: string, + snapshot: AnnualReportYearsProgressPayload +): void => { + annualReportYearsSnapshotCache.set(cacheKey, { + taskId, + snapshot: normalizeAnnualReportYearsSnapshot(snapshot), + updatedAt: Date.now() + }) + pruneAnnualReportYearsSnapshotCache() +} + +const getAnnualReportYearsSnapshot = ( + cacheKey: string +): { taskId: string; snapshot: AnnualReportYearsProgressPayload } | null => { + pruneAnnualReportYearsSnapshotCache() + const entry = annualReportYearsSnapshotCache.get(cacheKey) + if (!entry) return null + return { + taskId: entry.taskId, + snapshot: normalizeAnnualReportYearsSnapshot(entry.snapshot) + } +} + +const broadcastAnnualReportYearsProgress = ( + taskId: string, + payload: AnnualReportYearsProgressPayload +): void => { + for (const win of BrowserWindow.getAllWindows()) { + if (win.isDestroyed()) continue + win.webContents.send('annualReport:availableYearsProgress', { + taskId, + ...payload + }) + } +} + const isYearsLoadCanceled = (taskId: string): boolean => { - return annualReportYearsLoadTasks.get(taskId)?.canceled === true + const task = annualReportYearsLoadTasks.get(taskId) + return task?.canceled === true } const clearTaskControlState = (taskId?: string): void => { @@ -1543,51 +1629,178 @@ function registerIpcHandlers() { const cfg = configService || new ConfigService() configService = cfg - const taskId = `years_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` - annualReportYearsLoadTasks.set(taskId, { canceled: false }) - const sender = event.sender + const dbPath = cfg.get('dbPath') + const decryptKey = cfg.get('decryptKey') + const wxid = cfg.get('myWxid') + const cacheKey = buildAnnualReportYearsCacheKey(dbPath, wxid) - const sendProgress = (payload: { years?: number[]; done: boolean; error?: string; canceled?: boolean }) => { - if (!sender.isDestroyed()) { - sender.send('annualReport:availableYearsProgress', { - taskId, - ...payload - }) + const runningTaskId = annualReportYearsTaskByCacheKey.get(cacheKey) + if (runningTaskId) { + const runningTask = annualReportYearsLoadTasks.get(runningTaskId) + if (runningTask && !runningTask.done) { + return { + success: true, + taskId: runningTaskId, + reused: true, + snapshot: normalizeAnnualReportYearsSnapshot(runningTask.snapshot) + } + } + annualReportYearsTaskByCacheKey.delete(cacheKey) + } + + const cachedSnapshot = getAnnualReportYearsSnapshot(cacheKey) + if (cachedSnapshot && cachedSnapshot.snapshot.done) { + return { + success: true, + taskId: cachedSnapshot.taskId, + reused: true, + snapshot: normalizeAnnualReportYearsSnapshot(cachedSnapshot.snapshot) } } + const taskId = `years_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + const initialSnapshot: AnnualReportYearsProgressPayload = cachedSnapshot?.snapshot && !cachedSnapshot.snapshot.done + ? { + ...normalizeAnnualReportYearsSnapshot(cachedSnapshot.snapshot), + done: false, + canceled: false, + error: undefined + } + : { + years: [], + done: false, + strategy: 'native', + phase: 'native', + statusText: '准备使用原生快速模式加载年份...', + nativeElapsedMs: 0, + scanElapsedMs: 0, + totalElapsedMs: 0, + switched: false, + nativeTimedOut: false + } + + const updateTaskSnapshot = (payload: AnnualReportYearsProgressPayload): AnnualReportYearsProgressPayload | null => { + const task = annualReportYearsLoadTasks.get(taskId) + if (!task) return null + + const hasPayloadYears = Array.isArray(payload.years) + const nextYears = (hasPayloadYears && (payload.done || (payload.years || []).length > 0)) + ? [...(payload.years || [])] + : Array.isArray(task.snapshot.years) ? [...task.snapshot.years] : [] + + const nextSnapshot: AnnualReportYearsProgressPayload = normalizeAnnualReportYearsSnapshot({ + ...task.snapshot, + ...payload, + years: nextYears + }) + task.snapshot = nextSnapshot + task.done = nextSnapshot.done === true + task.updatedAt = Date.now() + annualReportYearsLoadTasks.set(taskId, task) + persistAnnualReportYearsSnapshot(task.cacheKey, taskId, nextSnapshot) + return nextSnapshot + } + + annualReportYearsLoadTasks.set(taskId, { + cacheKey, + canceled: false, + done: false, + snapshot: normalizeAnnualReportYearsSnapshot(initialSnapshot), + updatedAt: Date.now() + }) + annualReportYearsTaskByCacheKey.set(cacheKey, taskId) + persistAnnualReportYearsSnapshot(cacheKey, taskId, initialSnapshot) + void (async () => { try { const result = await annualReportService.getAvailableYears({ - dbPath: cfg.get('dbPath'), - decryptKey: cfg.get('decryptKey'), - wxid: cfg.get('myWxid'), - onProgress: (years) => { + dbPath, + decryptKey, + wxid, + nativeTimeoutMs: 5000, + onProgress: (progress) => { if (isYearsLoadCanceled(taskId)) return - sendProgress({ years, done: false }) + const snapshot = updateTaskSnapshot({ + ...progress, + done: false + }) + if (!snapshot) return + broadcastAnnualReportYearsProgress(taskId, snapshot) }, shouldCancel: () => isYearsLoadCanceled(taskId) }) const canceled = isYearsLoadCanceled(taskId) - annualReportYearsLoadTasks.delete(taskId) if (canceled) { - sendProgress({ done: true, canceled: true }) + const snapshot = updateTaskSnapshot({ + done: true, + canceled: true, + phase: 'done', + statusText: '已取消年份加载' + }) + if (snapshot) { + broadcastAnnualReportYearsProgress(taskId, snapshot) + } return } - if (result.success) { - sendProgress({ years: result.data || [], done: true }) - } else { - sendProgress({ years: result.data || [], done: true, error: result.error || '加载年度数据失败' }) + const completionPayload: AnnualReportYearsProgressPayload = result.success + ? { + years: result.data || [], + done: true, + strategy: result.meta?.strategy, + phase: 'done', + statusText: result.meta?.statusText || '年份数据加载完成', + nativeElapsedMs: result.meta?.nativeElapsedMs, + scanElapsedMs: result.meta?.scanElapsedMs, + totalElapsedMs: result.meta?.totalElapsedMs, + switched: result.meta?.switched, + nativeTimedOut: result.meta?.nativeTimedOut + } + : { + years: result.data || [], + done: true, + error: result.error || '加载年度数据失败', + strategy: result.meta?.strategy, + phase: 'done', + statusText: result.meta?.statusText || '年份数据加载失败', + nativeElapsedMs: result.meta?.nativeElapsedMs, + scanElapsedMs: result.meta?.scanElapsedMs, + totalElapsedMs: result.meta?.totalElapsedMs, + switched: result.meta?.switched, + nativeTimedOut: result.meta?.nativeTimedOut + } + + const snapshot = updateTaskSnapshot(completionPayload) + if (snapshot) { + broadcastAnnualReportYearsProgress(taskId, snapshot) } } catch (e) { + const snapshot = updateTaskSnapshot({ + done: true, + error: String(e), + phase: 'done', + statusText: '年份数据加载失败', + strategy: 'hybrid' + }) + if (snapshot) { + broadcastAnnualReportYearsProgress(taskId, snapshot) + } + } finally { + const task = annualReportYearsLoadTasks.get(taskId) + if (task) { + annualReportYearsTaskByCacheKey.delete(task.cacheKey) + } annualReportYearsLoadTasks.delete(taskId) - sendProgress({ done: true, error: String(e) }) } })() - return { success: true, taskId } + return { + success: true, + taskId, + reused: false, + snapshot: normalizeAnnualReportYearsSnapshot(initialSnapshot) + } }) ipcMain.handle('annualReport:cancelAvailableYearsLoad', async (_, taskId: string) => { diff --git a/electron/preload.ts b/electron/preload.ts index baeb5c2..6857811 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -270,7 +270,21 @@ contextBridge.exposeInMainWorld('electronAPI', { generateReport: (year: number) => ipcRenderer.invoke('annualReport:generateReport', year), exportImages: (payload: { baseDir: string; folderName: string; images: Array<{ name: string; dataUrl: string }> }) => ipcRenderer.invoke('annualReport:exportImages', payload), - onAvailableYearsProgress: (callback: (payload: { taskId: string; years?: number[]; done: boolean; error?: string; canceled?: boolean }) => void) => { + onAvailableYearsProgress: (callback: (payload: { + taskId: string + years?: number[] + done: boolean + error?: string + canceled?: boolean + strategy?: 'cache' | 'native' | 'hybrid' + phase?: 'cache' | 'native' | 'scan' | 'done' + statusText?: string + nativeElapsedMs?: number + scanElapsedMs?: number + totalElapsedMs?: number + switched?: boolean + nativeTimedOut?: boolean + }) => void) => { ipcRenderer.on('annualReport:availableYearsProgress', (_, payload) => callback(payload)) return () => ipcRenderer.removeAllListeners('annualReport:availableYearsProgress') }, diff --git a/electron/services/annualReportService.ts b/electron/services/annualReportService.ts index 4aa7722..f91cfc6 100644 --- a/electron/services/annualReportService.ts +++ b/electron/services/annualReportService.ts @@ -85,6 +85,28 @@ export interface AnnualReportData { } | null } +export interface AvailableYearsLoadProgress { + years: number[] + strategy: 'cache' | 'native' | 'hybrid' + phase: 'cache' | 'native' | 'scan' + statusText: string + nativeElapsedMs: number + scanElapsedMs: number + totalElapsedMs: number + switched?: boolean + nativeTimedOut?: boolean +} + +interface AvailableYearsLoadMeta { + strategy: 'cache' | 'native' | 'hybrid' + nativeElapsedMs: number + scanElapsedMs: number + totalElapsedMs: number + switched: boolean + nativeTimedOut: boolean + statusText: string +} + class AnnualReportService { private readonly availableYearsCacheTtlMs = 10 * 60 * 1000 private readonly availableYearsScanConcurrency = 4 @@ -596,46 +618,222 @@ class AnnualReportService { dbPath: string decryptKey: string wxid: string - onProgress?: (years: number[]) => void + onProgress?: (payload: AvailableYearsLoadProgress) => void shouldCancel?: () => boolean - }): Promise<{ success: boolean; data?: number[]; error?: string }> { + nativeTimeoutMs?: number + }): Promise<{ success: boolean; data?: number[]; error?: string; meta?: AvailableYearsLoadMeta }> { try { const isCancelled = () => params.shouldCancel?.() === true + const totalStartedAt = Date.now() + let nativeElapsedMs = 0 + let scanElapsedMs = 0 + let switched = false + let nativeTimedOut = false + let latestYears: number[] = [] + + const emitProgress = (payload: { + years?: number[] + strategy: 'cache' | 'native' | 'hybrid' + phase: 'cache' | 'native' | 'scan' + statusText: string + switched?: boolean + nativeTimedOut?: boolean + }) => { + if (!params.onProgress) return + if (Array.isArray(payload.years)) latestYears = payload.years + params.onProgress({ + years: latestYears, + strategy: payload.strategy, + phase: payload.phase, + statusText: payload.statusText, + nativeElapsedMs, + scanElapsedMs, + totalElapsedMs: Date.now() - totalStartedAt, + switched: payload.switched ?? switched, + nativeTimedOut: payload.nativeTimedOut ?? nativeTimedOut + }) + } + + const buildMeta = ( + strategy: 'cache' | 'native' | 'hybrid', + statusText: string + ): AvailableYearsLoadMeta => ({ + strategy, + nativeElapsedMs, + scanElapsedMs, + totalElapsedMs: Date.now() - totalStartedAt, + switched, + nativeTimedOut, + statusText + }) + const conn = await this.ensureConnectedWithConfig(params.dbPath, params.decryptKey, params.wxid) - if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error } - if (isCancelled()) return { success: false, error: '已取消加载年份数据' } + if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error, meta: buildMeta('hybrid', '连接数据库失败') } + if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') } const cacheKey = this.buildAvailableYearsCacheKey(params.dbPath, conn.cleanedWxid) const cached = this.getCachedAvailableYears(cacheKey) if (cached) { - params.onProgress?.(cached) - return { success: true, data: cached } + latestYears = cached + emitProgress({ + years: cached, + strategy: 'cache', + phase: 'cache', + statusText: '命中缓存,已快速加载年份数据' + }) + return { + success: true, + data: cached, + meta: buildMeta('cache', '命中缓存,已快速加载年份数据') + } } const sessionIds = await this.getPrivateSessions(conn.cleanedWxid) if (sessionIds.length === 0) { - return { success: false, error: '未找到消息会话' } + return { success: false, error: '未找到消息会话', meta: buildMeta('hybrid', '未找到消息会话') } } - if (isCancelled()) return { success: false, error: '已取消加载年份数据' } + if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') } + + const nativeTimeoutMs = Math.max(1000, Math.floor(params.nativeTimeoutMs || 5000)) + const nativeStartedAt = Date.now() + let nativeTicker: ReturnType | null = null + + emitProgress({ + strategy: 'native', + phase: 'native', + statusText: '正在使用原生快速模式加载年份...' + }) + nativeTicker = setInterval(() => { + nativeElapsedMs = Date.now() - nativeStartedAt + emitProgress({ + strategy: 'native', + phase: 'native', + statusText: '正在使用原生快速模式加载年份...' + }) + }, 120) + + const nativeRace = await Promise.race([ + wcdbService.getAvailableYears(sessionIds) + .then((result) => ({ kind: 'result' as const, result })) + .catch((error) => ({ kind: 'error' as const, error: String(error) })), + new Promise<{ kind: 'timeout' }>((resolve) => setTimeout(() => resolve({ kind: 'timeout' }), nativeTimeoutMs)) + ]) + + if (nativeTicker) { + clearInterval(nativeTicker) + nativeTicker = null + } + nativeElapsedMs = Math.max(nativeElapsedMs, Date.now() - nativeStartedAt) + + if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') } + + if (nativeRace.kind === 'result' && nativeRace.result.success && Array.isArray(nativeRace.result.data) && nativeRace.result.data.length > 0) { + const years = this.normalizeAvailableYears(nativeRace.result.data) + latestYears = years + this.setCachedAvailableYears(cacheKey, years) + emitProgress({ + years, + strategy: 'native', + phase: 'native', + statusText: '原生快速模式加载完成' + }) + return { + success: true, + data: years, + meta: buildMeta('native', '原生快速模式加载完成') + } + } + + switched = true + nativeTimedOut = nativeRace.kind === 'timeout' + emitProgress({ + strategy: 'hybrid', + phase: 'native', + statusText: nativeTimedOut + ? '原生快速模式超时,已自动切换到扫表兼容模式...' + : '原生快速模式不可用,已自动切换到扫表兼容模式...', + switched: true, + nativeTimedOut + }) + + const scanStartedAt = Date.now() + let scanTicker: ReturnType | null = null + scanTicker = setInterval(() => { + scanElapsedMs = Date.now() - scanStartedAt + emitProgress({ + strategy: 'hybrid', + phase: 'scan', + statusText: nativeTimedOut + ? '原生已超时,正在使用扫表兼容模式加载年份...' + : '正在使用扫表兼容模式加载年份...', + switched: true, + nativeTimedOut + }) + }, 120) let years = await this.getAvailableYearsByTableScan(sessionIds, { - onProgress: params.onProgress, + onProgress: (items) => { + latestYears = items + scanElapsedMs = Date.now() - scanStartedAt + emitProgress({ + years: items, + strategy: 'hybrid', + phase: 'scan', + statusText: nativeTimedOut + ? '原生已超时,正在使用扫表兼容模式加载年份...' + : '正在使用扫表兼容模式加载年份...', + switched: true, + nativeTimedOut + }) + }, shouldCancel: params.shouldCancel }) - if (isCancelled()) return { success: false, error: '已取消加载年份数据' } + + if (isCancelled()) { + if (scanTicker) clearInterval(scanTicker) + return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') } + } if (years.length === 0) { - // 扫表失败时,再降级到游标首尾扫描,保证兼容性。 years = await this.getAvailableYearsByEdgeScan(sessionIds, { - onProgress: params.onProgress, + onProgress: (items) => { + latestYears = items + scanElapsedMs = Date.now() - scanStartedAt + emitProgress({ + years: items, + strategy: 'hybrid', + phase: 'scan', + statusText: '扫表结果为空,正在执行游标兜底扫描...', + switched: true, + nativeTimedOut + }) + }, shouldCancel: params.shouldCancel }) } - if (isCancelled()) return { success: false, error: '已取消加载年份数据' } + if (scanTicker) { + clearInterval(scanTicker) + scanTicker = null + } + scanElapsedMs = Math.max(scanElapsedMs, Date.now() - scanStartedAt) + + if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') } this.setCachedAvailableYears(cacheKey, years) - params.onProgress?.(years) - return { success: true, data: years } + latestYears = years + emitProgress({ + years, + strategy: 'hybrid', + phase: 'scan', + statusText: '扫表兼容模式加载完成', + switched: true, + nativeTimedOut + }) + return { + success: true, + data: years, + meta: buildMeta('hybrid', '扫表兼容模式加载完成') + } } catch (e) { - return { success: false, error: String(e) } + return { success: false, error: String(e), meta: { strategy: 'hybrid', nativeElapsedMs: 0, scanElapsedMs: 0, totalElapsedMs: 0, switched: false, nativeTimedOut: false, statusText: '加载年度数据失败' } } } } diff --git a/src/pages/AnnualReportPage.scss b/src/pages/AnnualReportPage.scss index b0c986f..3e7beab 100644 --- a/src/pages/AnnualReportPage.scss +++ b/src/pages/AnnualReportPage.scss @@ -34,6 +34,40 @@ color: var(--text-secondary); } +.load-telemetry { + width: min(760px, 100%); + padding: 12px 14px; + margin: 0 0 28px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); + background: color-mix(in srgb, var(--card-bg) 92%, transparent); + text-align: left; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; + + p { + margin: 4px 0; + } + + .label { + color: var(--text-tertiary); + } +} + +.load-telemetry.loading { + border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color)); +} + +.load-telemetry.complete { + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); +} + +.load-telemetry.compact { + margin: 12px 0 0; + width: min(560px, 100%); +} + .report-sections { display: flex; flex-direction: column; diff --git a/src/pages/AnnualReportPage.tsx b/src/pages/AnnualReportPage.tsx index df7cca5..ded4362 100644 --- a/src/pages/AnnualReportPage.tsx +++ b/src/pages/AnnualReportPage.tsx @@ -4,6 +4,20 @@ import { Calendar, Loader2, Sparkles, Users } from 'lucide-react' import './AnnualReportPage.scss' type YearOption = number | 'all' +type YearsLoadPayload = { + years?: number[] + done: boolean + error?: string + canceled?: boolean + strategy?: 'cache' | 'native' | 'hybrid' + phase?: 'cache' | 'native' | 'scan' | 'done' + statusText?: string + nativeElapsedMs?: number + scanElapsedMs?: number + totalElapsedMs?: number + switched?: boolean + nativeTimedOut?: boolean +} const formatLoadElapsed = (ms: number) => { const totalSeconds = Math.max(0, ms) / 1000 @@ -21,37 +35,36 @@ function AnnualReportPage() { const [isLoading, setIsLoading] = useState(true) const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false) const [hasYearsLoadFinished, setHasYearsLoadFinished] = useState(false) - const [loadElapsedMs, setLoadElapsedMs] = useState(0) + const [loadStrategy, setLoadStrategy] = useState<'cache' | 'native' | 'hybrid'>('native') + const [loadPhase, setLoadPhase] = useState<'cache' | 'native' | 'scan' | 'done'>('native') + const [loadStatusText, setLoadStatusText] = useState('准备加载年份数据...') + const [nativeElapsedMs, setNativeElapsedMs] = useState(0) + const [scanElapsedMs, setScanElapsedMs] = useState(0) + const [totalElapsedMs, setTotalElapsedMs] = useState(0) + const [hasSwitchedStrategy, setHasSwitchedStrategy] = useState(false) + const [nativeTimedOut, setNativeTimedOut] = useState(false) const [isGenerating, setIsGenerating] = useState(false) const [loadError, setLoadError] = useState(null) useEffect(() => { let disposed = false let taskId = '' - const loadStartedAt = Date.now() - let ticker: ReturnType | null = null - const startTicker = () => { - setLoadElapsedMs(0) - if (ticker) clearInterval(ticker) - ticker = setInterval(() => { - if (disposed) return - setLoadElapsedMs(Date.now() - loadStartedAt) - }, 100) - } - - const stopTicker = () => { - setLoadElapsedMs(Date.now() - loadStartedAt) - if (ticker) { - clearInterval(ticker) - ticker = null + const applyLoadPayload = (payload: YearsLoadPayload) => { + if (payload.strategy) setLoadStrategy(payload.strategy) + if (payload.phase) setLoadPhase(payload.phase) + if (typeof payload.statusText === 'string' && payload.statusText) setLoadStatusText(payload.statusText) + if (typeof payload.nativeElapsedMs === 'number' && Number.isFinite(payload.nativeElapsedMs)) { + setNativeElapsedMs(Math.max(0, payload.nativeElapsedMs)) } - } - - const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => { - if (disposed) return - if (taskId && payload.taskId !== taskId) return - if (!taskId) taskId = payload.taskId + if (typeof payload.scanElapsedMs === 'number' && Number.isFinite(payload.scanElapsedMs)) { + setScanElapsedMs(Math.max(0, payload.scanElapsedMs)) + } + if (typeof payload.totalElapsedMs === 'number' && Number.isFinite(payload.totalElapsedMs)) { + setTotalElapsedMs(Math.max(0, payload.totalElapsedMs)) + } + if (typeof payload.switched === 'boolean') setHasSwitchedStrategy(payload.switched) + if (typeof payload.nativeTimedOut === 'boolean') setNativeTimedOut(payload.nativeTimedOut) const years = Array.isArray(payload.years) ? payload.years : [] if (years.length > 0) { @@ -66,7 +79,6 @@ function AnnualReportPage() { if (typeof prev === 'number' && years.includes(prev)) return prev return years[0] }) - // 只要有首批年份可选,就不再阻塞整个页面。 setIsLoading(false) } @@ -78,35 +90,50 @@ function AnnualReportPage() { setIsLoading(false) setIsLoadingMoreYears(false) setHasYearsLoadFinished(true) - stopTicker() + setLoadPhase('done') } else { setIsLoadingMoreYears(true) setHasYearsLoadFinished(false) } + } + + const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => { + if (disposed) return + if (taskId && payload.taskId !== taskId) return + if (!taskId) taskId = payload.taskId + applyLoadPayload(payload) }) const startLoad = async () => { setIsLoading(true) setIsLoadingMoreYears(true) setHasYearsLoadFinished(false) + setLoadStrategy('native') + setLoadPhase('native') + setLoadStatusText('准备使用原生快速模式加载年份...') + setNativeElapsedMs(0) + setScanElapsedMs(0) + setTotalElapsedMs(0) + setHasSwitchedStrategy(false) + setNativeTimedOut(false) setLoadError(null) - startTicker() try { const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad() if (!startResult.success || !startResult.taskId) { setLoadError(startResult.error || '加载年度数据失败') setIsLoading(false) setIsLoadingMoreYears(false) - stopTicker() return } taskId = startResult.taskId + if (startResult.snapshot) { + applyLoadPayload(startResult.snapshot) + } } catch (e) { console.error(e) setLoadError(String(e)) setIsLoading(false) setIsLoadingMoreYears(false) - stopTicker() } } @@ -114,11 +141,7 @@ function AnnualReportPage() { return () => { disposed = true - stopTicker() stopListen() - if (taskId) { - void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId) - } } }, []) @@ -146,7 +169,15 @@ function AnnualReportPage() {

正在加载年份数据(首批)...

-

已耗时 {formatLoadElapsed(loadElapsedMs)}

+
+

加载方式:{getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut })}

+

状态:{loadStatusText || '正在加载年份数据...'}

+

+ 原生耗时:{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '} + 扫表耗时:{formatLoadElapsed(scanElapsedMs)} |{' '} + 总耗时:{formatLoadElapsed(totalElapsedMs)} +

+
) } @@ -174,6 +205,7 @@ function AnnualReportPage() { const loadedYearCount = availableYears.length const isYearStatusComplete = hasYearsLoadFinished + const strategyLabel = getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut }) const renderYearLoadStatus = () => (
{isYearStatusComplete ? ( @@ -194,15 +226,27 @@ function AnnualReportPage() { {loadedYearCount > 0 && (

{isYearStatusComplete ? ( - <>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(loadElapsedMs)} + <>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(totalElapsedMs)} ) : ( <> 已显示 {loadedYearCount} 个年份,正在补充更多年份 - (已耗时 {formatLoadElapsed(loadElapsedMs)}) + (已耗时 {formatLoadElapsed(totalElapsedMs)}) )}

)} +
+

加载方式:{strategyLabel}

+

+ 状态: + {loadStatusText || (isYearStatusComplete ? '全部年份已加载完毕' : '正在加载年份数据...')} +

+

+ 原生耗时:{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '} + 扫表耗时:{formatLoadElapsed(scanElapsedMs)} |{' '} + 总耗时:{formatLoadElapsed(totalElapsedMs)} +

+
@@ -291,4 +335,23 @@ function AnnualReportPage() { ) } +function getStrategyLabel(params: { + loadStrategy: 'cache' | 'native' | 'hybrid' + loadPhase: 'cache' | 'native' | 'scan' | 'done' + hasYearsLoadFinished: boolean + hasSwitchedStrategy: boolean + nativeTimedOut: boolean +}): string { + const { loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut } = params + if (loadStrategy === 'cache') return '缓存模式(快速)' + if (hasYearsLoadFinished) { + if (loadStrategy === 'native') return '原生快速模式' + if (hasSwitchedStrategy || nativeTimedOut) return '混合策略(原生→扫表)' + return '扫表兼容模式' + } + if (loadPhase === 'native') return '原生快速模式(优先)' + if (loadPhase === 'scan') return '扫表兼容模式(回退)' + return '混合策略' +} + export default AnnualReportPage diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 7d2fec7..5265c33 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -486,6 +486,21 @@ export interface ElectronAPI { startAvailableYearsLoad: () => Promise<{ success: boolean taskId?: string + reused?: boolean + snapshot?: { + years?: number[] + done: boolean + error?: string + canceled?: boolean + strategy?: 'cache' | 'native' | 'hybrid' + phase?: 'cache' | 'native' | 'scan' | 'done' + statusText?: string + nativeElapsedMs?: number + scanElapsedMs?: number + totalElapsedMs?: number + switched?: boolean + nativeTimedOut?: boolean + } error?: string }> cancelAvailableYearsLoad: (taskId: string) => Promise<{ @@ -582,6 +597,14 @@ export interface ElectronAPI { done: boolean error?: string canceled?: boolean + strategy?: 'cache' | 'native' | 'hybrid' + phase?: 'cache' | 'native' | 'scan' | 'done' + statusText?: string + nativeElapsedMs?: number + scanElapsedMs?: number + totalElapsedMs?: number + switched?: boolean + nativeTimedOut?: boolean }) => void) => () => void onProgress: (callback: (payload: { status: string; progress: number }) => void) => () => void }