From f40f885af3d2f11330531238e4517381200aeb85 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Sun, 1 Feb 2026 01:26:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/dualReportService.ts | 56 ++-- src/pages/DualReportWindow.scss | 338 +++++++++---------------- src/pages/DualReportWindow.tsx | 267 ++++++++++--------- src/types/electron.d.ts | 12 +- 4 files changed, 302 insertions(+), 371 deletions(-) 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
暂无高频语句
} - const maxCount = words.length > 0 ? words[0].count : 1 - const topWords = words.slice(0, 32) + const sortedWords = [...words].sort((a, b) => b.count - a.count) + const maxCount = sortedWords.length > 0 ? sortedWords[0].count : 1 + const topWords = sortedWords.slice(0, 32) const baseSize = 520 const seededRandom = (seed: number) => { @@ -205,7 +202,7 @@ function DualReportWindow() { useEffect(() => { const loadEmojis = async () => { if (!reportData) return - const stats = reportData.yearlyStats + const stats = reportData.stats if (stats.myTopEmojiUrl) { const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5) if (res.success && res.localPath) { @@ -224,25 +221,35 @@ function DualReportWindow() { if (isLoading) { return ( -
- -
{loadingProgress}%
-
{loadingStage}
+
+
+ + + + + {loadingProgress}% +
+

{loadingStage}

+

进行中

) } if (error) { return ( -
-

生成报告失败:{error}

+
+

生成报告失败: {error}

) } if (!reportData) { return ( -
+

暂无数据

) @@ -253,112 +260,142 @@ function DualReportWindow() { const daysSince = firstChat ? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000)) : null - const thisYearFirstChat = reportData.thisYearFirstChat - const stats = reportData.yearlyStats + const yearFirstChat = reportData.yearFirstChat + const stats = reportData.stats return ( -
-
-
DUAL REPORT
-

{reportData.myName} & {reportData.friendName}

-

让我们一起回顾这段独一无二的对话

-
+
+
-
-
首次聊天
- {firstChat ? ( -
-
- 第一次聊天时间 - {firstChat.createTimeStr} -
-
- 距今天数 - {daysSince} 天 -
-
- 首条消息 - {firstChat.content || '(空)'} -
-
- ) : ( -
暂无首条消息
- )} -
+
+
+
+
+
+
+
- {thisYearFirstChat ? ( -
-
今年首次聊天
-
-
- 首次时间 - {thisYearFirstChat.createTimeStr} +
+
+
+
WEFLOW · DUAL REPORT
+

{yearTitle}
双人聊天报告

+
+
+ {reportData.selfName} + & + {reportData.friendName}
-
- 发起者 - {thisYearFirstChat.isSentByMe ? reportData.myName : reportData.friendName} -
-
- {thisYearFirstChat.firstThreeMessages.map((msg, idx) => ( -
-
{msg.isSentByMe ? reportData.myName : reportData.friendName} · {msg.createTimeStr}
-
{msg.content || '(空)'}
+

每一次对话都值得被珍藏

+
+ +
+
首次聊天
+

故事的开始

+ {firstChat ? ( +
+
+
第一次聊天时间
+
{firstChat.createTimeStr}
- ))} +
+
距今天数
+
{daysSince} 天
+
+
+
首条消息
+
{firstChat.content || '(空)'}
+
+
+ ) : ( +

暂无首条消息

+ )} +
+ + {yearFirstChat ? ( +
+
今年首次聊天
+

新一年的开场

+
+
+
首次时间
+
{yearFirstChat.createTimeStr}
+
+
+
发起者
+
{yearFirstChat.isSentByMe ? reportData.selfName : reportData.friendName}
+
+
+
+ {yearFirstChat.firstThreeMessages.map((msg, idx) => ( +
+
{msg.isSentByMe ? reportData.selfName : reportData.friendName} · {msg.createTimeStr}
+
{msg.content || '(空)'}
+
+ ))} +
+
+ ) : null} + +
+
常用语
+

{yearTitle}常用语

+ +
+ +
+
年度统计
+

{yearTitle}数据概览

+
+
+
{stats.totalMessages.toLocaleString()}
+
总消息数
+
+
+
{stats.totalWords.toLocaleString()}
+
总字数
+
+
+
{stats.imageCount.toLocaleString()}
+
图片
+
+
+
{stats.voiceCount.toLocaleString()}
+
语音
+
+
+
{stats.emojiCount.toLocaleString()}
+
表情
+
-
-
- ) : null} -
-
{yearTitle}常用语
- -
+
+
+
我常用的表情
+ {myEmojiUrl ? ( + my-emoji + ) : ( +
{stats.myTopEmojiMd5 || '暂无'}
+ )} +
+
+
{reportData.friendName}常用的表情
+ {friendEmojiUrl ? ( + friend-emoji + ) : ( +
{stats.friendTopEmojiMd5 || '暂无'}
+ )} +
+
+ -
-
{yearTitle}统计
-
-
-
{stats.totalMessages.toLocaleString()}
-
总消息数
-
-
-
{stats.totalWords.toLocaleString()}
-
总字数
-
-
-
{stats.imageCount.toLocaleString()}
-
图片
-
-
-
{stats.voiceCount.toLocaleString()}
-
语音
-
-
-
{stats.emojiCount.toLocaleString()}
-
表情
-
+
+
尾声
+

谢谢你一直在

+

愿我们继续把故事写下去

+
- -
-
-
我常用的表情
- {myEmojiUrl ? ( - my-emoji - ) : ( -
{stats.myTopEmojiMd5 || '暂无'}
- )} -
-
-
{reportData.friendName}常用的表情
- {friendEmojiUrl ? ( - friend-emoji - ) : ( -
{stats.friendTopEmojiMd5 || '暂无'}
- )} -
-
-
+
) } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index bfa4e6d..b67b8f8 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -342,7 +342,7 @@ export interface ElectronAPI { success: boolean data?: { year: number - myName: string + selfName: string friendUsername: string friendName: string firstChat: { @@ -352,7 +352,7 @@ export interface ElectronAPI { isSentByMe: boolean senderUsername?: string } | null - thisYearFirstChat?: { + yearFirstChat?: { createTime: number createTimeStr: string content: string @@ -365,7 +365,7 @@ export interface ElectronAPI { createTimeStr: string }> } | null - yearlyStats: { + stats: { totalMessages: number totalWords: number imageCount: number @@ -376,11 +376,7 @@ export interface ElectronAPI { myTopEmojiUrl?: string friendTopEmojiUrl?: string } - wordCloud: { - words: Array<{ phrase: string; count: number }> - totalWords: number - totalMessages: number - } + topPhrases: Array<{ phrase: string; count: number }> } error?: string }>