diff --git a/electron/main.ts b/electron/main.ts index 4d11ca1..5b3a16e 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3555,6 +3555,24 @@ function registerIpcHandlers() { } }) + ipcMain.handle('annualReport:captureCurrentWindow', async (event) => { + try { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win || win.isDestroyed()) { + return { success: false, error: '窗口不可用' } + } + + const image = await win.webContents.capturePage() + return { + success: true, + dataUrl: image.toDataURL(), + size: image.getSize() + } + } catch (e) { + return { success: false, error: String(e) } + } + }) + // 密钥获取 ipcMain.handle('key:autoGetDbKey', async (event) => { return keyService.autoGetDbKey(180_000, (message: string, level: number) => { diff --git a/electron/preload.ts b/electron/preload.ts index 09126a7..1e05df2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -412,6 +412,7 @@ 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), + captureCurrentWindow: () => ipcRenderer.invoke('annualReport:captureCurrentWindow'), onAvailableYearsProgress: (callback: (payload: { taskId: string years?: number[] diff --git a/electron/services/annualReportService.ts b/electron/services/annualReportService.ts index f17946b..9686ec3 100644 --- a/electron/services/annualReportService.ts +++ b/electron/services/annualReportService.ts @@ -1192,7 +1192,9 @@ class AnnualReportService { topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[] } | undefined - const snsStats = await wcdbService.getSnsAnnualStats(actualStartTime, actualEndTime) + const snsBeginTime = isAllTime ? 0 : actualStartTime + const snsEndTime = isAllTime ? Math.floor(Date.now() / 1000) : actualEndTime + const snsStats = await wcdbService.getSnsAnnualStats(snsBeginTime, snsEndTime) if (snsStats.success && snsStats.data) { const d = snsStats.data @@ -1219,6 +1221,20 @@ class AnnualReportService { } } + // ALL YEARS 兼容:部分底层实现 begin/end 为 0 时会返回 0,兜底使用导出统计总数。 + if (isAllTime && (!snsStatsResult || Number(snsStatsResult.totalPosts || 0) <= 0)) { + const snsExportStats = await wcdbService.getSnsExportStats(cleanedWxid || rawWxid) + if (snsExportStats.success && snsExportStats.data) { + const fallbackTotalPosts = Math.max(0, Number(snsExportStats.data.totalPosts || 0)) + snsStatsResult = { + totalPosts: fallbackTotalPosts, + typeCounts: snsStatsResult?.typeCounts, + topLikers: snsStatsResult?.topLikers || [], + topLiked: snsStatsResult?.topLiked || [] + } + } + } + this.reportProgress('整理联系人信息...', 85, onProgress) const contactIds = Array.from(contactStats.keys()) diff --git a/src/pages/AnnualReportWindow.scss b/src/pages/AnnualReportWindow.scss index 655b3fe..bc38030 100644 --- a/src/pages/AnnualReportWindow.scss +++ b/src/pages/AnnualReportWindow.scss @@ -447,6 +447,25 @@ letter-spacing: -0.04em; margin-top: 10vh; text-shadow: 0 18px 45px rgba(0, 0, 0, 0.45); + max-width: 90vw; + overflow-wrap: anywhere; + } + + #scene-0 .title-year--numeric { + font-size: clamp(6.8rem, 21vw, 18rem); + letter-spacing: -0.04em; + } + + #scene-0 .title-year--text { + font-size: clamp(4.8rem, 14vw, 10rem); + letter-spacing: 0.01em; + line-height: 1.08; + } + + #scene-0 .title-year--text-long { + font-size: clamp(3.8rem, 10.5vw, 7.5rem); + letter-spacing: 0.02em; + line-height: 1.12; } #scene-0 .title-year-wrap { diff --git a/src/pages/AnnualReportWindow.tsx b/src/pages/AnnualReportWindow.tsx index e54fd92..5274bb7 100644 --- a/src/pages/AnnualReportWindow.tsx +++ b/src/pages/AnnualReportWindow.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { X } from 'lucide-react' -import html2canvas from 'html2canvas' import { finishBackgroundTask, isBackgroundTaskCancelRequested, @@ -403,6 +402,23 @@ function AnnualReportWindow() { const formatFileYearLabel = (year: number) => (year === 0 ? '历史以来' : String(year)) const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + const waitForNextPaint = () => new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()) + }) + }) + const captureSceneDataUrl = async (): Promise => { + const captureFn = window.electronAPI.annualReport.captureCurrentWindow + if (typeof captureFn !== 'function') { + throw new Error('当前版本未启用原生截图接口,请重启应用后重试') + } + + const captureResult = await captureFn() + if (!captureResult.success || !captureResult.dataUrl) { + throw new Error(captureResult.error || '原生截图失败') + } + return captureResult.dataUrl + } const handleExtract = async () => { if (isExtracting || !reportData || !containerRef.current) return @@ -435,26 +451,20 @@ function AnnualReportWindow() { try { const images: Array<{ name: string; dataUrl: string }> = [] root.classList.add('exporting-scenes') + await waitForNextPaint() + await wait(120) + // 预检:强制验证主进程已注册原生截图 handler,确保导出链路不是旧逻辑。 + await captureSceneDataUrl() for (let i = 0; i < TOTAL_SCENES; i++) { setCurrentScene(i) setButtonText(`EXTRACTING ${i + 1}/${TOTAL_SCENES}`) - await wait(2000) - - const canvas = await html2canvas(root, { - backgroundColor: '#050505', - scale: 2, - useCORS: true, - allowTaint: true, - logging: false, - onclone: (clonedDoc) => { - clonedDoc.querySelector('.annual-report-window')?.classList.add('exporting-scenes') - } - }) + await waitForNextPaint() + await wait(1700) images.push({ name: `P${String(i).padStart(2, '0')}_${sceneNames[i] || `SCENE_${i}`}.png`, - dataUrl: canvas.toDataURL('image/png') + dataUrl: await captureSceneDataUrl() }) } @@ -521,6 +531,13 @@ function AnnualReportWindow() { const yearTitle = reportData.year === 0 ? '历史以来' : String(reportData.year) const finalYearLabel = reportData.year === 0 ? 'ALL YEARS' : String(reportData.year) + const compactYearTitle = yearTitle.replace(/\s+/g, '') + const isNumericYearTitle = /^\d+$/.test(compactYearTitle) + const yearTitleVariantClass = isNumericYearTitle + ? 'title-year--numeric' + : compactYearTitle.length >= 5 + ? 'title-year--text-long' + : 'title-year--text' const topFriends = reportData.coreFriends.slice(0, 3) const endingPostCount = reportData.snsStats?.totalPosts ?? 0 const endingReceivedChats = reportData.socialInitiative?.receivedChats ?? 0 @@ -570,7 +587,7 @@ function AnnualReportWindow() {
一切的起点
-
{yearTitle}
+
{yearTitle}
那些被岁月悄悄掩埋的对话
原来都在这里,等待一个春天。
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index a45755e..7be970b 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -883,6 +883,12 @@ export interface ElectronAPI { dir?: string error?: string }> + captureCurrentWindow: () => Promise<{ + success: boolean + dataUrl?: string + size?: { width: number; height: number } + error?: string + }> onAvailableYearsProgress: (callback: (payload: { taskId: string years?: number[]