mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
同步ui
This commit is contained in:
@@ -16,7 +16,7 @@ export interface DualReportFirstChat {
|
|||||||
senderUsername?: string
|
senderUsername?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DualReportYearlyStats {
|
export interface DualReportStats {
|
||||||
totalMessages: number
|
totalMessages: number
|
||||||
totalWords: number
|
totalWords: number
|
||||||
imageCount: number
|
imageCount: number
|
||||||
@@ -28,19 +28,13 @@ export interface DualReportYearlyStats {
|
|||||||
friendTopEmojiUrl?: string
|
friendTopEmojiUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DualReportWordCloud {
|
|
||||||
words: Array<{ phrase: string; count: number }>
|
|
||||||
totalWords: number
|
|
||||||
totalMessages: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DualReportData {
|
export interface DualReportData {
|
||||||
year: number
|
year: number
|
||||||
myName: string
|
selfName: string
|
||||||
friendUsername: string
|
friendUsername: string
|
||||||
friendName: string
|
friendName: string
|
||||||
firstChat: DualReportFirstChat | null
|
firstChat: DualReportFirstChat | null
|
||||||
thisYearFirstChat?: {
|
yearFirstChat?: {
|
||||||
createTime: number
|
createTime: number
|
||||||
createTimeStr: string
|
createTimeStr: string
|
||||||
content: string
|
content: string
|
||||||
@@ -48,8 +42,8 @@ export interface DualReportData {
|
|||||||
friendName: string
|
friendName: string
|
||||||
firstThreeMessages: DualReportMessage[]
|
firstThreeMessages: DualReportMessage[]
|
||||||
} | null
|
} | null
|
||||||
yearlyStats: DualReportYearlyStats
|
stats: DualReportStats
|
||||||
wordCloud: DualReportWordCloud
|
topPhrases: Array<{ phrase: string; count: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
class DualReportService {
|
class DualReportService {
|
||||||
@@ -272,7 +266,7 @@ class DualReportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let thisYearFirstChat: DualReportData['thisYearFirstChat'] = null
|
let yearFirstChat: DualReportData['yearFirstChat'] = null
|
||||||
if (!isAllTime) {
|
if (!isAllTime) {
|
||||||
this.reportProgress('获取今年首次聊天...', 20, onProgress)
|
this.reportProgress('获取今年首次聊天...', 20, onProgress)
|
||||||
const firstYearRows = await this.getFirstMessages(friendUsername, 3, startTime, endTime)
|
const firstYearRows = await this.getFirstMessages(friendUsername, 3, startTime, endTime)
|
||||||
@@ -289,7 +283,7 @@ class DualReportService {
|
|||||||
createTimeStr: this.formatDateTime(msgTime)
|
createTimeStr: this.formatDateTime(msgTime)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
thisYearFirstChat = {
|
yearFirstChat = {
|
||||||
createTime,
|
createTime,
|
||||||
createTimeStr: this.formatDateTime(createTime),
|
createTimeStr: this.formatDateTime(createTime),
|
||||||
content: String(this.decodeMessageContent(firstRow.message_content, firstRow.compress_content) || ''),
|
content: String(this.decodeMessageContent(firstRow.message_content, firstRow.compress_content) || ''),
|
||||||
@@ -301,7 +295,7 @@ class DualReportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.reportProgress('统计聊天数据...', 30, onProgress)
|
this.reportProgress('统计聊天数据...', 30, onProgress)
|
||||||
const yearlyStats: DualReportYearlyStats = {
|
const stats: DualReportStats = {
|
||||||
totalMessages: 0,
|
totalMessages: 0,
|
||||||
totalWords: 0,
|
totalWords: 0,
|
||||||
imageCount: 0,
|
imageCount: 0,
|
||||||
@@ -334,12 +328,12 @@ class DualReportService {
|
|||||||
for (const row of batch.rows) {
|
for (const row of batch.rows) {
|
||||||
const localType = parseInt(row.local_type || row.type || '1', 10)
|
const localType = parseInt(row.local_type || row.type || '1', 10)
|
||||||
const isSent = this.resolveIsSent(row, rawWxid, cleanedWxid)
|
const isSent = this.resolveIsSent(row, rawWxid, cleanedWxid)
|
||||||
yearlyStats.totalMessages += 1
|
stats.totalMessages += 1
|
||||||
|
|
||||||
if (localType === 3) yearlyStats.imageCount += 1
|
if (localType === 3) stats.imageCount += 1
|
||||||
if (localType === 34) yearlyStats.voiceCount += 1
|
if (localType === 34) stats.voiceCount += 1
|
||||||
if (localType === 47) {
|
if (localType === 47) {
|
||||||
yearlyStats.emojiCount += 1
|
stats.emojiCount += 1
|
||||||
const content = this.decodeMessageContent(row.message_content, row.compress_content)
|
const content = this.decodeMessageContent(row.message_content, row.compress_content)
|
||||||
const md5 = this.extractEmojiMd5(content)
|
const md5 = this.extractEmojiMd5(content)
|
||||||
const url = this.extractEmojiUrl(content)
|
const url = this.extractEmojiUrl(content)
|
||||||
@@ -357,7 +351,7 @@ class DualReportService {
|
|||||||
const content = this.decodeMessageContent(row.message_content, row.compress_content)
|
const content = this.decodeMessageContent(row.message_content, row.compress_content)
|
||||||
const text = String(content || '').trim()
|
const text = String(content || '').trim()
|
||||||
if (text.length > 0) {
|
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()
|
const normalized = text.replace(/\s+/g, ' ').trim()
|
||||||
if (normalized.length >= 2 &&
|
if (normalized.length >= 2 &&
|
||||||
normalized.length <= 50 &&
|
normalized.length <= 50 &&
|
||||||
@@ -405,33 +399,27 @@ class DualReportService {
|
|||||||
const myTopEmojiMd5 = pickTop(myEmojiCounts)
|
const myTopEmojiMd5 = pickTop(myEmojiCounts)
|
||||||
const friendTopEmojiMd5 = pickTop(friendEmojiCounts)
|
const friendTopEmojiMd5 = pickTop(friendEmojiCounts)
|
||||||
|
|
||||||
yearlyStats.myTopEmojiMd5 = myTopEmojiMd5
|
stats.myTopEmojiMd5 = myTopEmojiMd5
|
||||||
yearlyStats.friendTopEmojiMd5 = friendTopEmojiMd5
|
stats.friendTopEmojiMd5 = friendTopEmojiMd5
|
||||||
yearlyStats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined
|
stats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined
|
||||||
yearlyStats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined
|
stats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined
|
||||||
|
|
||||||
this.reportProgress('生成常用语词云...', 85, onProgress)
|
this.reportProgress('生成常用语词云...', 85, onProgress)
|
||||||
const wordCloudWords = Array.from(wordCountMap.entries())
|
const topPhrases = Array.from(wordCountMap.entries())
|
||||||
.filter(([_, count]) => count >= 2)
|
.filter(([_, count]) => count >= 2)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 50)
|
.slice(0, 50)
|
||||||
.map(([phrase, count]) => ({ phrase, count }))
|
.map(([phrase, count]) => ({ phrase, count }))
|
||||||
|
|
||||||
const wordCloud: DualReportWordCloud = {
|
|
||||||
words: wordCloudWords,
|
|
||||||
totalWords: yearlyStats.totalWords,
|
|
||||||
totalMessages: yearlyStats.totalMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportData: DualReportData = {
|
const reportData: DualReportData = {
|
||||||
year: reportYear,
|
year: reportYear,
|
||||||
myName,
|
selfName: myName,
|
||||||
friendUsername,
|
friendUsername,
|
||||||
friendName,
|
friendName,
|
||||||
firstChat,
|
firstChat,
|
||||||
thisYearFirstChat,
|
yearFirstChat,
|
||||||
yearlyStats,
|
stats,
|
||||||
wordCloud
|
topPhrases
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reportProgress('双人报告生成完成', 100, onProgress)
|
this.reportProgress('双人报告生成完成', 100, onProgress)
|
||||||
|
|||||||
@@ -1,220 +1,130 @@
|
|||||||
.dual-report-window {
|
.annual-report-window.dual-report-window {
|
||||||
color: var(--text-primary);
|
.dual-names {
|
||||||
padding: 32px 24px 60px;
|
font-size: clamp(24px, 4vw, 40px);
|
||||||
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;
|
|
||||||
font-weight: 700;
|
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;
|
font-size: 12px;
|
||||||
color: var(--text-tertiary);
|
color: var(--ar-text-sub);
|
||||||
margin-top: 4px;
|
}
|
||||||
|
|
||||||
|
.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); }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, type CSSProperties } from 'react'
|
import { useEffect, useState, type CSSProperties } from 'react'
|
||||||
import { Loader2 } from 'lucide-react'
|
import './AnnualReportWindow.scss'
|
||||||
import './DualReportWindow.scss'
|
import './DualReportWindow.scss'
|
||||||
|
|
||||||
interface DualReportMessage {
|
interface DualReportMessage {
|
||||||
@@ -11,7 +11,7 @@ interface DualReportMessage {
|
|||||||
|
|
||||||
interface DualReportData {
|
interface DualReportData {
|
||||||
year: number
|
year: number
|
||||||
myName: string
|
selfName: string
|
||||||
friendUsername: string
|
friendUsername: string
|
||||||
friendName: string
|
friendName: string
|
||||||
firstChat: {
|
firstChat: {
|
||||||
@@ -21,7 +21,7 @@ interface DualReportData {
|
|||||||
isSentByMe: boolean
|
isSentByMe: boolean
|
||||||
senderUsername?: string
|
senderUsername?: string
|
||||||
} | null
|
} | null
|
||||||
thisYearFirstChat?: {
|
yearFirstChat?: {
|
||||||
createTime: number
|
createTime: number
|
||||||
createTimeStr: string
|
createTimeStr: string
|
||||||
content: string
|
content: string
|
||||||
@@ -29,7 +29,7 @@ interface DualReportData {
|
|||||||
friendName: string
|
friendName: string
|
||||||
firstThreeMessages: DualReportMessage[]
|
firstThreeMessages: DualReportMessage[]
|
||||||
} | null
|
} | null
|
||||||
yearlyStats: {
|
stats: {
|
||||||
totalMessages: number
|
totalMessages: number
|
||||||
totalWords: number
|
totalWords: number
|
||||||
imageCount: number
|
imageCount: number
|
||||||
@@ -40,19 +40,16 @@ interface DualReportData {
|
|||||||
myTopEmojiUrl?: string
|
myTopEmojiUrl?: string
|
||||||
friendTopEmojiUrl?: string
|
friendTopEmojiUrl?: string
|
||||||
}
|
}
|
||||||
wordCloud: {
|
topPhrases: Array<{ phrase: string; count: number }>
|
||||||
words: Array<{ phrase: string; count: number }>
|
|
||||||
totalWords: number
|
|
||||||
totalMessages: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WordCloud = ({ words }: { words: { phrase: string; count: number }[] }) => {
|
const WordCloud = ({ words }: { words: { phrase: string; count: number }[] }) => {
|
||||||
if (!words || words.length === 0) {
|
if (!words || words.length === 0) {
|
||||||
return <div className="word-cloud-empty">暂无高频语句</div>
|
return <div className="word-cloud-empty">暂无高频语句</div>
|
||||||
}
|
}
|
||||||
const maxCount = words.length > 0 ? words[0].count : 1
|
const sortedWords = [...words].sort((a, b) => b.count - a.count)
|
||||||
const topWords = words.slice(0, 32)
|
const maxCount = sortedWords.length > 0 ? sortedWords[0].count : 1
|
||||||
|
const topWords = sortedWords.slice(0, 32)
|
||||||
const baseSize = 520
|
const baseSize = 520
|
||||||
|
|
||||||
const seededRandom = (seed: number) => {
|
const seededRandom = (seed: number) => {
|
||||||
@@ -205,7 +202,7 @@ function DualReportWindow() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadEmojis = async () => {
|
const loadEmojis = async () => {
|
||||||
if (!reportData) return
|
if (!reportData) return
|
||||||
const stats = reportData.yearlyStats
|
const stats = reportData.stats
|
||||||
if (stats.myTopEmojiUrl) {
|
if (stats.myTopEmojiUrl) {
|
||||||
const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5)
|
const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5)
|
||||||
if (res.success && res.localPath) {
|
if (res.success && res.localPath) {
|
||||||
@@ -224,25 +221,35 @@ function DualReportWindow() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="dual-report-window loading">
|
<div className="annual-report-window loading">
|
||||||
<Loader2 size={36} className="spin" />
|
<div className="loading-ring">
|
||||||
<div className="progress">{loadingProgress}%</div>
|
<svg viewBox="0 0 100 100">
|
||||||
<div className="stage">{loadingStage}</div>
|
<circle className="ring-bg" cx="50" cy="50" r="42" />
|
||||||
|
<circle
|
||||||
|
className="ring-progress"
|
||||||
|
cx="50" cy="50" r="42"
|
||||||
|
style={{ strokeDashoffset: 264 - (264 * loadingProgress / 100) }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="ring-text">{loadingProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<p className="loading-stage">{loadingStage}</p>
|
||||||
|
<p className="loading-hint">进行中</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="dual-report-window error">
|
<div className="annual-report-window error">
|
||||||
<p>生成报告失败:{error}</p>
|
<p>生成报告失败: {error}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reportData) {
|
if (!reportData) {
|
||||||
return (
|
return (
|
||||||
<div className="dual-report-window error">
|
<div className="annual-report-window error">
|
||||||
<p>暂无数据</p>
|
<p>暂无数据</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -253,112 +260,142 @@ function DualReportWindow() {
|
|||||||
const daysSince = firstChat
|
const daysSince = firstChat
|
||||||
? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000))
|
? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000))
|
||||||
: null
|
: null
|
||||||
const thisYearFirstChat = reportData.thisYearFirstChat
|
const yearFirstChat = reportData.yearFirstChat
|
||||||
const stats = reportData.yearlyStats
|
const stats = reportData.stats
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dual-report-window">
|
<div className="annual-report-window dual-report-window">
|
||||||
<section className="dual-section cover">
|
<div className="drag-region" />
|
||||||
<div className="label">DUAL REPORT</div>
|
|
||||||
<h1>{reportData.myName} & {reportData.friendName}</h1>
|
|
||||||
<p>让我们一起回顾这段独一无二的对话</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="dual-section">
|
<div className="bg-decoration">
|
||||||
<div className="section-title">首次聊天</div>
|
<div className="deco-circle c1" />
|
||||||
{firstChat ? (
|
<div className="deco-circle c2" />
|
||||||
<div className="info-card">
|
<div className="deco-circle c3" />
|
||||||
<div className="info-row">
|
<div className="deco-circle c4" />
|
||||||
<span className="info-label">第一次聊天时间</span>
|
<div className="deco-circle c5" />
|
||||||
<span className="info-value">{firstChat.createTimeStr}</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<span className="info-label">距今天数</span>
|
|
||||||
<span className="info-value">{daysSince} 天</span>
|
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<span className="info-label">首条消息</span>
|
|
||||||
<span className="info-value">{firstChat.content || '(空)'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="info-empty">暂无首条消息</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{thisYearFirstChat ? (
|
<div className="report-scroll-view">
|
||||||
<section className="dual-section">
|
<div className="report-container">
|
||||||
<div className="section-title">今年首次聊天</div>
|
<section className="section">
|
||||||
<div className="info-card">
|
<div className="label-text">WEFLOW · DUAL REPORT</div>
|
||||||
<div className="info-row">
|
<h1 className="hero-title">{yearTitle}<br />双人聊天报告</h1>
|
||||||
<span className="info-label">首次时间</span>
|
<hr className="divider" />
|
||||||
<span className="info-value">{thisYearFirstChat.createTimeStr}</span>
|
<div className="dual-names">
|
||||||
|
<span>{reportData.selfName}</span>
|
||||||
|
<span className="amp">&</span>
|
||||||
|
<span>{reportData.friendName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<p className="hero-desc">每一次对话都值得被珍藏</p>
|
||||||
<span className="info-label">发起者</span>
|
</section>
|
||||||
<span className="info-value">{thisYearFirstChat.isSentByMe ? reportData.myName : reportData.friendName}</span>
|
|
||||||
</div>
|
<section className="section">
|
||||||
<div className="message-list">
|
<div className="label-text">首次聊天</div>
|
||||||
{thisYearFirstChat.firstThreeMessages.map((msg, idx) => (
|
<h2 className="hero-title">故事的开始</h2>
|
||||||
<div key={idx} className={`message-item ${msg.isSentByMe ? 'sent' : 'received'}`}>
|
{firstChat ? (
|
||||||
<div className="message-meta">{msg.isSentByMe ? reportData.myName : reportData.friendName} · {msg.createTimeStr}</div>
|
<div className="dual-info-grid">
|
||||||
<div className="message-content">{msg.content || '(空)'}</div>
|
<div className="dual-info-card">
|
||||||
|
<div className="info-label">第一次聊天时间</div>
|
||||||
|
<div className="info-value">{firstChat.createTimeStr}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="dual-info-card">
|
||||||
|
<div className="info-label">距今天数</div>
|
||||||
|
<div className="info-value">{daysSince} 天</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-info-card full">
|
||||||
|
<div className="info-label">首条消息</div>
|
||||||
|
<div className="info-value">{firstChat.content || '(空)'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="hero-desc">暂无首条消息</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{yearFirstChat ? (
|
||||||
|
<section className="section">
|
||||||
|
<div className="label-text">今年首次聊天</div>
|
||||||
|
<h2 className="hero-title">新一年的开场</h2>
|
||||||
|
<div className="dual-info-grid">
|
||||||
|
<div className="dual-info-card">
|
||||||
|
<div className="info-label">首次时间</div>
|
||||||
|
<div className="info-value">{yearFirstChat.createTimeStr}</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-info-card">
|
||||||
|
<div className="info-label">发起者</div>
|
||||||
|
<div className="info-value">{yearFirstChat.isSentByMe ? reportData.selfName : reportData.friendName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-message-list">
|
||||||
|
{yearFirstChat.firstThreeMessages.map((msg, idx) => (
|
||||||
|
<div key={idx} className={`dual-message ${msg.isSentByMe ? 'sent' : 'received'}`}>
|
||||||
|
<div className="message-meta">{msg.isSentByMe ? reportData.selfName : reportData.friendName} · {msg.createTimeStr}</div>
|
||||||
|
<div className="message-content">{msg.content || '(空)'}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<section className="section">
|
||||||
|
<div className="label-text">常用语</div>
|
||||||
|
<h2 className="hero-title">{yearTitle}常用语</h2>
|
||||||
|
<WordCloud words={reportData.topPhrases} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section">
|
||||||
|
<div className="label-text">年度统计</div>
|
||||||
|
<h2 className="hero-title">{yearTitle}数据概览</h2>
|
||||||
|
<div className="dual-stat-grid">
|
||||||
|
<div className="dual-stat-card">
|
||||||
|
<div className="stat-num">{stats.totalMessages.toLocaleString()}</div>
|
||||||
|
<div className="stat-unit">总消息数</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-stat-card">
|
||||||
|
<div className="stat-num">{stats.totalWords.toLocaleString()}</div>
|
||||||
|
<div className="stat-unit">总字数</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-stat-card">
|
||||||
|
<div className="stat-num">{stats.imageCount.toLocaleString()}</div>
|
||||||
|
<div className="stat-unit">图片</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-stat-card">
|
||||||
|
<div className="stat-num">{stats.voiceCount.toLocaleString()}</div>
|
||||||
|
<div className="stat-unit">语音</div>
|
||||||
|
</div>
|
||||||
|
<div className="dual-stat-card">
|
||||||
|
<div className="stat-num">{stats.emojiCount.toLocaleString()}</div>
|
||||||
|
<div className="stat-unit">表情</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<section className="dual-section">
|
<div className="emoji-row">
|
||||||
<div className="section-title">{yearTitle}常用语</div>
|
<div className="emoji-card">
|
||||||
<WordCloud words={reportData.wordCloud.words} />
|
<div className="emoji-title">我常用的表情</div>
|
||||||
</section>
|
{myEmojiUrl ? (
|
||||||
|
<img src={myEmojiUrl} alt="my-emoji" />
|
||||||
|
) : (
|
||||||
|
<div className="emoji-placeholder">{stats.myTopEmojiMd5 || '暂无'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="emoji-card">
|
||||||
|
<div className="emoji-title">{reportData.friendName}常用的表情</div>
|
||||||
|
{friendEmojiUrl ? (
|
||||||
|
<img src={friendEmojiUrl} alt="friend-emoji" />
|
||||||
|
) : (
|
||||||
|
<div className="emoji-placeholder">{stats.friendTopEmojiMd5 || '暂无'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="dual-section">
|
<section className="section">
|
||||||
<div className="section-title">{yearTitle}统计</div>
|
<div className="label-text">尾声</div>
|
||||||
<div className="stats-grid">
|
<h2 className="hero-title">谢谢你一直在</h2>
|
||||||
<div className="stat-card">
|
<p className="hero-desc">愿我们继续把故事写下去</p>
|
||||||
<div className="stat-value">{stats.totalMessages.toLocaleString()}</div>
|
</section>
|
||||||
<div className="stat-label">总消息数</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-value">{stats.totalWords.toLocaleString()}</div>
|
|
||||||
<div className="stat-label">总字数</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-value">{stats.imageCount.toLocaleString()}</div>
|
|
||||||
<div className="stat-label">图片</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-value">{stats.voiceCount.toLocaleString()}</div>
|
|
||||||
<div className="stat-label">语音</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-value">{stats.emojiCount.toLocaleString()}</div>
|
|
||||||
<div className="stat-label">表情</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="emoji-row">
|
|
||||||
<div className="emoji-card">
|
|
||||||
<div className="emoji-title">我常用的表情</div>
|
|
||||||
{myEmojiUrl ? (
|
|
||||||
<img src={myEmojiUrl} alt="my-emoji" />
|
|
||||||
) : (
|
|
||||||
<div className="emoji-placeholder">{stats.myTopEmojiMd5 || '暂无'}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="emoji-card">
|
|
||||||
<div className="emoji-title">{reportData.friendName}常用的表情</div>
|
|
||||||
{friendEmojiUrl ? (
|
|
||||||
<img src={friendEmojiUrl} alt="friend-emoji" />
|
|
||||||
) : (
|
|
||||||
<div className="emoji-placeholder">{stats.friendTopEmojiMd5 || '暂无'}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/types/electron.d.ts
vendored
12
src/types/electron.d.ts
vendored
@@ -342,7 +342,7 @@ export interface ElectronAPI {
|
|||||||
success: boolean
|
success: boolean
|
||||||
data?: {
|
data?: {
|
||||||
year: number
|
year: number
|
||||||
myName: string
|
selfName: string
|
||||||
friendUsername: string
|
friendUsername: string
|
||||||
friendName: string
|
friendName: string
|
||||||
firstChat: {
|
firstChat: {
|
||||||
@@ -352,7 +352,7 @@ export interface ElectronAPI {
|
|||||||
isSentByMe: boolean
|
isSentByMe: boolean
|
||||||
senderUsername?: string
|
senderUsername?: string
|
||||||
} | null
|
} | null
|
||||||
thisYearFirstChat?: {
|
yearFirstChat?: {
|
||||||
createTime: number
|
createTime: number
|
||||||
createTimeStr: string
|
createTimeStr: string
|
||||||
content: string
|
content: string
|
||||||
@@ -365,7 +365,7 @@ export interface ElectronAPI {
|
|||||||
createTimeStr: string
|
createTimeStr: string
|
||||||
}>
|
}>
|
||||||
} | null
|
} | null
|
||||||
yearlyStats: {
|
stats: {
|
||||||
totalMessages: number
|
totalMessages: number
|
||||||
totalWords: number
|
totalWords: number
|
||||||
imageCount: number
|
imageCount: number
|
||||||
@@ -376,11 +376,7 @@ export interface ElectronAPI {
|
|||||||
myTopEmojiUrl?: string
|
myTopEmojiUrl?: string
|
||||||
friendTopEmojiUrl?: string
|
friendTopEmojiUrl?: string
|
||||||
}
|
}
|
||||||
wordCloud: {
|
topPhrases: Array<{ phrase: string; count: number }>
|
||||||
words: Array<{ phrase: string; count: number }>
|
|
||||||
totalWords: number
|
|
||||||
totalMessages: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
|||||||
Reference in New Issue
Block a user