From bd446016114906758bff81eb91f927681b4fadda Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Sun, 15 Feb 2026 14:32:47 +0800
Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E8=81=8A=E5=A4=A9=E9=A1=B5?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A5=E5=8E=86=E5=AE=9A=E4=BD=8D/?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E8=A7=A3=E6=9E=90/HTML=E5=AF=BC=E5=87=BA?=
=?UTF-8?q?=E5=88=86=E9=A1=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 /api/chat/messages/daily_counts 与 /api/chat/messages/anchor,用于月度热力图与按日/首条定位\n- messages/around 支持跨 message 分片定位,定位更稳定\n- 新增 /api/chat/chat_history/resolve 与 /api/chat/appmsg/resolve,合并转发/链接卡片可按 server_id 补全\n- 新增 /api/chat/media/favicon,并补齐 link 本地缩略图处理\n- HTML 导出支持分页加载(html_page_size),避免大聊天单文件卡顿\n- tests: 覆盖 heatmap/anchor、favicon 缓存、HTML 分页导出
---
frontend/assets/css/tailwind.css | 122 +
frontend/composables/useApi.js | 45 +
frontend/pages/chat/[[username]].vue | 1992 ++++++++++++++++-
.../chat_export_service.py | 663 +++++-
src/wechat_decrypt_tool/routers/chat.py | 1039 +++++++--
.../routers/chat_export.py | 5 +
src/wechat_decrypt_tool/routers/chat_media.py | 165 ++
tests/test_chat_export_html_paging.py | 221 ++
tests/test_chat_media_favicon.py | 133 ++
tests/test_chat_message_calendar_heatmap.py | 292 +++
10 files changed, 4375 insertions(+), 302 deletions(-)
create mode 100644 tests/test_chat_export_html_paging.py
create mode 100644 tests/test_chat_media_favicon.py
create mode 100644 tests/test_chat_message_calendar_heatmap.py
diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css
index 3bce7b4..d913f32 100644
--- a/frontend/assets/css/tailwind.css
+++ b/frontend/assets/css/tailwind.css
@@ -786,6 +786,128 @@
@apply px-3 py-3 border-b border-gray-100;
}
+ /* 时间侧边栏(按日期定位) */
+ .time-sidebar {
+ @apply w-[420px] h-full flex flex-col bg-white border-l border-gray-200 flex-shrink-0;
+ }
+
+ .time-sidebar-header {
+ @apply flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50;
+ }
+
+ .time-sidebar-title {
+ @apply flex items-center gap-2 text-sm font-medium text-gray-800;
+ }
+
+ .time-sidebar-close {
+ @apply p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-200 rounded-md transition-colors;
+ }
+
+ .time-sidebar-body {
+ @apply flex-1 overflow-y-auto min-h-0;
+ }
+
+ .time-sidebar-status {
+ @apply px-4 py-2 text-xs text-gray-600 border-b border-gray-100;
+ }
+
+ .time-sidebar-status-error {
+ @apply text-red-600;
+ }
+
+ .calendar-header {
+ @apply flex items-center justify-between px-4 py-3;
+ }
+
+ .calendar-nav-btn {
+ @apply p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
+ }
+
+ .calendar-month-label {
+ @apply text-sm font-medium text-gray-800;
+ }
+
+ .calendar-month-label-selects {
+ @apply flex items-center gap-2;
+ }
+
+ .calendar-ym-select {
+ @apply text-xs px-2 py-1 rounded-md border border-gray-200 bg-white text-gray-800 hover:border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#03C160]/20 disabled:opacity-60 disabled:cursor-not-allowed;
+ }
+
+ .calendar-weekdays {
+ @apply grid grid-cols-7 gap-1 px-4 pt-1;
+ }
+
+ .calendar-weekday {
+ @apply text-[11px] text-gray-400 text-center py-1;
+ }
+
+ .calendar-grid {
+ @apply grid grid-cols-7 gap-1 px-4 pb-4;
+ }
+
+ .calendar-day {
+ @apply h-9 rounded-md flex items-center justify-center text-xs font-medium transition-colors border border-gray-200 bg-white disabled:cursor-not-allowed;
+ }
+
+ .calendar-day-outside {
+ @apply bg-transparent border-transparent;
+ }
+
+ .calendar-day-empty {
+ @apply bg-gray-100 text-gray-400 border-gray-100;
+ }
+
+ .calendar-day-selected {
+ /* Keep background as-is (heatmap), but emphasize with a ring/outline. */
+ box-shadow: 0 0 0 2px rgba(3, 193, 96, 0.85);
+ border-color: rgba(3, 193, 96, 0.95) !important;
+ }
+
+ .calendar-day-l1 {
+ background: rgba(3, 193, 96, 0.12);
+ border-color: rgba(3, 193, 96, 0.18);
+ color: #065f46;
+ }
+
+ .calendar-day-l2 {
+ background: rgba(3, 193, 96, 0.24);
+ border-color: rgba(3, 193, 96, 0.28);
+ color: #065f46;
+ }
+
+ .calendar-day-l3 {
+ background: rgba(3, 193, 96, 0.38);
+ border-color: rgba(3, 193, 96, 0.40);
+ color: #064e3b;
+ }
+
+ .calendar-day-l4 {
+ background: rgba(3, 193, 96, 0.55);
+ border-color: rgba(3, 193, 96, 0.55);
+ color: #053d2e;
+ }
+
+ .calendar-day-l1:hover,
+ .calendar-day-l2:hover,
+ .calendar-day-l3:hover,
+ .calendar-day-l4:hover {
+ filter: brightness(0.98);
+ }
+
+ .calendar-day-number {
+ @apply select-none;
+ }
+
+ .time-sidebar-actions {
+ @apply px-4 pb-4;
+ }
+
+ .time-sidebar-action-btn {
+ @apply w-full text-xs px-3 py-2 rounded-md bg-[#03C160] text-white hover:bg-[#02a650] transition-colors disabled:opacity-60 disabled:cursor-not-allowed;
+ }
+
/* 整合搜索框样式 */
.search-input-combined {
@apply flex items-center bg-white border-2 border-gray-200 rounded-lg overflow-hidden transition-all duration-200;
diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js
index f30f7d3..956e6e1 100644
--- a/frontend/composables/useApi.js
+++ b/frontend/composables/useApi.js
@@ -180,6 +180,46 @@ export const useApi = () => {
return await request(url)
}
+ // 聊天记录日历热力图:某月每日消息数
+ const getChatMessageDailyCounts = async (params = {}) => {
+ const query = new URLSearchParams()
+ if (params && params.account) query.set('account', params.account)
+ if (params && params.username) query.set('username', params.username)
+ if (params && params.year != null) query.set('year', String(params.year))
+ if (params && params.month != null) query.set('month', String(params.month))
+ const url = '/chat/messages/daily_counts' + (query.toString() ? `?${query.toString()}` : '')
+ return await request(url)
+ }
+
+ // 聊天记录定位锚点:某日第一条 / 会话最早一条
+ const getChatMessageAnchor = async (params = {}) => {
+ const query = new URLSearchParams()
+ if (params && params.account) query.set('account', params.account)
+ if (params && params.username) query.set('username', params.username)
+ if (params && params.kind) query.set('kind', String(params.kind))
+ if (params && params.date) query.set('date', String(params.date))
+ const url = '/chat/messages/anchor' + (query.toString() ? `?${query.toString()}` : '')
+ return await request(url)
+ }
+
+ // 解析嵌套合并转发聊天记录(通过 server_id)
+ const resolveNestedChatHistory = async (params = {}) => {
+ const query = new URLSearchParams()
+ if (params && params.account) query.set('account', params.account)
+ if (params && params.server_id != null) query.set('server_id', String(params.server_id))
+ const url = '/chat/chat_history/resolve' + (query.toString() ? `?${query.toString()}` : '')
+ return await request(url)
+ }
+
+ // 解析卡片/小程序等 App 消息(通过 server_id)
+ const resolveAppMsg = async (params = {}) => {
+ const query = new URLSearchParams()
+ if (params && params.account) query.set('account', params.account)
+ if (params && params.server_id != null) query.set('server_id', String(params.server_id))
+ const url = '/chat/appmsg/resolve' + (query.toString() ? `?${query.toString()}` : '')
+ return await request(url)
+ }
+
// 朋友圈时间线
const listSnsTimeline = async (params = {}) => {
const query = new URLSearchParams()
@@ -295,6 +335,7 @@ export const useApi = () => {
output_dir: data.output_dir == null ? null : String(data.output_dir || '').trim(),
allow_process_key_extract: !!data.allow_process_key_extract,
download_remote_media: !!data.download_remote_media,
+ html_page_size: data.html_page_size != null ? Number(data.html_page_size) : 1000,
privacy_mode: !!data.privacy_mode,
file_name: data.file_name || null
}
@@ -408,6 +449,10 @@ export const useApi = () => {
buildChatSearchIndex,
listChatSearchSenders,
getChatMessagesAround,
+ getChatMessageDailyCounts,
+ getChatMessageAnchor,
+ resolveNestedChatHistory,
+ resolveAppMsg,
listSnsTimeline,
listSnsMediaCandidates,
saveSnsMediaPicks,
diff --git a/frontend/pages/chat/[[username]].vue b/frontend/pages/chat/[[username]].vue
index 6d600f5..44ff0af 100644
--- a/frontend/pages/chat/[[username]].vue
+++ b/frontend/pages/chat/[[username]].vue
@@ -271,7 +271,16 @@
- {{ contact.unreadCount > 0 ? `[${contact.unreadCount > 99 ? '99+' : contact.unreadCount}条] ` : '' }}{{ contact.lastMessage }}
+
+ {{ seg.content }}
+
+
@@ -328,6 +337,19 @@
+