mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
feat(report): reuse years loading across page switches
This commit is contained in:
261
electron/main.ts
261
electron/main.ts
@@ -100,9 +100,38 @@ interface ExportTaskControlState {
|
|||||||
stopRequested: boolean
|
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<string, ExportTaskControlState>()
|
const exportTaskControlMap = new Map<string, ExportTaskControlState>()
|
||||||
const pendingExportTaskControlMap = new Map<string, ExportTaskControlState>()
|
const pendingExportTaskControlMap = new Map<string, ExportTaskControlState>()
|
||||||
const annualReportYearsLoadTasks = new Map<string, { canceled: boolean }>()
|
const annualReportYearsLoadTasks = new Map<string, AnnualReportYearsTaskState>()
|
||||||
|
const annualReportYearsTaskByCacheKey = new Map<string, string>()
|
||||||
|
const annualReportYearsSnapshotCache = new Map<string, { snapshot: AnnualReportYearsProgressPayload; updatedAt: number; taskId: string }>()
|
||||||
|
const annualReportYearsSnapshotTtlMs = 10 * 60 * 1000
|
||||||
|
|
||||||
const getTaskControlState = (taskId?: string): ExportTaskControlState | null => {
|
const getTaskControlState = (taskId?: string): ExportTaskControlState | null => {
|
||||||
const normalized = typeof taskId === 'string' ? taskId.trim() : ''
|
const normalized = typeof taskId === 'string' ? taskId.trim() : ''
|
||||||
@@ -122,8 +151,65 @@ const createTaskControlState = (taskId?: string): string | null => {
|
|||||||
return normalized
|
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 => {
|
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 => {
|
const clearTaskControlState = (taskId?: string): void => {
|
||||||
@@ -1543,51 +1629,178 @@ function registerIpcHandlers() {
|
|||||||
const cfg = configService || new ConfigService()
|
const cfg = configService || new ConfigService()
|
||||||
configService = cfg
|
configService = cfg
|
||||||
|
|
||||||
const taskId = `years_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
const dbPath = cfg.get('dbPath')
|
||||||
annualReportYearsLoadTasks.set(taskId, { canceled: false })
|
const decryptKey = cfg.get('decryptKey')
|
||||||
const sender = event.sender
|
const wxid = cfg.get('myWxid')
|
||||||
|
const cacheKey = buildAnnualReportYearsCacheKey(dbPath, wxid)
|
||||||
|
|
||||||
const sendProgress = (payload: { years?: number[]; done: boolean; error?: string; canceled?: boolean }) => {
|
const runningTaskId = annualReportYearsTaskByCacheKey.get(cacheKey)
|
||||||
if (!sender.isDestroyed()) {
|
if (runningTaskId) {
|
||||||
sender.send('annualReport:availableYearsProgress', {
|
const runningTask = annualReportYearsLoadTasks.get(runningTaskId)
|
||||||
taskId,
|
if (runningTask && !runningTask.done) {
|
||||||
...payload
|
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 () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const result = await annualReportService.getAvailableYears({
|
const result = await annualReportService.getAvailableYears({
|
||||||
dbPath: cfg.get('dbPath'),
|
dbPath,
|
||||||
decryptKey: cfg.get('decryptKey'),
|
decryptKey,
|
||||||
wxid: cfg.get('myWxid'),
|
wxid,
|
||||||
onProgress: (years) => {
|
nativeTimeoutMs: 5000,
|
||||||
|
onProgress: (progress) => {
|
||||||
if (isYearsLoadCanceled(taskId)) return
|
if (isYearsLoadCanceled(taskId)) return
|
||||||
sendProgress({ years, done: false })
|
const snapshot = updateTaskSnapshot({
|
||||||
|
...progress,
|
||||||
|
done: false
|
||||||
|
})
|
||||||
|
if (!snapshot) return
|
||||||
|
broadcastAnnualReportYearsProgress(taskId, snapshot)
|
||||||
},
|
},
|
||||||
shouldCancel: () => isYearsLoadCanceled(taskId)
|
shouldCancel: () => isYearsLoadCanceled(taskId)
|
||||||
})
|
})
|
||||||
|
|
||||||
const canceled = isYearsLoadCanceled(taskId)
|
const canceled = isYearsLoadCanceled(taskId)
|
||||||
annualReportYearsLoadTasks.delete(taskId)
|
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
sendProgress({ done: true, canceled: true })
|
const snapshot = updateTaskSnapshot({
|
||||||
|
done: true,
|
||||||
|
canceled: true,
|
||||||
|
phase: 'done',
|
||||||
|
statusText: '已取消年份加载'
|
||||||
|
})
|
||||||
|
if (snapshot) {
|
||||||
|
broadcastAnnualReportYearsProgress(taskId, snapshot)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.success) {
|
const completionPayload: AnnualReportYearsProgressPayload = result.success
|
||||||
sendProgress({ years: result.data || [], done: true })
|
? {
|
||||||
} else {
|
years: result.data || [],
|
||||||
sendProgress({ years: result.data || [], done: true, error: result.error || '加载年度数据失败' })
|
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) {
|
} 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)
|
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) => {
|
ipcMain.handle('annualReport:cancelAvailableYearsLoad', async (_, taskId: string) => {
|
||||||
|
|||||||
@@ -270,7 +270,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
generateReport: (year: number) => ipcRenderer.invoke('annualReport:generateReport', year),
|
generateReport: (year: number) => ipcRenderer.invoke('annualReport:generateReport', year),
|
||||||
exportImages: (payload: { baseDir: string; folderName: string; images: Array<{ name: string; dataUrl: string }> }) =>
|
exportImages: (payload: { baseDir: string; folderName: string; images: Array<{ name: string; dataUrl: string }> }) =>
|
||||||
ipcRenderer.invoke('annualReport:exportImages', payload),
|
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))
|
ipcRenderer.on('annualReport:availableYearsProgress', (_, payload) => callback(payload))
|
||||||
return () => ipcRenderer.removeAllListeners('annualReport:availableYearsProgress')
|
return () => ipcRenderer.removeAllListeners('annualReport:availableYearsProgress')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,6 +85,28 @@ export interface AnnualReportData {
|
|||||||
} | null
|
} | 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 {
|
class AnnualReportService {
|
||||||
private readonly availableYearsCacheTtlMs = 10 * 60 * 1000
|
private readonly availableYearsCacheTtlMs = 10 * 60 * 1000
|
||||||
private readonly availableYearsScanConcurrency = 4
|
private readonly availableYearsScanConcurrency = 4
|
||||||
@@ -596,46 +618,222 @@ class AnnualReportService {
|
|||||||
dbPath: string
|
dbPath: string
|
||||||
decryptKey: string
|
decryptKey: string
|
||||||
wxid: string
|
wxid: string
|
||||||
onProgress?: (years: number[]) => void
|
onProgress?: (payload: AvailableYearsLoadProgress) => void
|
||||||
shouldCancel?: () => boolean
|
shouldCancel?: () => boolean
|
||||||
}): Promise<{ success: boolean; data?: number[]; error?: string }> {
|
nativeTimeoutMs?: number
|
||||||
|
}): Promise<{ success: boolean; data?: number[]; error?: string; meta?: AvailableYearsLoadMeta }> {
|
||||||
try {
|
try {
|
||||||
const isCancelled = () => params.shouldCancel?.() === true
|
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)
|
const conn = await this.ensureConnectedWithConfig(params.dbPath, params.decryptKey, params.wxid)
|
||||||
if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error }
|
if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error, meta: buildMeta('hybrid', '连接数据库失败') }
|
||||||
if (isCancelled()) return { success: false, error: '已取消加载年份数据' }
|
if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }
|
||||||
const cacheKey = this.buildAvailableYearsCacheKey(params.dbPath, conn.cleanedWxid)
|
const cacheKey = this.buildAvailableYearsCacheKey(params.dbPath, conn.cleanedWxid)
|
||||||
const cached = this.getCachedAvailableYears(cacheKey)
|
const cached = this.getCachedAvailableYears(cacheKey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
params.onProgress?.(cached)
|
latestYears = cached
|
||||||
return { success: true, data: cached }
|
emitProgress({
|
||||||
|
years: cached,
|
||||||
|
strategy: 'cache',
|
||||||
|
phase: 'cache',
|
||||||
|
statusText: '命中缓存,已快速加载年份数据'
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: cached,
|
||||||
|
meta: buildMeta('cache', '命中缓存,已快速加载年份数据')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionIds = await this.getPrivateSessions(conn.cleanedWxid)
|
const sessionIds = await this.getPrivateSessions(conn.cleanedWxid)
|
||||||
if (sessionIds.length === 0) {
|
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<typeof setInterval> | 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<typeof setInterval> | 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, {
|
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
|
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) {
|
if (years.length === 0) {
|
||||||
// 扫表失败时,再降级到游标首尾扫描,保证兼容性。
|
|
||||||
years = await this.getAvailableYearsByEdgeScan(sessionIds, {
|
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
|
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)
|
this.setCachedAvailableYears(cacheKey, years)
|
||||||
params.onProgress?.(years)
|
latestYears = years
|
||||||
return { success: true, data: years }
|
emitProgress({
|
||||||
|
years,
|
||||||
|
strategy: 'hybrid',
|
||||||
|
phase: 'scan',
|
||||||
|
statusText: '扫表兼容模式加载完成',
|
||||||
|
switched: true,
|
||||||
|
nativeTimedOut
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: years,
|
||||||
|
meta: buildMeta('hybrid', '扫表兼容模式加载完成')
|
||||||
|
}
|
||||||
} catch (e) {
|
} 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: '加载年度数据失败' } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,40 @@
|
|||||||
color: var(--text-secondary);
|
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 {
|
.report-sections {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ import { Calendar, Loader2, Sparkles, Users } from 'lucide-react'
|
|||||||
import './AnnualReportPage.scss'
|
import './AnnualReportPage.scss'
|
||||||
|
|
||||||
type YearOption = number | 'all'
|
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 formatLoadElapsed = (ms: number) => {
|
||||||
const totalSeconds = Math.max(0, ms) / 1000
|
const totalSeconds = Math.max(0, ms) / 1000
|
||||||
@@ -21,37 +35,36 @@ function AnnualReportPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false)
|
const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false)
|
||||||
const [hasYearsLoadFinished, setHasYearsLoadFinished] = 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 [isGenerating, setIsGenerating] = useState(false)
|
||||||
const [loadError, setLoadError] = useState<string | null>(null)
|
const [loadError, setLoadError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false
|
let disposed = false
|
||||||
let taskId = ''
|
let taskId = ''
|
||||||
const loadStartedAt = Date.now()
|
|
||||||
let ticker: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
const startTicker = () => {
|
const applyLoadPayload = (payload: YearsLoadPayload) => {
|
||||||
setLoadElapsedMs(0)
|
if (payload.strategy) setLoadStrategy(payload.strategy)
|
||||||
if (ticker) clearInterval(ticker)
|
if (payload.phase) setLoadPhase(payload.phase)
|
||||||
ticker = setInterval(() => {
|
if (typeof payload.statusText === 'string' && payload.statusText) setLoadStatusText(payload.statusText)
|
||||||
if (disposed) return
|
if (typeof payload.nativeElapsedMs === 'number' && Number.isFinite(payload.nativeElapsedMs)) {
|
||||||
setLoadElapsedMs(Date.now() - loadStartedAt)
|
setNativeElapsedMs(Math.max(0, payload.nativeElapsedMs))
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
if (typeof payload.scanElapsedMs === 'number' && Number.isFinite(payload.scanElapsedMs)) {
|
||||||
const stopTicker = () => {
|
setScanElapsedMs(Math.max(0, payload.scanElapsedMs))
|
||||||
setLoadElapsedMs(Date.now() - loadStartedAt)
|
|
||||||
if (ticker) {
|
|
||||||
clearInterval(ticker)
|
|
||||||
ticker = null
|
|
||||||
}
|
}
|
||||||
|
if (typeof payload.totalElapsedMs === 'number' && Number.isFinite(payload.totalElapsedMs)) {
|
||||||
|
setTotalElapsedMs(Math.max(0, payload.totalElapsedMs))
|
||||||
}
|
}
|
||||||
|
if (typeof payload.switched === 'boolean') setHasSwitchedStrategy(payload.switched)
|
||||||
const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => {
|
if (typeof payload.nativeTimedOut === 'boolean') setNativeTimedOut(payload.nativeTimedOut)
|
||||||
if (disposed) return
|
|
||||||
if (taskId && payload.taskId !== taskId) return
|
|
||||||
if (!taskId) taskId = payload.taskId
|
|
||||||
|
|
||||||
const years = Array.isArray(payload.years) ? payload.years : []
|
const years = Array.isArray(payload.years) ? payload.years : []
|
||||||
if (years.length > 0) {
|
if (years.length > 0) {
|
||||||
@@ -66,7 +79,6 @@ function AnnualReportPage() {
|
|||||||
if (typeof prev === 'number' && years.includes(prev)) return prev
|
if (typeof prev === 'number' && years.includes(prev)) return prev
|
||||||
return years[0]
|
return years[0]
|
||||||
})
|
})
|
||||||
// 只要有首批年份可选,就不再阻塞整个页面。
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,35 +90,50 @@ function AnnualReportPage() {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsLoadingMoreYears(false)
|
setIsLoadingMoreYears(false)
|
||||||
setHasYearsLoadFinished(true)
|
setHasYearsLoadFinished(true)
|
||||||
stopTicker()
|
setLoadPhase('done')
|
||||||
} else {
|
} else {
|
||||||
setIsLoadingMoreYears(true)
|
setIsLoadingMoreYears(true)
|
||||||
setHasYearsLoadFinished(false)
|
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 () => {
|
const startLoad = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setIsLoadingMoreYears(true)
|
setIsLoadingMoreYears(true)
|
||||||
setHasYearsLoadFinished(false)
|
setHasYearsLoadFinished(false)
|
||||||
|
setLoadStrategy('native')
|
||||||
|
setLoadPhase('native')
|
||||||
|
setLoadStatusText('准备使用原生快速模式加载年份...')
|
||||||
|
setNativeElapsedMs(0)
|
||||||
|
setScanElapsedMs(0)
|
||||||
|
setTotalElapsedMs(0)
|
||||||
|
setHasSwitchedStrategy(false)
|
||||||
|
setNativeTimedOut(false)
|
||||||
setLoadError(null)
|
setLoadError(null)
|
||||||
startTicker()
|
|
||||||
try {
|
try {
|
||||||
const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad()
|
const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad()
|
||||||
if (!startResult.success || !startResult.taskId) {
|
if (!startResult.success || !startResult.taskId) {
|
||||||
setLoadError(startResult.error || '加载年度数据失败')
|
setLoadError(startResult.error || '加载年度数据失败')
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsLoadingMoreYears(false)
|
setIsLoadingMoreYears(false)
|
||||||
stopTicker()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskId = startResult.taskId
|
taskId = startResult.taskId
|
||||||
|
if (startResult.snapshot) {
|
||||||
|
applyLoadPayload(startResult.snapshot)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
setLoadError(String(e))
|
setLoadError(String(e))
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsLoadingMoreYears(false)
|
setIsLoadingMoreYears(false)
|
||||||
stopTicker()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,11 +141,7 @@ function AnnualReportPage() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true
|
disposed = true
|
||||||
stopTicker()
|
|
||||||
stopListen()
|
stopListen()
|
||||||
if (taskId) {
|
|
||||||
void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -146,7 +169,15 @@ function AnnualReportPage() {
|
|||||||
<div className="annual-report-page">
|
<div className="annual-report-page">
|
||||||
<Loader2 size={32} className="spin" style={{ color: 'var(--text-tertiary)' }} />
|
<Loader2 size={32} className="spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||||
<p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>正在加载年份数据(首批)...</p>
|
<p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>正在加载年份数据(首批)...</p>
|
||||||
<p style={{ color: 'var(--text-tertiary)', marginTop: 8 }}>已耗时 {formatLoadElapsed(loadElapsedMs)}</p>
|
<div className="load-telemetry compact">
|
||||||
|
<p><span className="label">加载方式:</span>{getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut })}</p>
|
||||||
|
<p><span className="label">状态:</span>{loadStatusText || '正在加载年份数据...'}</p>
|
||||||
|
<p>
|
||||||
|
<span className="label">原生耗时:</span>{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '}
|
||||||
|
<span className="label">扫表耗时:</span>{formatLoadElapsed(scanElapsedMs)} |{' '}
|
||||||
|
<span className="label">总耗时:</span>{formatLoadElapsed(totalElapsedMs)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,6 +205,7 @@ function AnnualReportPage() {
|
|||||||
|
|
||||||
const loadedYearCount = availableYears.length
|
const loadedYearCount = availableYears.length
|
||||||
const isYearStatusComplete = hasYearsLoadFinished
|
const isYearStatusComplete = hasYearsLoadFinished
|
||||||
|
const strategyLabel = getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut })
|
||||||
const renderYearLoadStatus = () => (
|
const renderYearLoadStatus = () => (
|
||||||
<div className={`year-load-status ${isYearStatusComplete ? 'complete' : 'loading'}`}>
|
<div className={`year-load-status ${isYearStatusComplete ? 'complete' : 'loading'}`}>
|
||||||
{isYearStatusComplete ? (
|
{isYearStatusComplete ? (
|
||||||
@@ -194,15 +226,27 @@ function AnnualReportPage() {
|
|||||||
{loadedYearCount > 0 && (
|
{loadedYearCount > 0 && (
|
||||||
<p className={`page-desc load-summary ${isYearStatusComplete ? 'complete' : 'loading'}`}>
|
<p className={`page-desc load-summary ${isYearStatusComplete ? 'complete' : 'loading'}`}>
|
||||||
{isYearStatusComplete ? (
|
{isYearStatusComplete ? (
|
||||||
<>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(loadElapsedMs)}</>
|
<>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(totalElapsedMs)}</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
已显示 {loadedYearCount} 个年份,正在补充更多年份<span className="dot-ellipsis" aria-hidden="true">...</span>
|
已显示 {loadedYearCount} 个年份,正在补充更多年份<span className="dot-ellipsis" aria-hidden="true">...</span>
|
||||||
(已耗时 {formatLoadElapsed(loadElapsedMs)})
|
(已耗时 {formatLoadElapsed(totalElapsedMs)})
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<div className={`load-telemetry ${isYearStatusComplete ? 'complete' : 'loading'}`}>
|
||||||
|
<p><span className="label">加载方式:</span>{strategyLabel}</p>
|
||||||
|
<p>
|
||||||
|
<span className="label">状态:</span>
|
||||||
|
{loadStatusText || (isYearStatusComplete ? '全部年份已加载完毕' : '正在加载年份数据...')}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="label">原生耗时:</span>{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} |{' '}
|
||||||
|
<span className="label">扫表耗时:</span>{formatLoadElapsed(scanElapsedMs)} |{' '}
|
||||||
|
<span className="label">总耗时:</span>{formatLoadElapsed(totalElapsedMs)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="report-sections">
|
<div className="report-sections">
|
||||||
<section className="report-section">
|
<section className="report-section">
|
||||||
@@ -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
|
export default AnnualReportPage
|
||||||
|
|||||||
23
src/types/electron.d.ts
vendored
23
src/types/electron.d.ts
vendored
@@ -486,6 +486,21 @@ export interface ElectronAPI {
|
|||||||
startAvailableYearsLoad: () => Promise<{
|
startAvailableYearsLoad: () => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
taskId?: string
|
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
|
error?: string
|
||||||
}>
|
}>
|
||||||
cancelAvailableYearsLoad: (taskId: string) => Promise<{
|
cancelAvailableYearsLoad: (taskId: string) => Promise<{
|
||||||
@@ -582,6 +597,14 @@ export interface ElectronAPI {
|
|||||||
done: boolean
|
done: boolean
|
||||||
error?: string
|
error?: string
|
||||||
canceled?: boolean
|
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
|
}) => void) => () => void
|
||||||
onProgress: (callback: (payload: { status: string; progress: number }) => void) => () => void
|
onProgress: (callback: (payload: { status: string; progress: number }) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user