From 0444ca143e4b3e5df0de3662c424f780b5c83107 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Sun, 1 Mar 2026 15:53:01 +0800 Subject: [PATCH] fix(export): correct profile name, sns stats, avatars and sorting --- electron/services/snsService.ts | 77 +++++++++++++++++++++++++++------ src/components/Sidebar.tsx | 33 ++++++++++++-- src/pages/ExportPage.tsx | 63 +++++++++++++++++++++++---- 3 files changed, 148 insertions(+), 25 deletions(-) diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index b9f43c2..9484cdb 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -242,6 +242,43 @@ class SnsService { return Number.isFinite(num) && num > 0 ? Math.floor(num) : 0 } + private pickTimelineUsername(post: any): string { + const raw = post?.username ?? post?.user_name ?? post?.userName ?? '' + if (typeof raw !== 'string') return '' + return raw.trim() + } + + private async getExportStatsFromTimeline(): Promise<{ totalPosts: number; totalFriends: number }> { + const pageSize = 500 + const uniqueUsers = new Set() + let totalPosts = 0 + let offset = 0 + + for (let round = 0; round < 2000; round++) { + const result = await wcdbService.getSnsTimeline(pageSize, offset, undefined, undefined, 0, 0) + if (!result.success || !Array.isArray(result.timeline)) { + throw new Error(result.error || '获取朋友圈统计失败') + } + + const rows = result.timeline + if (rows.length === 0) break + + totalPosts += rows.length + for (const row of rows) { + const username = this.pickTimelineUsername(row) + if (username) uniqueUsers.add(username) + } + + if (rows.length < pageSize) break + offset += rows.length + } + + return { + totalPosts, + totalFriends: uniqueUsers.size + } + } + private parseLikesFromXml(xml: string): string[] { if (!xml) return [] const likes: string[] = [] @@ -369,27 +406,41 @@ class SnsService { async getExportStats(): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> { try { let totalPosts = 0 + let totalFriends = 0 + const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine') if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) { totalPosts = this.parseCountValue(postCountResult.rows[0]) } - let totalFriends = 0 - const friendCountPrimary = await wcdbService.execQuery( - 'sns', - null, - "SELECT COUNT(DISTINCT user_name) AS total FROM SnsTimeLine WHERE user_name IS NOT NULL AND user_name <> ''" - ) - if (friendCountPrimary.success && friendCountPrimary.rows && friendCountPrimary.rows.length > 0) { - totalFriends = this.parseCountValue(friendCountPrimary.rows[0]) - } else { - const friendCountFallback = await wcdbService.execQuery( + if (totalPosts > 0) { + const friendCountPrimary = await wcdbService.execQuery( 'sns', null, - "SELECT COUNT(DISTINCT userName) AS total FROM SnsTimeLine WHERE userName IS NOT NULL AND userName <> ''" + "SELECT COUNT(DISTINCT user_name) AS total FROM SnsTimeLine WHERE user_name IS NOT NULL AND user_name <> ''" ) - if (friendCountFallback.success && friendCountFallback.rows && friendCountFallback.rows.length > 0) { - totalFriends = this.parseCountValue(friendCountFallback.rows[0]) + if (friendCountPrimary.success && friendCountPrimary.rows && friendCountPrimary.rows.length > 0) { + totalFriends = this.parseCountValue(friendCountPrimary.rows[0]) + } else { + const friendCountFallback = await wcdbService.execQuery( + 'sns', + null, + "SELECT COUNT(DISTINCT userName) AS total FROM SnsTimeLine WHERE userName IS NOT NULL AND userName <> ''" + ) + if (friendCountFallback.success && friendCountFallback.rows && friendCountFallback.rows.length > 0) { + totalFriends = this.parseCountValue(friendCountFallback.rows[0]) + } + } + } + + // 某些环境下 SnsTimeLine 统计查询会返回 0,这里回退到与导出同源的 timeline 接口统计。 + if (totalPosts <= 0 || totalFriends <= 0) { + const timelineStats = await this.getExportStatsFromTimeline() + if (timelineStats.totalPosts > 0) { + totalPosts = timelineStats.totalPosts + } + if (timelineStats.totalFriends > 0) { + totalFriends = timelineStats.totalFriends } } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 2effba3..b1478e1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -32,10 +32,37 @@ function Sidebar() { const wxid = await configService.getMyWxid() let displayName = wxid || '未识别用户' + const normalizeName = (value?: string | null): string | undefined => { + if (!value) return undefined + const trimmed = value.trim() + if (!trimmed || trimmed.toLowerCase() === 'self') return undefined + return trimmed + } + + let enrichedDisplayName: string | undefined + let fallbackSelfName: string | undefined + if (wxid) { - const myContact = await window.electronAPI.chat.getContact(wxid) - const bestName = [myContact?.remark, myContact?.nickName, myContact?.alias, wxid].find(Boolean) - if (bestName) displayName = bestName + const [myContact, enrichedResult] = await Promise.all([ + window.electronAPI.chat.getContact(wxid), + window.electronAPI.chat.enrichSessionsContactInfo([wxid, 'self']) + ]) + + enrichedDisplayName = normalizeName(enrichedResult.contacts?.[wxid]?.displayName) + fallbackSelfName = normalizeName(enrichedResult.contacts?.self?.displayName) + + const bestName = + normalizeName(myContact?.remark) || + normalizeName(myContact?.nickName) || + normalizeName(myContact?.alias) || + enrichedDisplayName || + fallbackSelfName + + if (bestName) { + displayName = bestName + } else if (fallbackSelfName && fallbackSelfName !== wxid) { + displayName = fallbackSelfName + } } let avatarUrl: string | undefined diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 7f34983..76cc77d 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -177,7 +177,7 @@ const formatAbsoluteDate = (timestamp: number): string => { } const formatRecentExportTime = (timestamp?: number, now = Date.now()): string => { - if (!timestamp) return '未导出' + if (!timestamp) return '' const diff = Math.max(0, now - timestamp) const minute = 60 * 1000 const hour = 60 * minute @@ -290,6 +290,7 @@ function ExportPage() { const progressUnsubscribeRef = useRef<(() => void) | null>(null) const runningTaskIdRef = useRef(null) const tasksRef = useRef([]) + const sessionMetricsRef = useRef>({}) const loadingMetricsRef = useRef>(new Set()) const preselectAppliedRef = useRef(false) @@ -297,6 +298,10 @@ function ExportPage() { tasksRef.current = tasks }, [tasks]) + useEffect(() => { + sessionMetricsRef.current = sessionMetrics + }, [sessionMetrics]) + const preselectSessionIds = useMemo(() => { const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null const rawList = Array.isArray(state?.preselectSessionIds) @@ -393,7 +398,7 @@ function ExportPage() { }, {}) if (sessionsResult.success && sessionsResult.sessions) { - const nextSessions = sessionsResult.sessions + const baseSessions = sessionsResult.sessions .map((session) => { const contact = nextContactMap[session.username] const kind = toKindByContactType(session, contact) @@ -405,7 +410,29 @@ function ExportPage() { avatarUrl: session.avatarUrl || contact?.avatarUrl } as SessionRow }) - .sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0)) + + const needsEnrichment = baseSessions + .filter(session => !session.avatarUrl || !session.displayName || session.displayName === session.username) + .map(session => session.username) + + let nextSessions = baseSessions + if (needsEnrichment.length > 0) { + try { + const enrichResult = await window.electronAPI.chat.enrichSessionsContactInfo(needsEnrichment) + if (enrichResult.success && enrichResult.contacts) { + nextSessions = baseSessions.map((session) => { + const extra = enrichResult.contacts?.[session.username] + return { + ...session, + displayName: extra?.displayName || session.displayName || session.username, + avatarUrl: extra?.avatarUrl || session.avatarUrl + } + }) + } + } catch (enrichError) { + console.error('导出页补充会话联系人信息失败:', enrichError) + } + } setSessions(nextSessions) } @@ -441,18 +468,31 @@ function ExportPage() { const visibleSessions = useMemo(() => { const keyword = searchKeyword.trim().toLowerCase() - return sessions.filter((session) => { + return sessions + .filter((session) => { if (session.kind !== activeTab) return false if (!keyword) return true return ( (session.displayName || '').toLowerCase().includes(keyword) || session.username.toLowerCase().includes(keyword) ) - }) - }, [sessions, activeTab, searchKeyword]) + }) + .sort((a, b) => { + const totalA = sessionMetrics[a.username]?.totalMessages ?? 0 + const totalB = sessionMetrics[b.username]?.totalMessages ?? 0 + if (totalB !== totalA) { + return totalB - totalA + } + + const latestA = sessionMetrics[a.username]?.lastTimestamp ?? a.lastTimestamp ?? 0 + const latestB = sessionMetrics[b.username]?.lastTimestamp ?? b.lastTimestamp ?? 0 + return latestB - latestA + }) + }, [sessions, activeTab, searchKeyword, sessionMetrics]) const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => { - const pending = targetSessions.filter(session => !sessionMetrics[session.username] && !loadingMetricsRef.current.has(session.username)) + const currentMetrics = sessionMetricsRef.current + const pending = targetSessions.filter(session => !currentMetrics[session.username] && !loadingMetricsRef.current.has(session.username)) if (pending.length === 0) return const updates: Record = {} @@ -494,13 +534,18 @@ function ExportPage() { if (Object.keys(updates).length > 0) { setSessionMetrics(prev => ({ ...prev, ...updates })) } - }, [sessionMetrics]) + }, []) useEffect(() => { const targets = visibleSessions.slice(0, 40) void ensureSessionMetrics(targets) }, [visibleSessions, ensureSessionMetrics]) + useEffect(() => { + if (sessions.length === 0) return + void ensureSessionMetrics(sessions) + }, [sessions, ensureSessionMetrics]) + const selectedCount = selectedSessions.size const toggleSelectSession = (sessionId: string) => { @@ -1042,7 +1087,7 @@ function ExportPage() { ) : isQueued ? '排队中' : '导出'} - {recent} + {recent && {recent}} ) }