This commit is contained in:
xuncha
2026-02-01 02:26:00 +08:00
parent f40f885af3
commit ddbb0c3b26
5 changed files with 276 additions and 55 deletions

View File

@@ -21,6 +21,7 @@ interface DualReportData {
isSentByMe: boolean
senderUsername?: string
} | null
firstChatMessages?: DualReportMessage[]
yearFirstChat?: {
createTime: number
createTimeStr: string
@@ -257,11 +258,72 @@ function DualReportWindow() {
const yearTitle = reportData.year === 0 ? '全部时间' : `${reportData.year}`
const firstChat = reportData.firstChat
const firstChatMessages = (reportData.firstChatMessages && reportData.firstChatMessages.length > 0)
? reportData.firstChatMessages.slice(0, 3)
: firstChat
? [{
content: firstChat.content,
isSentByMe: firstChat.isSentByMe,
createTime: firstChat.createTime,
createTimeStr: firstChat.createTimeStr
}]
: []
const daysSince = firstChat
? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000))
: null
const yearFirstChat = reportData.yearFirstChat
const stats = reportData.stats
const statItems = [
{ label: '总消息数', value: stats.totalMessages },
{ label: '总字数', value: stats.totalWords },
{ label: '图片', value: stats.imageCount },
{ label: '语音', value: stats.voiceCount },
{ label: '表情', value: stats.emojiCount },
]
const decodeEntities = (text: string) => (
text
.replace(/&/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
)
const stripCdata = (text: string) => text.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
const extractXmlText = (content: string) => {
const titleMatch = content.match(/<title>([\s\S]*?)<\/title>/i)
if (titleMatch?.[1]) return titleMatch[1]
const descMatch = content.match(/<des>([\s\S]*?)<\/des>/i)
if (descMatch?.[1]) return descMatch[1]
const summaryMatch = content.match(/<summary>([\s\S]*?)<\/summary>/i)
if (summaryMatch?.[1]) return summaryMatch[1]
const contentMatch = content.match(/<content>([\s\S]*?)<\/content>/i)
if (contentMatch?.[1]) return contentMatch[1]
return ''
}
const formatMessageContent = (content?: string) => {
const raw = String(content || '').trim()
if (!raw) return '(空)'
const hasXmlTag = /<\s*[a-zA-Z]+[^>]*>/.test(raw)
const looksLikeXml = /<\?xml|<msg\b|<appmsg\b|<sysmsg\b|<appattach\b|<emoji\b|<img\b|<voip\b/i.test(raw)
|| hasXmlTag
if (!looksLikeXml) return raw
const extracted = extractXmlText(raw)
if (!extracted) return 'XML消息'
return decodeEntities(stripCdata(extracted).trim()) || 'XML消息'
}
const formatFullDate = (timestamp: number) => {
const d = new Date(timestamp)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
return `${year}/${month}/${day} ${hour}:${minute}`
}
return (
<div className="annual-report-window dual-report-window">
@@ -279,7 +341,7 @@ function DualReportWindow() {
<div className="report-container">
<section className="section">
<div className="label-text">WEFLOW · DUAL REPORT</div>
<h1 className="hero-title">{yearTitle}<br /></h1>
<h1 className="hero-title dual-cover-title">{yearTitle}<br /></h1>
<hr className="divider" />
<div className="dual-names">
<span>{reportData.selfName}</span>
@@ -293,20 +355,33 @@ function DualReportWindow() {
<div className="label-text"></div>
<h2 className="hero-title"></h2>
{firstChat ? (
<div className="dual-info-grid">
<div className="dual-info-card">
<div className="info-label"></div>
<div className="info-value">{firstChat.createTimeStr}</div>
<>
<div className="dual-info-grid">
<div className="dual-info-card">
<div className="info-label"></div>
<div className="info-value">{formatFullDate(firstChat.createTime)}</div>
</div>
<div className="dual-info-card">
<div className="info-label"></div>
<div className="info-value">{daysSince} </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>
{firstChatMessages.length > 0 ? (
<div className="dual-message-list">
{firstChatMessages.map((msg, idx) => (
<div
key={idx}
className={`dual-message ${msg.isSentByMe ? 'sent' : 'received'}`}
>
<div className="message-meta">
{msg.isSentByMe ? reportData.selfName : reportData.friendName} · {formatFullDate(msg.createTime)}
</div>
<div className="message-content">{formatMessageContent(msg.content)}</div>
</div>
))}
</div>
) : null}
</>
) : (
<p className="hero-desc"></p>
)}
@@ -314,12 +389,14 @@ function DualReportWindow() {
{yearFirstChat ? (
<section className="section">
<div className="label-text"></div>
<h2 className="hero-title"></h2>
<div className="label-text"></div>
<h2 className="hero-title">
{reportData.year === 0 ? '你们的第一段对话' : `${reportData.year}年的第一段对话`}
</h2>
<div className="dual-info-grid">
<div className="dual-info-card">
<div className="info-label"></div>
<div className="info-value">{yearFirstChat.createTimeStr}</div>
<div className="info-label"></div>
<div className="info-value">{formatFullDate(yearFirstChat.createTime)}</div>
</div>
<div className="dual-info-card">
<div className="info-label"></div>
@@ -329,8 +406,10 @@ function DualReportWindow() {
<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 className="message-meta">
{msg.isSentByMe ? reportData.selfName : reportData.friendName} · {formatFullDate(msg.createTime)}
</div>
<div className="message-content">{formatMessageContent(msg.content)}</div>
</div>
))}
</div>
@@ -347,26 +426,16 @@ function DualReportWindow() {
<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>
{statItems.map((item) => {
const valueText = item.value.toLocaleString()
const isLong = valueText.length > 7
return (
<div key={item.label} className={`dual-stat-card ${isLong ? 'long' : ''}`}>
<div className="stat-num">{valueText}</div>
<div className="stat-unit">{item.label}</div>
</div>
)
})}
</div>
<div className="emoji-row">