mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 07:26:46 +00:00
年度报告优化 #720
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
src/types/electron.d.ts
vendored
6
src/types/electron.d.ts
vendored
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user