mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +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) => {
|
ipcMain.handle('key:autoGetDbKey', async (event) => {
|
||||||
return keyService.autoGetDbKey(180_000, (message: string, level: number) => {
|
return keyService.autoGetDbKey(180_000, (message: string, level: number) => {
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ 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),
|
||||||
|
captureCurrentWindow: () => ipcRenderer.invoke('annualReport:captureCurrentWindow'),
|
||||||
onAvailableYearsProgress: (callback: (payload: {
|
onAvailableYearsProgress: (callback: (payload: {
|
||||||
taskId: string
|
taskId: string
|
||||||
years?: number[]
|
years?: number[]
|
||||||
|
|||||||
@@ -1192,7 +1192,9 @@ class AnnualReportService {
|
|||||||
topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[]
|
topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[]
|
||||||
} | undefined
|
} | 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) {
|
if (snsStats.success && snsStats.data) {
|
||||||
const d = 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)
|
this.reportProgress('整理联系人信息...', 85, onProgress)
|
||||||
|
|
||||||
const contactIds = Array.from(contactStats.keys())
|
const contactIds = Array.from(contactStats.keys())
|
||||||
|
|||||||
@@ -447,6 +447,25 @@
|
|||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
margin-top: 10vh;
|
margin-top: 10vh;
|
||||||
text-shadow: 0 18px 45px rgba(0, 0, 0, 0.45);
|
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 {
|
#scene-0 .title-year-wrap {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import html2canvas from 'html2canvas'
|
|
||||||
import {
|
import {
|
||||||
finishBackgroundTask,
|
finishBackgroundTask,
|
||||||
isBackgroundTaskCancelRequested,
|
isBackgroundTaskCancelRequested,
|
||||||
@@ -403,6 +402,23 @@ function AnnualReportWindow() {
|
|||||||
const formatFileYearLabel = (year: number) => (year === 0 ? '历史以来' : String(year))
|
const formatFileYearLabel = (year: number) => (year === 0 ? '历史以来' : String(year))
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
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 () => {
|
const handleExtract = async () => {
|
||||||
if (isExtracting || !reportData || !containerRef.current) return
|
if (isExtracting || !reportData || !containerRef.current) return
|
||||||
@@ -435,26 +451,20 @@ function AnnualReportWindow() {
|
|||||||
try {
|
try {
|
||||||
const images: Array<{ name: string; dataUrl: string }> = []
|
const images: Array<{ name: string; dataUrl: string }> = []
|
||||||
root.classList.add('exporting-scenes')
|
root.classList.add('exporting-scenes')
|
||||||
|
await waitForNextPaint()
|
||||||
|
await wait(120)
|
||||||
|
// 预检:强制验证主进程已注册原生截图 handler,确保导出链路不是旧逻辑。
|
||||||
|
await captureSceneDataUrl()
|
||||||
|
|
||||||
for (let i = 0; i < TOTAL_SCENES; i++) {
|
for (let i = 0; i < TOTAL_SCENES; i++) {
|
||||||
setCurrentScene(i)
|
setCurrentScene(i)
|
||||||
setButtonText(`EXTRACTING ${i + 1}/${TOTAL_SCENES}`)
|
setButtonText(`EXTRACTING ${i + 1}/${TOTAL_SCENES}`)
|
||||||
await wait(2000)
|
await waitForNextPaint()
|
||||||
|
await wait(1700)
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
images.push({
|
images.push({
|
||||||
name: `P${String(i).padStart(2, '0')}_${sceneNames[i] || `SCENE_${i}`}.png`,
|
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 yearTitle = reportData.year === 0 ? '历史以来' : String(reportData.year)
|
||||||
const finalYearLabel = reportData.year === 0 ? 'ALL YEARS' : 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 topFriends = reportData.coreFriends.slice(0, 3)
|
||||||
const endingPostCount = reportData.snsStats?.totalPosts ?? 0
|
const endingPostCount = reportData.snsStats?.totalPosts ?? 0
|
||||||
const endingReceivedChats = reportData.socialInitiative?.receivedChats ?? 0
|
const endingReceivedChats = reportData.socialInitiative?.receivedChats ?? 0
|
||||||
@@ -570,7 +587,7 @@ function AnnualReportWindow() {
|
|||||||
<div className="reveal-inner serif scene0-cn-tag">一切的起点</div>
|
<div className="reveal-inner serif scene0-cn-tag">一切的起点</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="reveal-wrap title-year-wrap">
|
<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>
|
||||||
<div className="reveal-wrap desc-text p0-desc">
|
<div className="reveal-wrap desc-text p0-desc">
|
||||||
<div className="reveal-inner serif delay-2 p0-desc-inner">那些被岁月悄悄掩埋的对话<br/>原来都在这里,等待一个春天。</div>
|
<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
|
dir?: string
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
captureCurrentWindow: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
dataUrl?: string
|
||||||
|
size?: { width: number; height: number }
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
onAvailableYearsProgress: (callback: (payload: {
|
onAvailableYearsProgress: (callback: (payload: {
|
||||||
taskId: string
|
taskId: string
|
||||||
years?: number[]
|
years?: number[]
|
||||||
|
|||||||
Reference in New Issue
Block a user