修复双人年度报告相关

This commit is contained in:
xuncha
2026-02-08 22:41:50 +08:00
parent e28ef9b783
commit 2b5bb34392
9 changed files with 183 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, type CSSProperties } from 'react'
import { Clock, Zap, MessageSquare, Type, Image as ImageIcon, Mic, Smile } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Clock, Zap, MessageCircle, MessageSquare, Type, Image as ImageIcon, Mic, Smile } from 'lucide-react'
import ReportHeatmap from '../components/ReportHeatmap'
import ReportWordCloud from '../components/ReportWordCloud'
import './AnnualReportWindow.scss'
@@ -15,8 +15,10 @@ interface DualReportMessage {
interface DualReportData {
year: number
selfName: string
selfAvatarUrl?: string
friendUsername: string
friendName: string
friendAvatarUrl?: string
firstChat: {
createTime: number
createTimeStr: string
@@ -43,6 +45,8 @@ interface DualReportData {
friendTopEmojiMd5?: string
myTopEmojiUrl?: string
friendTopEmojiUrl?: string
myTopEmojiCount?: number
friendTopEmojiCount?: number
}
topPhrases: Array<{ phrase: string; count: number }>
heatmap?: number[][]
@@ -108,6 +112,8 @@ function DualReportWindow() {
useEffect(() => {
const loadEmojis = async () => {
if (!reportData) return
setMyEmojiUrl(null)
setFriendEmojiUrl(null)
const stats = reportData.stats
if (stats.myTopEmojiUrl) {
const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5)
@@ -178,6 +184,9 @@ function DualReportWindow() {
: null
const yearFirstChat = reportData.yearFirstChat
const stats = reportData.stats
const initiativeTotal = (reportData.initiative?.initiated || 0) + (reportData.initiative?.received || 0)
const initiatedPercent = initiativeTotal > 0 ? (reportData.initiative!.initiated / initiativeTotal) * 100 : 0
const receivedPercent = initiativeTotal > 0 ? (reportData.initiative!.received / initiativeTotal) * 100 : 0
const statItems = [
{ label: '总消息数', value: stats.totalMessages, icon: MessageSquare, color: '#07C160' },
{ label: '总字数', value: stats.totalWords, icon: Type, color: '#10AEFF' },
@@ -247,6 +256,30 @@ function DualReportWindow() {
return `${year}/${month}/${day} ${hour}:${minute}`
}
const getMostActiveTime = (data: number[][]) => {
let maxHour = 0
let maxWeekday = 0
let maxVal = -1
data.forEach((row, weekday) => {
row.forEach((value, hour) => {
if (value > maxVal) {
maxVal = value
maxHour = hour
maxWeekday = weekday
}
})
})
const weekdayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
return {
weekday: weekdayNames[maxWeekday] || '周一',
hour: maxHour,
value: Math.max(0, maxVal)
}
}
const mostActive = reportData.heatmap ? getMostActiveTime(reportData.heatmap) : null
const responseAvgMinutes = reportData.response ? Math.max(0, Math.round(reportData.response.avg / 60)) : 0
return (
<div className="annual-report-window dual-report-window">
<div className="drag-region" />
@@ -344,6 +377,11 @@ function DualReportWindow() {
<section className="section">
<div className="label-text"></div>
<h2 className="hero-title"></h2>
{mostActive && (
<p className="hero-desc active-time dual-active-time">
{'\u5728'} <span className="hl">{mostActive.weekday} {String(mostActive.hour).padStart(2, '0')}:00</span> {'\u6700\u6d3b\u8dc3\uff08'}{mostActive.value}{'\u6761\uff09'}
</p>
)}
<ReportHeatmap data={reportData.heatmap} />
</section>
)}
@@ -358,22 +396,28 @@ function DualReportWindow() {
</div>
<div className="initiative-bar-wrapper">
<div className="initiative-side">
<div className="avatar-placeholder"></div>
<div className="count">{reportData.initiative.initiated}</div>
<div className="avatar-placeholder">
{reportData.selfAvatarUrl ? <img src={reportData.selfAvatarUrl} alt="me-avatar" /> : '\u6211'}
</div>
<div className="count">{reportData.initiative.initiated}{'\u6b21'}</div>
<div className="percent">{initiatedPercent.toFixed(1)}%</div>
</div>
<div className="initiative-progress">
<div
className="bar-segment left"
style={{ width: `${reportData.initiative.initiated / (reportData.initiative.initiated + reportData.initiative.received) * 100}%` }}
style={{ width: `${initiatedPercent}%` }}
/>
<div
className="bar-segment right"
style={{ width: `${reportData.initiative.received / (reportData.initiative.initiated + reportData.initiative.received) * 100}%` }}
style={{ width: `${receivedPercent}%` }}
/>
</div>
<div className="initiative-side">
<div className="avatar-placeholder">{reportData.friendName.substring(0, 1)}</div>
<div className="count">{reportData.initiative.received}</div>
<div className="avatar-placeholder">
{reportData.friendAvatarUrl ? <img src={reportData.friendAvatarUrl} alt="friend-avatar" /> : reportData.friendName.substring(0, 1)}
</div>
<div className="count">{reportData.initiative.received}{'\u6b21'}</div>
<div className="percent">{receivedPercent.toFixed(1)}%</div>
</div>
</div>
</div>
@@ -383,33 +427,43 @@ function DualReportWindow() {
{reportData.response && (
<section className="section">
<div className="label-text"></div>
<h2 className="hero-title"></h2>
<h2 className="hero-title">{'\u79d2\u56de\uff0c\u662f\u56e0\u4e3a\u5728\u4e4e'}</h2>
<div className="response-grid">
<div className="response-card">
<div className="icon-box">
<Clock size={24} />
</div>
<div className="label"></div>
<div className="value">{Math.round(reportData.response.avg / 60)}<span></span></div>
<div className="label">{'\u5e73\u5747\u56de\u590d'}</div>
<div className="value">{Math.round(reportData.response.avg / 60)}<span>{'\u5206'}</span></div>
</div>
<div className="response-card fastest">
<div className="icon-box">
<Zap size={24} />
</div>
<div className="label"></div>
<div className="value">{reportData.response.fastest}<span></span></div>
<div className="label">{'\u6700\u5feb\u56de\u590d'}</div>
<div className="value">{reportData.response.fastest}<span>{'\u79d2'}</span></div>
</div>
<div className="response-card sample">
<div className="icon-box">
<MessageCircle size={24} />
</div>
<div className="label">{'\u7edf\u8ba1\u6837\u672c'}</div>
<div className="value">{reportData.response.count}<span>{'\u6b21'}</span></div>
</div>
</div>
<p className="hero-desc response-note">
{`\u5171\u7edf\u8ba1 ${reportData.response.count} \u6b21\u6709\u6548\u56de\u590d\uff0c\u5e73\u5747\u7ea6 ${responseAvgMinutes} \u5206\u949f\uff0c\u6700\u5feb ${reportData.response.fastest} \u79d2\u3002`}
</p>
</section>
)}
{reportData.streak && (
<section className="section">
<div className="label-text"></div>
<h2 className="hero-title"></h2>
<div className="label-text">{'\u804a\u5929\u706b\u82b1'}</div>
<h2 className="hero-title">{'\u6700\u957f\u8fde\u7eed\u804a\u5929'}</h2>
<div className="streak-container">
<div className="streak-flame">🔥</div>
<div className="streak-days">{reportData.streak.days}<span></span></div>
<div className="streak-flame">{'\uD83D\uDD25'}</div>
<div className="streak-days">{reportData.streak.days}<span>{'\u5929'}</span></div>
<div className="streak-range">
{reportData.streak.startDate} ~ {reportData.streak.endDate}
</div>
@@ -461,6 +515,7 @@ function DualReportWindow() {
) : (
<div className="emoji-placeholder">{stats.myTopEmojiMd5 || '暂无'}</div>
)}
<div className="emoji-count">{stats.myTopEmojiCount ? `${stats.myTopEmojiCount}\u6b21` : '\u6682\u65e0\u7edf\u8ba1'}</div>
</div>
<div className="emoji-card">
<div className="emoji-title">{reportData.friendName}</div>
@@ -469,6 +524,7 @@ function DualReportWindow() {
) : (
<div className="emoji-placeholder">{stats.friendTopEmojiMd5 || '暂无'}</div>
)}
<div className="emoji-count">{stats.friendTopEmojiCount ? `${stats.friendTopEmojiCount}\u6b21` : '\u6682\u65e0\u7edf\u8ba1'}</div>
</div>
</div>
</section>