import { useState, useEffect, useRef } from 'react' import { Loader2, Download, Image, Check, X, SlidersHorizontal } from 'lucide-react' import html2canvas from 'html2canvas' import { useThemeStore } from '../stores/themeStore' import { finishBackgroundTask, isBackgroundTaskCancelRequested, registerBackgroundTask, updateBackgroundTask } from '../services/backgroundTaskMonitor' import './AnnualReportWindow.scss' // SVG 背景图案 (用于导出) const PATTERN_LIGHT_SVG = `` const PATTERN_DARK_SVG = `` // 绘制 SVG 图案背景到 canvas const drawPatternBackground = async (ctx: CanvasRenderingContext2D, width: number, height: number, bgColor: string, isDark: boolean) => { // 先填充背景色 ctx.fillStyle = bgColor ctx.fillRect(0, 0, width, height) // 加载 SVG 图案 const svgString = isDark ? PATTERN_DARK_SVG : PATTERN_LIGHT_SVG const blob = new Blob([svgString], { type: 'image/svg+xml' }) const url = URL.createObjectURL(blob) return new Promise((resolve) => { const img = new window.Image() img.onload = () => { // 平铺绘制图案 const pattern = ctx.createPattern(img, 'repeat') if (pattern) { ctx.fillStyle = pattern ctx.fillRect(0, 0, width, height) } URL.revokeObjectURL(url) resolve() } img.onerror = () => { URL.revokeObjectURL(url) resolve() } img.src = url }) } interface TopContact { username: string displayName: string avatarUrl?: string messageCount: number sentCount: number receivedCount: number } interface MonthlyTopFriend { month: number displayName: string avatarUrl?: string messageCount: number } interface AnnualReportData { year: number totalMessages: number totalFriends: number coreFriends: TopContact[] monthlyTopFriends: MonthlyTopFriend[] peakDay: { date: string; messageCount: number; topFriend?: string; topFriendCount?: number } | null longestStreak: { friendName: string; days: number; startDate: string; endDate: string } | null activityHeatmap: { data: number[][] } midnightKing: { displayName: string; count: number; percentage: number } | null selfAvatarUrl?: string mutualFriend?: { displayName: string; avatarUrl?: string; sentCount: number; receivedCount: number; ratio: number } | null socialInitiative?: { initiatedChats: number; receivedChats: number; initiativeRate: number } | null responseSpeed?: { avgResponseTime: number; fastestFriend: string; fastestTime: number } | null topPhrases?: { phrase: string; count: number }[] snsStats?: { totalPosts: number typeCounts?: Record topLikers: { username: string; displayName: string; avatarUrl?: string; count: number }[] topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[] } lostFriend: { username: string displayName: string avatarUrl?: string earlyCount: number lateCount: number periodDesc: string } | null } interface SectionInfo { id: string name: string ref: React.RefObject } // 头像组件 const Avatar = ({ url, name, size = 'md' }: { url?: string; name: string; size?: 'sm' | 'md' | 'lg' }) => { const [imgError, setImgError] = useState(false) const initial = name?.[0] || '友' return (
{url && !imgError ? ( setImgError(true)} crossOrigin="anonymous" /> ) : ( {initial} )}
) } import Heatmap from '../components/ReportHeatmap' import WordCloud from '../components/ReportWordCloud' function AnnualReportWindow() { const [reportData, setReportData] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isExporting, setIsExporting] = useState(false) const [exportProgress, setExportProgress] = useState('') const [showExportModal, setShowExportModal] = useState(false) const [selectedSections, setSelectedSections] = useState>(new Set()) const [fabOpen, setFabOpen] = useState(false) const [loadingProgress, setLoadingProgress] = useState(0) const [loadingStage, setLoadingStage] = useState('正在初始化...') const [exportMode, setExportMode] = useState<'separate' | 'long'>('separate') const { currentTheme, themeMode } = useThemeStore() // Section refs const sectionRefs = { cover: useRef(null), overview: useRef(null), bestFriend: useRef(null), monthlyFriends: useRef(null), mutualFriend: useRef(null), socialInitiative: useRef(null), peakDay: useRef(null), streak: useRef(null), heatmap: useRef(null), midnightKing: useRef(null), responseSpeed: useRef(null), topPhrases: useRef(null), ranking: useRef(null), sns: useRef(null), lostFriend: useRef(null), ending: useRef(null), } const containerRef = useRef(null) useEffect(() => { const params = new URLSearchParams(window.location.hash.split('?')[1] || '') const yearParam = params.get('year') const parsedYear = yearParam ? parseInt(yearParam, 10) : new Date().getFullYear() const year = Number.isNaN(parsedYear) ? new Date().getFullYear() : parsedYear generateReport(year) }, []) const generateReport = async (year: number) => { const taskId = registerBackgroundTask({ sourcePage: 'annualReport', title: '年度报告生成', detail: `正在生成 ${formatYearLabel(year)} 年度报告`, progressText: '初始化', cancelable: true }) setIsLoading(true) setError(null) setLoadingProgress(0) const removeProgressListener = window.electronAPI.annualReport.onProgress?.((payload: { status: string; progress: number }) => { setLoadingProgress(payload.progress) setLoadingStage(payload.status) updateBackgroundTask(taskId, { detail: payload.status || '正在生成年度报告', progressText: `${Math.max(0, Math.round(payload.progress || 0))}%` }) }) try { const result = await window.electronAPI.annualReport.generateReport(year) removeProgressListener?.() if (isBackgroundTaskCancelRequested(taskId)) { finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,当前报告结果未继续写入页面' }) setIsLoading(false) return } setLoadingProgress(100) setLoadingStage('完成') if (result.success && result.data) { finishBackgroundTask(taskId, 'completed', { detail: '年度报告生成完成', progressText: '100%' }) setTimeout(() => { setReportData(result.data!) setIsLoading(false) }, 300) } else { finishBackgroundTask(taskId, 'failed', { detail: result.error || '生成年度报告失败' }) setError(result.error || '生成报告失败') setIsLoading(false) } } catch (e) { removeProgressListener?.() finishBackgroundTask(taskId, 'failed', { detail: String(e) }) setError(String(e)) setIsLoading(false) } } const formatNumber = (num: number) => num.toLocaleString() const getMostActiveTime = (data: number[][]) => { let maxHour = 0, maxWeekday = 0, maxVal = 0 data.forEach((row, w) => { row.forEach((val, h) => { if (val > maxVal) { maxVal = val; maxHour = h; maxWeekday = w } }) }) const weekdayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] return { weekday: weekdayNames[maxWeekday], hour: maxHour } } const formatTime = (seconds: number) => { if (seconds < 60) return `${seconds}秒` if (seconds < 3600) return `${Math.round(seconds / 60)}分钟` return `${Math.round(seconds / 3600)}小时` } const formatYearLabel = (value: number, withSuffix: boolean = true) => { if (value === 0) return '历史以来' return withSuffix ? `${value}年` : `${value}` } // 获取可用的板块列表 const getAvailableSections = (): SectionInfo[] => { if (!reportData) return [] const sections: SectionInfo[] = [ { id: 'cover', name: '封面', ref: sectionRefs.cover }, { id: 'overview', name: '年度概览', ref: sectionRefs.overview }, ] if (reportData.coreFriends[0]) { sections.push({ id: 'bestFriend', name: '年度挚友', ref: sectionRefs.bestFriend }) } sections.push({ id: 'monthlyFriends', name: '月度好友', ref: sectionRefs.monthlyFriends }) if (reportData.mutualFriend) { sections.push({ id: 'mutualFriend', name: '双向奔赴', ref: sectionRefs.mutualFriend }) } if (reportData.socialInitiative) { sections.push({ id: 'socialInitiative', name: '社交主动性', ref: sectionRefs.socialInitiative }) } if (reportData.peakDay) { sections.push({ id: 'peakDay', name: '巅峰时刻', ref: sectionRefs.peakDay }) } if (reportData.longestStreak) { sections.push({ id: 'streak', name: '聊天火花', ref: sectionRefs.streak }) } sections.push({ id: 'heatmap', name: '作息规律', ref: sectionRefs.heatmap }) if (reportData.midnightKing) { sections.push({ id: 'midnightKing', name: '深夜好友', ref: sectionRefs.midnightKing }) } if (reportData.responseSpeed) { sections.push({ id: 'responseSpeed', name: '回应速度', ref: sectionRefs.responseSpeed }) } if (reportData.lostFriend) { sections.push({ id: 'lostFriend', name: '曾经的好朋友', ref: sectionRefs.lostFriend }) } if (reportData.topPhrases && reportData.topPhrases.length > 0) { sections.push({ id: 'topPhrases', name: '年度常用语', ref: sectionRefs.topPhrases }) } sections.push({ id: 'ranking', name: '好友排行', ref: sectionRefs.ranking }) if (reportData.snsStats && reportData.snsStats.totalPosts > 0) { sections.push({ id: 'sns', name: '朋友圈', ref: sectionRefs.sns }) } sections.push({ id: 'ending', name: '尾声', ref: sectionRefs.ending }) return sections } // 导出单个板块 - 统一 16:9 尺寸 const exportSection = async (section: SectionInfo): Promise<{ name: string; data: string } | null> => { const element = section.ref.current if (!element) { return null } // 固定输出尺寸 1920x1080 (16:9) const OUTPUT_WIDTH = 1920 const OUTPUT_HEIGHT = 1080 try { const selection = window.getSelection() if (selection && selection.rangeCount > 0) selection.removeAllRanges() const activeEl = document.activeElement as HTMLElement | null activeEl?.blur?.() document.body.classList.add('exporting-snapshot') document.documentElement.classList.add('exporting-snapshot') const originalStyle = element.style.cssText element.style.minHeight = 'auto' element.style.padding = '40px 20px' element.style.background = 'transparent' element.style.backgroundColor = 'transparent' element.style.boxShadow = 'none' // 修复词云 const wordCloudInner = element.querySelector('.word-cloud-inner') as HTMLElement const wordTags = element.querySelectorAll('.word-tag') as NodeListOf let wordCloudOriginalStyle = '' const wordTagOriginalStyles: string[] = [] if (wordCloudInner) { wordCloudOriginalStyle = wordCloudInner.style.cssText wordCloudInner.style.transform = 'none' } wordTags.forEach((tag, i) => { wordTagOriginalStyles[i] = tag.style.cssText tag.style.opacity = String(tag.style.getPropertyValue('--final-opacity') || '1') tag.style.animation = 'none' }) await new Promise(r => setTimeout(r, 50)) const computedStyle = getComputedStyle(document.documentElement) const bgColor = computedStyle.getPropertyValue('--bg-primary').trim() || '#F9F8F6' const canvas = await html2canvas(element, { backgroundColor: 'transparent', // 透明背景,让 SVG 图案显示 scale: 2, useCORS: true, allowTaint: true, logging: false, onclone: (clonedDoc) => { clonedDoc.body.classList.add('exporting-snapshot') clonedDoc.documentElement.classList.add('exporting-snapshot') clonedDoc.getSelection?.()?.removeAllRanges() }, }) // 恢复样式 element.style.cssText = originalStyle if (wordCloudInner) { wordCloudInner.style.cssText = wordCloudOriginalStyle } wordTags.forEach((tag, i) => { tag.style.cssText = wordTagOriginalStyles[i] }) document.body.classList.remove('exporting-snapshot') document.documentElement.classList.remove('exporting-snapshot') // 创建固定 16:9 尺寸的画布 const outputCanvas = document.createElement('canvas') outputCanvas.width = OUTPUT_WIDTH outputCanvas.height = OUTPUT_HEIGHT const ctx = outputCanvas.getContext('2d')! // 绘制带 SVG 图案的背景 const isDark = themeMode === 'dark' await drawPatternBackground(ctx, OUTPUT_WIDTH, OUTPUT_HEIGHT, bgColor, isDark) // 边距 (留出更多空白) const PADDING = 80 const contentWidth = OUTPUT_WIDTH - PADDING * 2 const contentHeight = OUTPUT_HEIGHT - PADDING * 2 // 计算缩放和居中位置 const srcRatio = canvas.width / canvas.height const dstRatio = contentWidth / contentHeight let drawWidth: number, drawHeight: number, drawX: number, drawY: number if (srcRatio > dstRatio) { // 源图更宽,以宽度为准 drawWidth = contentWidth drawHeight = contentWidth / srcRatio drawX = PADDING drawY = PADDING + (contentHeight - drawHeight) / 2 } else { // 源图更高,以高度为准 drawHeight = contentHeight drawWidth = contentHeight * srcRatio drawX = PADDING + (contentWidth - drawWidth) / 2 drawY = PADDING } ctx.drawImage(canvas, drawX, drawY, drawWidth, drawHeight) return { name: section.name, data: outputCanvas.toDataURL('image/png') } } catch (e) { document.body.classList.remove('exporting-snapshot') return null } } // 导出整个报告为长图 const exportFullReport = async (filterIds?: Set) => { if (!containerRef.current) { return } setIsExporting(true) setExportProgress('正在生成长图...') try { const selection = window.getSelection() if (selection && selection.rangeCount > 0) selection.removeAllRanges() const activeEl = document.activeElement as HTMLElement | null activeEl?.blur?.() document.body.classList.add('exporting-snapshot') document.documentElement.classList.add('exporting-snapshot') const container = containerRef.current const sections = container.querySelectorAll('.section') const originalStyles: string[] = [] sections.forEach((section, i) => { const el = section as HTMLElement originalStyles[i] = el.style.cssText el.style.minHeight = 'auto' el.style.padding = '40px 0' }) // 如果有筛选,隐藏未选中的板块 if (filterIds) { const available = getAvailableSections() available.forEach(s => { if (!filterIds.has(s.id) && s.ref.current) { s.ref.current.style.display = 'none' } }) } // 修复词云导出问题 const wordCloudInner = container.querySelector('.word-cloud-inner') as HTMLElement const wordTags = container.querySelectorAll('.word-tag') as NodeListOf let wordCloudOriginalStyle = '' const wordTagOriginalStyles: string[] = [] if (wordCloudInner) { wordCloudOriginalStyle = wordCloudInner.style.cssText wordCloudInner.style.transform = 'none' } wordTags.forEach((tag, i) => { wordTagOriginalStyles[i] = tag.style.cssText tag.style.opacity = String(tag.style.getPropertyValue('--final-opacity') || '1') tag.style.animation = 'none' }) // 等待样式生效 await new Promise(r => setTimeout(r, 100)) // 获取计算后的背景色 const computedStyle = getComputedStyle(document.documentElement) const bgColor = computedStyle.getPropertyValue('--bg-primary').trim() || '#F9F8F6' const canvas = await html2canvas(container, { backgroundColor: 'transparent', // 透明背景 scale: 2, useCORS: true, allowTaint: true, logging: false, onclone: (clonedDoc) => { clonedDoc.body.classList.add('exporting-snapshot') clonedDoc.documentElement.classList.add('exporting-snapshot') clonedDoc.getSelection?.()?.removeAllRanges() }, }) // 恢复原始样式 sections.forEach((section, i) => { const el = section as HTMLElement el.style.cssText = originalStyles[i] }) if (wordCloudInner) { wordCloudInner.style.cssText = wordCloudOriginalStyle } wordTags.forEach((tag, i) => { tag.style.cssText = wordTagOriginalStyles[i] }) document.body.classList.remove('exporting-snapshot') document.documentElement.classList.remove('exporting-snapshot') // 创建带 SVG 图案背景的输出画布 const outputCanvas = document.createElement('canvas') outputCanvas.width = canvas.width outputCanvas.height = canvas.height const ctx = outputCanvas.getContext('2d')! // 绘制 SVG 图案背景 const isDark = themeMode === 'dark' await drawPatternBackground(ctx, canvas.width, canvas.height, bgColor, isDark) // 绘制内容 ctx.drawImage(canvas, 0, 0) const dataUrl = outputCanvas.toDataURL('image/png') const link = document.createElement('a') const yearFilePrefix = reportData ? formatYearLabel(reportData.year, false) : '' link.download = `${yearFilePrefix}年度报告${filterIds ? '_自定义' : ''}.png` link.href = dataUrl document.body.appendChild(link) link.click() document.body.removeChild(link) } catch (e) { alert('导出失败: ' + String(e)) } finally { document.body.classList.remove('exporting-snapshot') document.documentElement.classList.remove('exporting-snapshot') setIsExporting(false) setExportProgress('') } } // 导出选中的板块 const exportSelectedSections = async () => { const sections = getAvailableSections().filter(s => selectedSections.has(s.id)) if (sections.length === 0) { alert('请至少选择一个板块') return } if (exportMode === 'long') { setShowExportModal(false) await exportFullReport(selectedSections) setSelectedSections(new Set()) return } setIsExporting(true) setShowExportModal(false) const exportedImages: { name: string; data: string }[] = [] for (let i = 0; i < sections.length; i++) { const section = sections[i] setExportProgress(`正在导出: ${section.name} (${i + 1}/${sections.length})`) const result = await exportSection(section) if (result) { exportedImages.push(result) } } if (exportedImages.length === 0) { alert('导出失败') setIsExporting(false) setExportProgress('') return } const dirResult = await window.electronAPI.dialog.openDirectory({ title: '选择导出文件夹', properties: ['openDirectory', 'createDirectory'] }) if (dirResult.canceled || !dirResult.filePaths?.[0]) { setIsExporting(false) setExportProgress('') return } setExportProgress('正在写入文件...') const yearFilePrefix = reportData ? formatYearLabel(reportData.year, false) : '' const exportResult = await window.electronAPI.annualReport.exportImages({ baseDir: dirResult.filePaths[0], folderName: `${yearFilePrefix}年度报告_分模块`, images: exportedImages.map((img) => ({ name: `${yearFilePrefix}年度报告_${img.name}.png`, dataUrl: img.data })) }) if (!exportResult.success) { alert('导出失败: ' + (exportResult.error || '未知错误')) } setIsExporting(false) setExportProgress('') setSelectedSections(new Set()) } // 切换板块选择 const toggleSection = (id: string) => { const newSet = new Set(selectedSections) if (newSet.has(id)) { newSet.delete(id) } else { newSet.add(id) } setSelectedSections(newSet) } // 全选/取消全选 const toggleAll = () => { const sections = getAvailableSections() if (selectedSections.size === sections.length) { setSelectedSections(new Set()) } else { setSelectedSections(new Set(sections.map(s => s.id))) } } if (isLoading) { return (
{loadingProgress}%

{loadingStage}

进行中

) } if (error) { return (

生成报告失败: {error}

) } if (!reportData) { return (

暂无数据

) } const { year, totalMessages, totalFriends, coreFriends, monthlyTopFriends, peakDay, longestStreak, activityHeatmap, midnightKing, selfAvatarUrl, mutualFriend, socialInitiative, responseSpeed, topPhrases, lostFriend } = reportData const topFriend = coreFriends[0] const mostActive = getMostActiveTime(activityHeatmap.data) const socialStoryName = topFriend?.displayName || '好友' const yearTitle = formatYearLabel(year, true) const yearTitleShort = formatYearLabel(year, false) const monthlyTitle = year === 0 ? '历史以来月度好友' : `${year}年月度好友` const phrasesTitle = year === 0 ? '你在历史以来的常用语' : `你在${year}年的年度常用语` return (
{/* 背景装饰 */}
{/* 浮动操作按钮 */}
{/* 导出进度 */} {isExporting && (

正在导出

{exportProgress}

)} {/* 模块选择弹窗 */} {showExportModal && (
setShowExportModal(false)}>
e.stopPropagation()}>

{exportMode === 'long' ? '自定义导出长图' : '选择要导出的板块'}

{getAvailableSections().map(section => (
toggleSection(section.id)} >
{selectedSections.has(section.id) && }
{section.name}
))}
)}
{/* 封面 */}
WEFLOW · ANNUAL REPORT

{yearTitle}
微信聊天报告


每一条消息背后
都藏着一段独特的故事

{/* 年度概览 */}
年度概览

你和你的朋友们
互相发过

{formatNumber(totalMessages)} 条消息

在这段时光里,你与 {formatNumber(totalFriends)} 位好友交换过喜怒哀乐。
每一个对话,都是一段故事的开始。

{/* 年度挚友 */} {topFriend && (
年度挚友

{topFriend.displayName}

{formatNumber(topFriend.messageCount)} 条消息

你发出 {formatNumber(topFriend.sentCount)} 条 · TA发来 {formatNumber(topFriend.receivedCount)}


在一起,就可以

)} {/* 月度好友 */}
月度好友

{monthlyTitle}

根据12个月的聊天习惯

{monthlyTopFriends.map((m, i) => (
{m.month}月
{m.displayName}
))}

你只管说
我一直在

{/* 双向奔赴 */} {mutualFriend && (
双向奔赴

默契与平衡

{formatNumber(mutualFriend.sentCount)}
🤝
{mutualFriend.ratio}
{formatNumber(mutualFriend.receivedCount)}
{mutualFriend.displayName}

你们的互动比例接近 {mutualFriend.ratio}
你来我往,势均力敌。

)} {/* 社交主动性 */} {socialInitiative && (
社交主动性

主动才有故事

{socialInitiative.initiativeRate}% 的对话由你发起

面对 {socialStoryName} 的时候,你总是那个先开口的人。

)} {/* 巅峰时刻 */} {peakDay && (
巅峰时刻

{peakDay.date}

一天里你一共发了

{formatNumber(peakDay.messageCount)} 条消息

在这个快节奏的世界,有人正陪在你身边听你慢慢地讲
那天,你和 {peakDay.topFriend || '好友'} 的 {formatNumber(peakDay.topFriendCount || 0)} 条消息见证着这一切
有些话,只想对你说

)} {/* 聊天火花 */} {longestStreak && (
持之以恒

聊天火花

{longestStreak.friendName} 持续了

{longestStreak.days}

从 {longestStreak.startDate} 到 {longestStreak.endDate}

陪伴,是最长情的告白

)} {/* 作息规律 */}
作息规律

时间的痕迹

{mostActive.weekday} {String(mostActive.hour).padStart(2, '0')}:00 最活跃

{/* 深夜好友 */} {midnightKing && (
深夜好友

月光下的你

在这一年你留下了

{midnightKing.count} 条深夜的消息

其中 {midnightKing.displayName} 常常在深夜中陪着你胡思乱想。
你和Ta的对话占你深夜期间聊天的 {midnightKing.percentage}%

)} {/* 回应速度 */} {responseSpeed && (
回应速度

念念不忘,必有回响

{formatTime(responseSpeed.avgResponseTime)} 是你的平均回复时间

你回复 {responseSpeed.fastestFriend} 最快
平均只需 {formatTime(responseSpeed.fastestTime)}

)} {/* 曾经的好朋友 */} {lostFriend && (
曾经的好朋友

{lostFriend.displayName}

{formatNumber(lostFriend.earlyCount)} 条消息

{lostFriend.periodDesc}
你们曾有聊不完的话题

TA

人类发明后悔
来证明拥有的珍贵

)} {/* 年度常用语 - 词云 */} {topPhrases && topPhrases.length > 0 && (
年度常用语

{phrasesTitle}

这一年,你说得最多的是:
{topPhrases.slice(0, 3).map(p => p.phrase).join('、')}

颜色越深代表出现频率越高

)} {/* 朋友圈 */} {reportData.snsStats && reportData.snsStats.totalPosts > 0 && (
朋友圈

记录生活时刻

这一年,你发布了

{reportData.snsStats.totalPosts} 条朋友圈
{reportData.snsStats.topLikers.length > 0 && (

更关心你的Ta

{reportData.snsStats.topLikers.slice(0, 3).map((u, i) => (
{u.displayName}
{u.count}赞
))}
)} {reportData.snsStats.topLiked.length > 0 && (

你最关心的Ta

{reportData.snsStats.topLiked.slice(0, 3).map((u, i) => (
{u.displayName}
{u.count}赞
))}
)}
)} {/* 好友排行 */}
好友排行

聊得最多的人

{/* 领奖台 - 前三名 */}
{/* 第二名 - 左边 */} {coreFriends[1] && (
{coreFriends[1].displayName}
{formatNumber(coreFriends[1].messageCount)} 条
2
)} {/* 第一名 - 中间最高 */} {coreFriends[0] && (
👑
{coreFriends[0].displayName}
{formatNumber(coreFriends[0].messageCount)} 条
1
)} {/* 第三名 - 右边 */} {coreFriends[2] && (
{coreFriends[2].displayName}
{formatNumber(coreFriends[2].messageCount)} 条
3
)}
{/* 结尾 */}

尾声

我们总是在向前走
却很少有机会回头看看
如果这份报告让你有所触动,不妨把它分享给你在意的人
愿新的一年,
所有期待,皆有回声。

{yearTitleShort}
WEFLOW
) } export default AnnualReportWindow