diff --git a/electron/services/dualReportService.ts b/electron/services/dualReportService.ts index 6764bff..1b2f8f9 100644 --- a/electron/services/dualReportService.ts +++ b/electron/services/dualReportService.ts @@ -16,7 +16,7 @@ export interface DualReportFirstChat { senderUsername?: string } -export interface DualReportYearlyStats { +export interface DualReportStats { totalMessages: number totalWords: number imageCount: number @@ -28,19 +28,13 @@ export interface DualReportYearlyStats { friendTopEmojiUrl?: string } -export interface DualReportWordCloud { - words: Array<{ phrase: string; count: number }> - totalWords: number - totalMessages: number -} - export interface DualReportData { year: number - myName: string + selfName: string friendUsername: string friendName: string firstChat: DualReportFirstChat | null - thisYearFirstChat?: { + yearFirstChat?: { createTime: number createTimeStr: string content: string @@ -48,8 +42,8 @@ export interface DualReportData { friendName: string firstThreeMessages: DualReportMessage[] } | null - yearlyStats: DualReportYearlyStats - wordCloud: DualReportWordCloud + stats: DualReportStats + topPhrases: Array<{ phrase: string; count: number }> } class DualReportService { @@ -272,7 +266,7 @@ class DualReportService { } } - let thisYearFirstChat: DualReportData['thisYearFirstChat'] = null + let yearFirstChat: DualReportData['yearFirstChat'] = null if (!isAllTime) { this.reportProgress('获取今年首次聊天...', 20, onProgress) const firstYearRows = await this.getFirstMessages(friendUsername, 3, startTime, endTime) @@ -289,7 +283,7 @@ class DualReportService { createTimeStr: this.formatDateTime(msgTime) } }) - thisYearFirstChat = { + yearFirstChat = { createTime, createTimeStr: this.formatDateTime(createTime), content: String(this.decodeMessageContent(firstRow.message_content, firstRow.compress_content) || ''), @@ -301,7 +295,7 @@ class DualReportService { } this.reportProgress('统计聊天数据...', 30, onProgress) - const yearlyStats: DualReportYearlyStats = { + const stats: DualReportStats = { totalMessages: 0, totalWords: 0, imageCount: 0, @@ -334,12 +328,12 @@ class DualReportService { for (const row of batch.rows) { const localType = parseInt(row.local_type || row.type || '1', 10) const isSent = this.resolveIsSent(row, rawWxid, cleanedWxid) - yearlyStats.totalMessages += 1 + stats.totalMessages += 1 - if (localType === 3) yearlyStats.imageCount += 1 - if (localType === 34) yearlyStats.voiceCount += 1 + if (localType === 3) stats.imageCount += 1 + if (localType === 34) stats.voiceCount += 1 if (localType === 47) { - yearlyStats.emojiCount += 1 + stats.emojiCount += 1 const content = this.decodeMessageContent(row.message_content, row.compress_content) const md5 = this.extractEmojiMd5(content) const url = this.extractEmojiUrl(content) @@ -357,7 +351,7 @@ class DualReportService { const content = this.decodeMessageContent(row.message_content, row.compress_content) const text = String(content || '').trim() if (text.length > 0) { - yearlyStats.totalWords += text.replace(/\s+/g, '').length + stats.totalWords += text.replace(/\s+/g, '').length const normalized = text.replace(/\s+/g, ' ').trim() if (normalized.length >= 2 && normalized.length <= 50 && @@ -405,33 +399,27 @@ class DualReportService { const myTopEmojiMd5 = pickTop(myEmojiCounts) const friendTopEmojiMd5 = pickTop(friendEmojiCounts) - yearlyStats.myTopEmojiMd5 = myTopEmojiMd5 - yearlyStats.friendTopEmojiMd5 = friendTopEmojiMd5 - yearlyStats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined - yearlyStats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined + stats.myTopEmojiMd5 = myTopEmojiMd5 + stats.friendTopEmojiMd5 = friendTopEmojiMd5 + stats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined + stats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined this.reportProgress('生成常用语词云...', 85, onProgress) - const wordCloudWords = Array.from(wordCountMap.entries()) + const topPhrases = Array.from(wordCountMap.entries()) .filter(([_, count]) => count >= 2) .sort((a, b) => b[1] - a[1]) .slice(0, 50) .map(([phrase, count]) => ({ phrase, count })) - const wordCloud: DualReportWordCloud = { - words: wordCloudWords, - totalWords: yearlyStats.totalWords, - totalMessages: yearlyStats.totalMessages - } - const reportData: DualReportData = { year: reportYear, - myName, + selfName: myName, friendUsername, friendName, firstChat, - thisYearFirstChat, - yearlyStats, - wordCloud + yearFirstChat, + stats, + topPhrases } this.reportProgress('双人报告生成完成', 100, onProgress) diff --git a/src/pages/DualReportWindow.scss b/src/pages/DualReportWindow.scss index 2b0d19a..14d6e6c 100644 --- a/src/pages/DualReportWindow.scss +++ b/src/pages/DualReportWindow.scss @@ -1,220 +1,130 @@ -.dual-report-window { - color: var(--text-primary); - padding: 32px 24px 60px; - background: var(--bg-primary); -} - -.dual-report-window.loading, -.dual-report-window.error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 60vh; - gap: 12px; - color: var(--text-tertiary); -} - -.dual-section { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 24px; - margin: 16px auto; - max-width: 900px; -} - -.dual-section.cover { - text-align: center; - background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 10%, transparent) 0%, var(--card-bg) 100%); - - .label { - font-size: 12px; - letter-spacing: 2px; - color: var(--text-tertiary); - margin-bottom: 12px; - } - - h1 { - margin: 0 0 12px; - font-size: 36px; - } - - p { - margin: 0; - color: var(--text-secondary); - } -} - -.section-title { - font-size: 18px; - font-weight: 700; - margin-bottom: 16px; -} - -.info-card { - display: flex; - flex-direction: column; - gap: 12px; -} - -.info-row { - display: flex; - justify-content: space-between; - gap: 16px; - font-size: 14px; -} - -.info-label { - color: var(--text-tertiary); -} - -.info-value { - color: var(--text-primary); - font-weight: 600; -} - -.info-empty { - color: var(--text-tertiary); -} - -.message-list { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 8px; -} - -.message-item { - padding: 10px 12px; - border-radius: 12px; - background: color-mix(in srgb, var(--primary) 6%, transparent); - - &.received { - background: color-mix(in srgb, var(--border-color) 35%, transparent); - } -} - -.message-meta { - font-size: 12px; - color: var(--text-tertiary); - margin-bottom: 6px; -} - -.message-content { - font-size: 14px; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 12px; - margin-bottom: 16px; -} - -.stat-card { - background: color-mix(in srgb, var(--primary) 6%, transparent); - border-radius: 12px; - padding: 14px; - text-align: center; - - .stat-value { - font-size: 20px; +.annual-report-window.dual-report-window { + .dual-names { + font-size: clamp(24px, 4vw, 40px); font-weight: 700; + display: flex; + align-items: center; + gap: 12px; + margin: 8px 0 16px; + color: var(--ar-text-main); + + .amp { + color: var(--ar-primary); + } } - .stat-label { + .dual-info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 16px; + } + + .dual-info-card { + background: var(--ar-card-bg); + border: 1px solid var(--bg-tertiary, rgba(0, 0, 0, 0.05)); + border-radius: 14px; + padding: 16px; + + &.full { + grid-column: 1 / -1; + } + + .info-label { + font-size: 12px; + color: var(--ar-text-sub); + margin-bottom: 8px; + } + + .info-value { + font-size: 16px; + font-weight: 600; + color: var(--ar-text-main); + } + } + + .dual-message-list { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + + .dual-message { + background: var(--ar-card-bg); + border-radius: 14px; + padding: 14px; + + &.received { + background: var(--ar-card-bg-hover); + } + + .message-meta { + font-size: 12px; + color: var(--ar-text-sub); + margin-bottom: 6px; + } + + .message-content { + font-size: 14px; + color: var(--ar-text-main); + } + } + + .dual-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 16px; + margin: 20px 0 24px; + } + + .dual-stat-card { + background: var(--ar-card-bg); + border-radius: 16px; + padding: 18px; + text-align: center; + } + + .emoji-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + } + + .emoji-card { + border: 1px solid var(--bg-tertiary, rgba(0, 0, 0, 0.08)); + border-radius: 16px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + justify-content: center; + background: var(--ar-card-bg); + + img { + width: 64px; + height: 64px; + object-fit: contain; + } + } + + .emoji-title { font-size: 12px; - color: var(--text-tertiary); - margin-top: 4px; + color: var(--ar-text-sub); + } + + .emoji-placeholder { + font-size: 12px; + color: var(--ar-text-sub); + word-break: break-all; + text-align: center; + } + + .word-cloud-empty { + color: var(--ar-text-sub); + font-size: 14px; + text-align: center; + padding: 24px 0; } } - -.emoji-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 12px; -} - -.emoji-card { - border: 1px solid var(--border-color); - border-radius: 14px; - padding: 14px; - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - justify-content: center; - - img { - width: 64px; - height: 64px; - object-fit: contain; - } -} - -.emoji-title { - font-size: 12px; - color: var(--text-tertiary); -} - -.emoji-placeholder { - font-size: 12px; - color: var(--text-secondary); - word-break: break-all; - text-align: center; -} - -.word-cloud-wrapper { - position: relative; - width: 100%; - padding-top: 80%; - background: color-mix(in srgb, var(--primary) 4%, transparent); - border-radius: 18px; - overflow: hidden; -} - -.word-cloud-inner { - position: absolute; - inset: 0; -} - -.word-tag { - position: absolute; - font-weight: 600; - color: var(--text-primary); - transform: translate(-50%, -50%); - opacity: 0; - animation: fadeUp 0.8s ease forwards; -} - -.word-cloud-empty { - color: var(--text-tertiary); - font-size: 14px; - text-align: center; - padding: 40px 0; -} - -.progress { - font-size: 20px; - font-weight: 700; -} - -.stage { - font-size: 12px; - color: var(--text-tertiary); -} - -.spin { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -@keyframes fadeUp { - from { opacity: 0; transform: translate(-50%, -50%) translateY(10px); } - to { opacity: var(--final-opacity, 1); transform: translate(-50%, -50%) translateY(0); } -} diff --git a/src/pages/DualReportWindow.tsx b/src/pages/DualReportWindow.tsx index 2112f88..883acc3 100644 --- a/src/pages/DualReportWindow.tsx +++ b/src/pages/DualReportWindow.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type CSSProperties } from 'react' -import { Loader2 } from 'lucide-react' +import './AnnualReportWindow.scss' import './DualReportWindow.scss' interface DualReportMessage { @@ -11,7 +11,7 @@ interface DualReportMessage { interface DualReportData { year: number - myName: string + selfName: string friendUsername: string friendName: string firstChat: { @@ -21,7 +21,7 @@ interface DualReportData { isSentByMe: boolean senderUsername?: string } | null - thisYearFirstChat?: { + yearFirstChat?: { createTime: number createTimeStr: string content: string @@ -29,7 +29,7 @@ interface DualReportData { friendName: string firstThreeMessages: DualReportMessage[] } | null - yearlyStats: { + stats: { totalMessages: number totalWords: number imageCount: number @@ -40,19 +40,16 @@ interface DualReportData { myTopEmojiUrl?: string friendTopEmojiUrl?: string } - wordCloud: { - words: Array<{ phrase: string; count: number }> - totalWords: number - totalMessages: number - } + topPhrases: Array<{ phrase: string; count: number }> } const WordCloud = ({ words }: { words: { phrase: string; count: number }[] }) => { if (!words || words.length === 0) { return
{loadingStage}
+进行中
生成报告失败:{error}
+生成报告失败: {error}
暂无数据
让我们一起回顾这段独一无二的对话
-每一次对话都值得被珍藏
+ + +暂无首条消息
+ )} +愿我们继续把故事写下去
+