mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(report): stream available years loading
This commit is contained in:
@@ -102,6 +102,7 @@ interface ExportTaskControlState {
|
|||||||
|
|
||||||
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 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() : ''
|
||||||
@@ -121,6 +122,10 @@ const createTaskControlState = (taskId?: string): string | null => {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isYearsLoadCanceled = (taskId: string): boolean => {
|
||||||
|
return annualReportYearsLoadTasks.get(taskId)?.canceled === true
|
||||||
|
}
|
||||||
|
|
||||||
const clearTaskControlState = (taskId?: string): void => {
|
const clearTaskControlState = (taskId?: string): void => {
|
||||||
const normalized = typeof taskId === 'string' ? taskId.trim() : ''
|
const normalized = typeof taskId === 'string' ? taskId.trim() : ''
|
||||||
if (!normalized) return
|
if (!normalized) return
|
||||||
@@ -1534,6 +1539,67 @@ function registerIpcHandlers() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('annualReport:startAvailableYearsLoad', async (event) => {
|
||||||
|
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 sendProgress = (payload: { years?: number[]; done: boolean; error?: string; canceled?: boolean }) => {
|
||||||
|
if (!sender.isDestroyed()) {
|
||||||
|
sender.send('annualReport:availableYearsProgress', {
|
||||||
|
taskId,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await annualReportService.getAvailableYears({
|
||||||
|
dbPath: cfg.get('dbPath'),
|
||||||
|
decryptKey: cfg.get('decryptKey'),
|
||||||
|
wxid: cfg.get('myWxid'),
|
||||||
|
onProgress: (years) => {
|
||||||
|
if (isYearsLoadCanceled(taskId)) return
|
||||||
|
sendProgress({ years, done: false })
|
||||||
|
},
|
||||||
|
shouldCancel: () => isYearsLoadCanceled(taskId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canceled = isYearsLoadCanceled(taskId)
|
||||||
|
annualReportYearsLoadTasks.delete(taskId)
|
||||||
|
if (canceled) {
|
||||||
|
sendProgress({ done: true, canceled: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
sendProgress({ years: result.data || [], done: true })
|
||||||
|
} else {
|
||||||
|
sendProgress({ years: result.data || [], done: true, error: result.error || '加载年度数据失败' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
annualReportYearsLoadTasks.delete(taskId)
|
||||||
|
sendProgress({ done: true, error: String(e) })
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return { success: true, taskId }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('annualReport:cancelAvailableYearsLoad', async (_, taskId: string) => {
|
||||||
|
const key = String(taskId || '').trim()
|
||||||
|
if (!key) return { success: false, error: '任务ID不能为空' }
|
||||||
|
const task = annualReportYearsLoadTasks.get(key)
|
||||||
|
if (!task) return { success: true }
|
||||||
|
task.canceled = true
|
||||||
|
annualReportYearsLoadTasks.set(key, task)
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('annualReport:generateReport', async (_, year: number) => {
|
ipcMain.handle('annualReport:generateReport', async (_, year: number) => {
|
||||||
const cfg = configService || new ConfigService()
|
const cfg = configService || new ConfigService()
|
||||||
configService = cfg
|
configService = cfg
|
||||||
|
|||||||
@@ -265,9 +265,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// 年度报告
|
// 年度报告
|
||||||
annualReport: {
|
annualReport: {
|
||||||
getAvailableYears: () => ipcRenderer.invoke('annualReport:getAvailableYears'),
|
getAvailableYears: () => ipcRenderer.invoke('annualReport:getAvailableYears'),
|
||||||
|
startAvailableYearsLoad: () => ipcRenderer.invoke('annualReport:startAvailableYearsLoad'),
|
||||||
|
cancelAvailableYearsLoad: (taskId: string) => ipcRenderer.invoke('annualReport:cancelAvailableYearsLoad', taskId),
|
||||||
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) => {
|
||||||
|
ipcRenderer.on('annualReport:availableYearsProgress', (_, payload) => callback(payload))
|
||||||
|
return () => ipcRenderer.removeAllListeners('annualReport:availableYearsProgress')
|
||||||
|
},
|
||||||
onProgress: (callback: (payload: { status: string; progress: number }) => void) => {
|
onProgress: (callback: (payload: { status: string; progress: number }) => void) => {
|
||||||
ipcRenderer.on('annualReport:progress', (_, payload) => callback(payload))
|
ipcRenderer.on('annualReport:progress', (_, payload) => callback(payload))
|
||||||
return () => ipcRenderer.removeAllListeners('annualReport:progress')
|
return () => ipcRenderer.removeAllListeners('annualReport:progress')
|
||||||
|
|||||||
@@ -198,23 +198,36 @@ class AnnualReportService {
|
|||||||
return seconds > 0 ? seconds : 0
|
return seconds > 0 ? seconds : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private addYearsFromRange(years: Set<number>, firstTs: number, lastTs: number): void {
|
private addYearsFromRange(years: Set<number>, firstTs: number, lastTs: number): boolean {
|
||||||
|
let changed = false
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const minTs = firstTs > 0 ? firstTs : lastTs
|
const minTs = firstTs > 0 ? firstTs : lastTs
|
||||||
const maxTs = lastTs > 0 ? lastTs : firstTs
|
const maxTs = lastTs > 0 ? lastTs : firstTs
|
||||||
if (minTs <= 0 || maxTs <= 0) return
|
if (minTs <= 0 || maxTs <= 0) return changed
|
||||||
|
|
||||||
const minYear = new Date(minTs * 1000).getFullYear()
|
const minYear = new Date(minTs * 1000).getFullYear()
|
||||||
const maxYear = new Date(maxTs * 1000).getFullYear()
|
const maxYear = new Date(maxTs * 1000).getFullYear()
|
||||||
for (let y = minYear; y <= maxYear; y++) {
|
for (let y = minYear; y <= maxYear; y++) {
|
||||||
if (y >= 2010 && y <= currentYear) years.add(y)
|
if (y >= 2010 && y <= currentYear && !years.has(y)) {
|
||||||
|
years.add(y)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeAvailableYears(years: Iterable<number>): number[] {
|
||||||
|
return Array.from(new Set(Array.from(years)))
|
||||||
|
.filter((y) => Number.isFinite(y))
|
||||||
|
.map((y) => Math.floor(y))
|
||||||
|
.sort((a, b) => b - a)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async forEachWithConcurrency<T>(
|
private async forEachWithConcurrency<T>(
|
||||||
items: T[],
|
items: T[],
|
||||||
concurrency: number,
|
concurrency: number,
|
||||||
handler: (item: T, index: number) => Promise<void>
|
handler: (item: T, index: number) => Promise<void>,
|
||||||
|
shouldStop?: () => boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!items.length) return
|
if (!items.length) return
|
||||||
const workerCount = Math.max(1, Math.min(concurrency, items.length))
|
const workerCount = Math.max(1, Math.min(concurrency, items.length))
|
||||||
@@ -224,6 +237,7 @@ class AnnualReportService {
|
|||||||
for (let i = 0; i < workerCount; i++) {
|
for (let i = 0; i < workerCount; i++) {
|
||||||
workers.push((async () => {
|
workers.push((async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (shouldStop?.()) break
|
||||||
const current = nextIndex
|
const current = nextIndex
|
||||||
nextIndex += 1
|
nextIndex += 1
|
||||||
if (current >= items.length) break
|
if (current >= items.length) break
|
||||||
@@ -297,37 +311,72 @@ class AnnualReportService {
|
|||||||
return queryByColumn(detectedColumn)
|
return queryByColumn(detectedColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAvailableYearsByTableScan(sessionIds: string[]): Promise<number[]> {
|
private async getAvailableYearsByTableScan(
|
||||||
|
sessionIds: string[],
|
||||||
|
options?: { onProgress?: (years: number[]) => void; shouldCancel?: () => boolean }
|
||||||
|
): Promise<number[]> {
|
||||||
const years = new Set<number>()
|
const years = new Set<number>()
|
||||||
|
let lastEmittedSize = 0
|
||||||
|
|
||||||
|
const emitIfChanged = (force = false) => {
|
||||||
|
if (!options?.onProgress) return
|
||||||
|
const next = this.normalizeAvailableYears(years)
|
||||||
|
if (!force && next.length === lastEmittedSize) return
|
||||||
|
options.onProgress(next)
|
||||||
|
lastEmittedSize = next.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldCancel = () => options?.shouldCancel?.() === true
|
||||||
|
|
||||||
await this.forEachWithConcurrency(sessionIds, this.availableYearsScanConcurrency, async (sessionId) => {
|
await this.forEachWithConcurrency(sessionIds, this.availableYearsScanConcurrency, async (sessionId) => {
|
||||||
|
if (shouldCancel()) return
|
||||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
||||||
if (!tableStats.success || !Array.isArray(tableStats.tables) || tableStats.tables.length === 0) {
|
if (!tableStats.success || !Array.isArray(tableStats.tables) || tableStats.tables.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const table of tableStats.tables as Record<string, any>[]) {
|
for (const table of tableStats.tables as Record<string, any>[]) {
|
||||||
|
if (shouldCancel()) return
|
||||||
const tableName = String(table.table_name || table.name || '').trim()
|
const tableName = String(table.table_name || table.name || '').trim()
|
||||||
const dbPath = String(table.db_path || table.dbPath || '').trim()
|
const dbPath = String(table.db_path || table.dbPath || '').trim()
|
||||||
if (!tableName || !dbPath) continue
|
if (!tableName || !dbPath) continue
|
||||||
|
|
||||||
const range = await this.getTableTimeRange(dbPath, tableName)
|
const range = await this.getTableTimeRange(dbPath, tableName)
|
||||||
if (!range) continue
|
if (!range) continue
|
||||||
this.addYearsFromRange(years, range.first, range.last)
|
const changed = this.addYearsFromRange(years, range.first, range.last)
|
||||||
|
if (changed) emitIfChanged()
|
||||||
}
|
}
|
||||||
})
|
}, shouldCancel)
|
||||||
|
|
||||||
return Array.from(years).sort((a, b) => b - a)
|
emitIfChanged(true)
|
||||||
|
return this.normalizeAvailableYears(years)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAvailableYearsByEdgeScan(sessionIds: string[]): Promise<number[]> {
|
private async getAvailableYearsByEdgeScan(
|
||||||
|
sessionIds: string[],
|
||||||
|
options?: { onProgress?: (years: number[]) => void; shouldCancel?: () => boolean }
|
||||||
|
): Promise<number[]> {
|
||||||
const years = new Set<number>()
|
const years = new Set<number>()
|
||||||
|
let lastEmittedSize = 0
|
||||||
|
const shouldCancel = () => options?.shouldCancel?.() === true
|
||||||
|
|
||||||
|
const emitIfChanged = (force = false) => {
|
||||||
|
if (!options?.onProgress) return
|
||||||
|
const next = this.normalizeAvailableYears(years)
|
||||||
|
if (!force && next.length === lastEmittedSize) return
|
||||||
|
options.onProgress(next)
|
||||||
|
lastEmittedSize = next.length
|
||||||
|
}
|
||||||
|
|
||||||
for (const sessionId of sessionIds) {
|
for (const sessionId of sessionIds) {
|
||||||
|
if (shouldCancel()) break
|
||||||
const first = await this.getEdgeMessageTime(sessionId, true)
|
const first = await this.getEdgeMessageTime(sessionId, true)
|
||||||
const last = await this.getEdgeMessageTime(sessionId, false)
|
const last = await this.getEdgeMessageTime(sessionId, false)
|
||||||
this.addYearsFromRange(years, first || 0, last || 0)
|
const changed = this.addYearsFromRange(years, first || 0, last || 0)
|
||||||
|
if (changed) emitIfChanged()
|
||||||
}
|
}
|
||||||
return Array.from(years).sort((a, b) => b - a)
|
emitIfChanged(true)
|
||||||
|
return this.normalizeAvailableYears(years)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAvailableYearsCacheKey(dbPath: string, cleanedWxid: string): string {
|
private buildAvailableYearsCacheKey(dbPath: string, cleanedWxid: string): string {
|
||||||
@@ -345,10 +394,7 @@ class AnnualReportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setCachedAvailableYears(cacheKey: string, years: number[]): void {
|
private setCachedAvailableYears(cacheKey: string, years: number[]): void {
|
||||||
const normalized = Array.from(new Set(years))
|
const normalized = this.normalizeAvailableYears(years)
|
||||||
.filter((y) => Number.isFinite(y))
|
|
||||||
.map((y) => Math.floor(y))
|
|
||||||
.sort((a, b) => b - a)
|
|
||||||
|
|
||||||
this.availableYearsCache.set(cacheKey, {
|
this.availableYearsCache.set(cacheKey, {
|
||||||
years: normalized,
|
years: normalized,
|
||||||
@@ -546,13 +592,22 @@ class AnnualReportService {
|
|||||||
return { sessionId: bestSessionId, days: bestDays, start: bestStart, end: bestEnd }
|
return { sessionId: bestSessionId, days: bestDays, start: bestStart, end: bestEnd }
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableYears(params: { dbPath: string; decryptKey: string; wxid: string }): Promise<{ success: boolean; data?: number[]; error?: string }> {
|
async getAvailableYears(params: {
|
||||||
|
dbPath: string
|
||||||
|
decryptKey: string
|
||||||
|
wxid: string
|
||||||
|
onProgress?: (years: number[]) => void
|
||||||
|
shouldCancel?: () => boolean
|
||||||
|
}): Promise<{ success: boolean; data?: number[]; error?: string }> {
|
||||||
try {
|
try {
|
||||||
|
const isCancelled = () => params.shouldCancel?.() === true
|
||||||
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 }
|
||||||
|
if (isCancelled()) return { success: false, error: '已取消加载年份数据' }
|
||||||
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)
|
||||||
return { success: true, data: cached }
|
return { success: true, data: cached }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,14 +615,24 @@ class AnnualReportService {
|
|||||||
if (sessionIds.length === 0) {
|
if (sessionIds.length === 0) {
|
||||||
return { success: false, error: '未找到消息会话' }
|
return { success: false, error: '未找到消息会话' }
|
||||||
}
|
}
|
||||||
|
if (isCancelled()) return { success: false, error: '已取消加载年份数据' }
|
||||||
|
|
||||||
let years = await this.getAvailableYearsByTableScan(sessionIds)
|
let years = await this.getAvailableYearsByTableScan(sessionIds, {
|
||||||
|
onProgress: params.onProgress,
|
||||||
|
shouldCancel: params.shouldCancel
|
||||||
|
})
|
||||||
|
if (isCancelled()) return { success: false, error: '已取消加载年份数据' }
|
||||||
if (years.length === 0) {
|
if (years.length === 0) {
|
||||||
// 扫表失败时,再降级到游标首尾扫描,保证兼容性。
|
// 扫表失败时,再降级到游标首尾扫描,保证兼容性。
|
||||||
years = await this.getAvailableYearsByEdgeScan(sessionIds)
|
years = await this.getAvailableYearsByEdgeScan(sessionIds, {
|
||||||
|
onProgress: params.onProgress,
|
||||||
|
shouldCancel: params.shouldCancel
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
if (isCancelled()) return { success: false, error: '已取消加载年份数据' }
|
||||||
|
|
||||||
this.setCachedAvailableYears(cacheKey, years)
|
this.setCachedAvailableYears(cacheKey, years)
|
||||||
|
params.onProgress?.(years)
|
||||||
return { success: true, data: years }
|
return { success: true, data: years }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: String(e) }
|
||||||
|
|||||||
@@ -11,33 +11,79 @@ function AnnualReportPage() {
|
|||||||
const [selectedYear, setSelectedYear] = useState<YearOption | null>(null)
|
const [selectedYear, setSelectedYear] = useState<YearOption | null>(null)
|
||||||
const [selectedPairYear, setSelectedPairYear] = useState<YearOption | null>(null)
|
const [selectedPairYear, setSelectedPairYear] = useState<YearOption | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isLoadingMoreYears, setIsLoadingMoreYears] = 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(() => {
|
||||||
loadAvailableYears()
|
let disposed = false
|
||||||
}, [])
|
let taskId = ''
|
||||||
|
|
||||||
const loadAvailableYears = async () => {
|
const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => {
|
||||||
setIsLoading(true)
|
if (disposed) return
|
||||||
setLoadError(null)
|
if (taskId && payload.taskId !== taskId) return
|
||||||
try {
|
if (!taskId) taskId = payload.taskId
|
||||||
const result = await window.electronAPI.annualReport.getAvailableYears()
|
|
||||||
const years = result.data
|
const years = Array.isArray(payload.years) ? payload.years : []
|
||||||
if (result.success && Array.isArray(years) && years.length > 0) {
|
if (years.length > 0) {
|
||||||
setAvailableYears(years)
|
setAvailableYears(years)
|
||||||
setSelectedYear((prev) => prev ?? years[0])
|
setSelectedYear((prev) => {
|
||||||
setSelectedPairYear((prev) => prev ?? years[0])
|
if (prev === 'all') return prev
|
||||||
} else if (!result.success) {
|
if (typeof prev === 'number' && years.includes(prev)) return prev
|
||||||
setLoadError(result.error || '加载年度数据失败')
|
return years[0]
|
||||||
|
})
|
||||||
|
setSelectedPairYear((prev) => {
|
||||||
|
if (prev === 'all') return prev
|
||||||
|
if (typeof prev === 'number' && years.includes(prev)) return prev
|
||||||
|
return years[0]
|
||||||
|
})
|
||||||
|
// 只要有首批年份可选,就不再阻塞整个页面。
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.error && !payload.canceled) {
|
||||||
|
setLoadError(payload.error || '加载年度数据失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.done) {
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsLoadingMoreYears(false)
|
||||||
|
} else {
|
||||||
|
setIsLoadingMoreYears(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const startLoad = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setIsLoadingMoreYears(true)
|
||||||
|
setLoadError(null)
|
||||||
|
try {
|
||||||
|
const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad()
|
||||||
|
if (!startResult.success || !startResult.taskId) {
|
||||||
|
setLoadError(startResult.error || '加载年度数据失败')
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsLoadingMoreYears(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskId = startResult.taskId
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setLoadError(String(e))
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsLoadingMoreYears(false)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
setLoadError(String(e))
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void startLoad()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true
|
||||||
|
stopListen()
|
||||||
|
if (taskId) {
|
||||||
|
void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleGenerateReport = async () => {
|
const handleGenerateReport = async () => {
|
||||||
if (selectedYear === null) return
|
if (selectedYear === null) return
|
||||||
@@ -58,16 +104,16 @@ function AnnualReportPage() {
|
|||||||
navigate(`/dual-report?year=${yearParam}`)
|
navigate(`/dual-report?year=${yearParam}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading && availableYears.length === 0) {
|
||||||
return (
|
return (
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (availableYears.length === 0) {
|
if (availableYears.length === 0 && !isLoadingMoreYears) {
|
||||||
return (
|
return (
|
||||||
<div className="annual-report-page">
|
<div className="annual-report-page">
|
||||||
<Calendar size={64} style={{ color: 'var(--text-tertiary)', opacity: 0.5 }} />
|
<Calendar size={64} style={{ color: 'var(--text-tertiary)', opacity: 0.5 }} />
|
||||||
@@ -93,6 +139,9 @@ function AnnualReportPage() {
|
|||||||
<Sparkles size={32} className="header-icon" />
|
<Sparkles size={32} className="header-icon" />
|
||||||
<h1 className="page-title">年度报告</h1>
|
<h1 className="page-title">年度报告</h1>
|
||||||
<p className="page-desc">选择年份,回顾你在微信里的点点滴滴</p>
|
<p className="page-desc">选择年份,回顾你在微信里的点点滴滴</p>
|
||||||
|
{isLoadingMoreYears && (
|
||||||
|
<p className="page-desc">已显示首批年份,正在补充更多年份...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="report-sections">
|
<div className="report-sections">
|
||||||
<section className="report-section">
|
<section className="report-section">
|
||||||
|
|||||||
16
src/types/electron.d.ts
vendored
16
src/types/electron.d.ts
vendored
@@ -483,6 +483,15 @@ export interface ElectronAPI {
|
|||||||
data?: number[]
|
data?: number[]
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
startAvailableYearsLoad: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
taskId?: string
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
|
cancelAvailableYearsLoad: (taskId: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
generateReport: (year: number) => Promise<{
|
generateReport: (year: number) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
data?: {
|
data?: {
|
||||||
@@ -567,6 +576,13 @@ export interface ElectronAPI {
|
|||||||
dir?: string
|
dir?: string
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
onAvailableYearsProgress: (callback: (payload: {
|
||||||
|
taskId: string
|
||||||
|
years?: number[]
|
||||||
|
done: boolean
|
||||||
|
error?: string
|
||||||
|
canceled?: boolean
|
||||||
|
}) => void) => () => void
|
||||||
onProgress: (callback: (payload: { status: string; progress: number }) => void) => () => void
|
onProgress: (callback: (payload: { status: string; progress: number }) => void) => () => void
|
||||||
}
|
}
|
||||||
dualReport: {
|
dualReport: {
|
||||||
|
|||||||
Reference in New Issue
Block a user