年度报告优化 #720

This commit is contained in:
cc
2026-04-19 19:28:14 +08:00
parent bc2e7d616a
commit 682f43bf2f
6 changed files with 93 additions and 16 deletions

View File

@@ -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) => {

View File

@@ -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[]

View File

@@ -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())

View File

@@ -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 {

View File

@@ -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<void>((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve())
})
})
const captureSceneDataUrl = async (): Promise<string> => {
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() {
<div className="reveal-inner serif scene0-cn-tag"></div>
</div>
<div className="reveal-wrap title-year-wrap">
<div className="reveal-inner serif title-year delay-1">{yearTitle}</div>
<div className={`reveal-inner serif title-year ${yearTitleVariantClass} delay-1`}>{yearTitle}</div>
</div>
<div className="reveal-wrap desc-text p0-desc">
<div className="reveal-inner serif delay-2 p0-desc-inner"><br/></div>

View File

@@ -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[]