' +
+ '
' + esc(fullTime(message)) + source + '
' +
(isSystem ? '' : avatar) + '
' +
(isSystem ? '' : esc(sender)) + '
' + media + audio + quote + structured +
contentMarkup + mediaStatus + '
' + locateAction + '
'
@@ -869,26 +1046,26 @@ const renderExportScript = (name: string): string => `
layout.classList.add('single-conversation')
return
}
- const counts = new Map()
- for (const message of allMessages) {
- const id = message.exportConversationId || ''
- counts.set(id, (counts.get(id) || 0) + 1)
- }
- const option = (id, label, total) =>
- '
'
+ const option = (id, label) => '
'
title.hidden = true
conversationFilter.hidden = false
- conversationSelect.innerHTML = option('all', '全部聊天', allMessages.length) +
- conversations.map((conversation) => option(
- conversation.id,
- conversation.name,
- counts.get(conversation.id) || 0
- )).join('')
+ conversationSelect.innerHTML = option('all', '全部聊天') +
+ conversations.map((conversation) => option(conversation.id, conversation.name)).join('')
conversationSelect.value = activeConversation
}
+ const setExpandedTimelineYear = (year) => {
+ expandedTimelineYear = year || ''
+ timeline.querySelectorAll('.timeline-year').forEach((button) => {
+ const expanded = button.dataset.year === expandedTimelineYear
+ button.setAttribute('aria-expanded', String(expanded))
+ const months = button.nextElementSibling
+ if (months) months.hidden = !expanded
+ })
+ }
const renderTimeline = () => {
if (filtered.length === 0) {
+ expandedTimelineYear = ''
timeline.innerHTML = '
没有可跳转的月份
'
return
}
@@ -898,16 +1075,29 @@ const renderExportScript = (name: string): string => `
if (key === 'unknown') continue
groups.set(key, (groups.get(key) || 0) + 1)
}
- let currentYear = ''
- let html = ''
+ const yearGroups = new Map()
for (const [key, total] of groups) {
const parts = key.split('-')
- if (parts[0] !== currentYear) {
- currentYear = parts[0]
- html += '
' + esc(currentYear) + ' 年
'
- }
- html += '
'
+ if (!yearGroups.has(parts[0])) yearGroups.set(parts[0], [])
+ yearGroups.get(parts[0]).push({ key, month: Number(parts[1]), total })
+ }
+ const years = Array.from(yearGroups.keys())
+ if (!years.includes(expandedTimelineYear)) {
+ expandedTimelineYear = years[years.length - 1] || ''
+ }
+ let html = ''
+ for (const year of years) {
+ const expanded = year === expandedTimelineYear
+ const monthsId = 'timeline-months-' + year
+ html += '
' +
+ '' +
+ '' +
+ yearGroups.get(year).map((entry) =>
+ ''
+ ).join('') + '
'
}
timeline.innerHTML = html || '
时间信息不可用
'
}
@@ -923,10 +1113,16 @@ const renderExportScript = (name: string): string => `
const updateActiveMonth = () => {
const messages = Array.from(list.querySelectorAll('.message'))
const visible = visibleMessages()
+ const listBounds = list.getBoundingClientRect()
+ const topAnchor = listBounds.top + Math.min(24, listBounds.height / 4)
const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight <= 2
+ const anchoredMessage = messages.find((message) => {
+ const bounds = message.getBoundingClientRect()
+ return bounds.bottom > topAnchor && bounds.top < listBounds.bottom
+ })
const activeMessage = atBottom
? visible[visible.length - 1] || messages[messages.length - 1]
- : visible[0] || messages[0]
+ : anchoredMessage || visible[0] || messages[0]
const key = activeMessage && activeMessage.dataset.month
let activeButton
timeline.querySelectorAll('.timeline-month').forEach((button) => {
@@ -935,6 +1131,8 @@ const renderExportScript = (name: string): string => `
if (active) activeButton = button
})
if (!activeButton) return
+ const activeYear = key.split('-')[0]
+ if (activeYear !== expandedTimelineYear) setExpandedTimelineYear(activeYear)
const timelineBounds = timeline.getBoundingClientRect()
const buttonBounds = activeButton.getBoundingClientRect()
if (buttonBounds.top < timelineBounds.top) {
@@ -1092,6 +1290,7 @@ const renderExportScript = (name: string): string => `
renderWindow()
const target = list.querySelector('.message[data-index="' + index + '"]')
setScrollTop(target ? Math.max(0, scrollTopForTarget(target, 24)) : 0)
+ setExpandedTimelineYear(key.split('-')[0])
timeline.querySelectorAll('.timeline-month').forEach((button) => {
button.classList.toggle('active', button.dataset.month === key)
})
@@ -1154,6 +1353,13 @@ const renderExportScript = (name: string): string => `
applyFilters(true)
})
timeline.addEventListener('click', (event) => {
+ const yearButton = event.target.closest('[data-year]')
+ if (yearButton) {
+ setExpandedTimelineYear(
+ yearButton.dataset.year === expandedTimelineYear ? '' : yearButton.dataset.year
+ )
+ return
+ }
const button = event.target.closest('[data-month]')
if (button) jumpToMonth(button.dataset.month)
})
@@ -1202,6 +1408,39 @@ const renderExportScript = (name: string): string => `
'更新于 ' + updatedAt
renderConversations()
applyFilters()
+ finishLoading()
+ } catch (error) {
+ console.error('初始化聊天档案失败', error)
+ failLoading('消息数据解析失败,请确认档案文件完整后重试')
+ }
+ }
+
+ if (window.__WECHAT_EXPORT__) {
+ initializeArchive()
+ return
+ }
+
+ const loadArchiveData = () => {
+ const dataScript = document.createElement('script')
+ dataScript.src = 'data/messages.js'
+ dataScript.onload = () => {
+ if (!window.__WECHAT_EXPORT__) {
+ failLoading('消息数据为空或格式不正确,请重新导出聊天档案')
+ return
+ }
+ initializeArchive()
+ }
+ dataScript.onerror = () => {
+ failLoading('未找到消息数据,请确认 data/messages.js 与当前页面在同一档案中')
+ }
+ document.body.appendChild(dataScript)
+ }
+
+ if (typeof window.requestAnimationFrame === 'function') {
+ window.requestAnimationFrame(() => window.setTimeout(loadArchiveData, 0))
+ } else {
+ window.setTimeout(loadArchiveData, 0)
+ }
})()
`
@@ -1215,6 +1454,11 @@ export function renderExportPage(name: string): string {
+
+
+
正在加载聊天档案
+
消息较多时可能需要一些时间
+
@@ -1251,7 +1495,6 @@ export function renderExportPage(name: string): string {
-