From 204baa52ab3980505d7f958f0e8e1a8f59f3d295 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Mon, 2 Mar 2026 13:43:21 +0800 Subject: [PATCH] feat(sns): show per-contact post counts in filter panel --- electron/main.ts | 4 +++ electron/preload.ts | 1 + electron/services/snsService.ts | 34 ++++++++++++++++++++++++ src/components/Sns/SnsFilterPanel.tsx | 6 ++++- src/pages/SnsPage.scss | 38 ++++++++++++++++++++------- src/pages/SnsPage.tsx | 17 ++++++++---- src/types/electron.d.ts | 1 + 7 files changed, 86 insertions(+), 15 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 5985639..85a8ffb 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1044,6 +1044,10 @@ function registerIpcHandlers() { return snsService.getSnsUsernames() }) + ipcMain.handle('sns:getUserPostCounts', async () => { + return snsService.getUserPostCounts() + }) + ipcMain.handle('sns:getExportStats', async () => { return snsService.getExportStats() }) diff --git a/electron/preload.ts b/electron/preload.ts index 8723db5..1ac66df 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -294,6 +294,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime), getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'), + getUserPostCounts: () => ipcRenderer.invoke('sns:getUserPostCounts'), getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'), getExportStats: () => ipcRenderer.invoke('sns:getExportStats'), debugResource: (url: string) => ipcRenderer.invoke('sns:debugResource', url), diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index 369a003..d22c853 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -407,6 +407,40 @@ class SnsService { return { success: true, usernames: result.rows.map((r: any) => r.user_name).filter(Boolean) } } + async getUserPostCounts(): Promise<{ success: boolean; data?: Record; error?: string }> { + try { + const counts: Record = {} + const primary = await wcdbService.execQuery( + 'sns', + null, + "SELECT user_name AS username, COUNT(1) AS total FROM SnsTimeLine WHERE user_name IS NOT NULL AND user_name <> '' GROUP BY user_name" + ) + + let rows = primary.rows + if (!primary.success || !rows) { + const fallback = await wcdbService.execQuery( + 'sns', + null, + "SELECT userName AS username, COUNT(1) AS total FROM SnsTimeLine WHERE userName IS NOT NULL AND userName <> '' GROUP BY userName" + ) + if (!fallback.success || !fallback.rows) { + return { success: false, error: primary.error || fallback.error || '获取朋友圈联系人条数失败' } + } + rows = fallback.rows + } + + for (const row of rows) { + const usernameRaw = row?.username ?? row?.user_name ?? row?.userName ?? '' + const username = typeof usernameRaw === 'string' ? usernameRaw.trim() : String(usernameRaw || '').trim() + if (!username) continue + counts[username] = this.parseCountValue(row) + } + return { success: true, data: counts } + } catch (e) { + return { success: false, error: String(e) } + } + } + private async getExportStatsFromTableCount(): Promise<{ totalPosts: number; totalFriends: number }> { let totalPosts = 0 let totalFriends = 0 diff --git a/src/components/Sns/SnsFilterPanel.tsx b/src/components/Sns/SnsFilterPanel.tsx index 9894689..6c914a0 100644 --- a/src/components/Sns/SnsFilterPanel.tsx +++ b/src/components/Sns/SnsFilterPanel.tsx @@ -7,6 +7,7 @@ interface Contact { username: string displayName: string avatarUrl?: string + postCount?: number } interface SnsFilterPanelProps { @@ -150,7 +151,10 @@ export const SnsFilterPanel: React.FC = ({ onClick={() => toggleUserSelection(contact.username)} > - {contact.displayName} +
+ {contact.displayName} + {Math.max(0, Number(contact.postCount || 0))} 条 +
))} {filteredContacts.length === 0 && ( diff --git a/src/pages/SnsPage.scss b/src/pages/SnsPage.scss index 896e18c..98c2286 100644 --- a/src/pages/SnsPage.scss +++ b/src/pages/SnsPage.scss @@ -1055,9 +1055,16 @@ margin-bottom: 0; /* Remove margin to merge */ - .contact-name { - color: var(--primary); - font-weight: 600; + .contact-meta { + .contact-name { + color: var(--primary); + font-weight: 600; + } + + .contact-post-count { + color: var(--primary); + opacity: 0.9; + } } /* If the NEXT item is also selected */ @@ -1080,13 +1087,26 @@ /* Compensate for missing border */ } - .contact-name { + .contact-meta { flex: 1; - font-size: 14px; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + + .contact-name { + font-size: 14px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .contact-post-count { + font-size: 12px; + color: var(--text-tertiary); + line-height: 1.2; + } } } } diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 60f79f1..6416700 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -11,6 +11,7 @@ interface Contact { displayName: string avatarUrl?: string type?: 'friend' | 'former_friend' | 'sns_only' + postCount: number } interface SnsOverviewStats { @@ -251,11 +252,16 @@ export default function SnsPage() { const loadContacts = useCallback(async () => { setContactsLoading(true) try { - // 并行获取联系人列表和朋友圈发布者列表 - const [contactsResult, snsResult] = await Promise.all([ + // 并行获取联系人列表、朋友圈发布者列表和每个发布者的动态条数 + const [contactsResult, snsResult, snsCountsResult] = await Promise.all([ window.electronAPI.chat.getContacts(), - window.electronAPI.sns.getSnsUsernames() + window.electronAPI.sns.getSnsUsernames(), + window.electronAPI.sns.getUserPostCounts() ]) + const snsPostCountMap = new Map( + Object.entries(snsCountsResult.success ? (snsCountsResult.data || {}) : {}) + .map(([username, count]) => [username, Math.max(0, Number(count || 0))]) + ) // 以联系人为基础,按 username 去重 const contactMap = new Map() @@ -268,7 +274,8 @@ export default function SnsPage() { username: c.username, displayName: c.displayName, avatarUrl: c.avatarUrl, - type: c.type === 'former_friend' ? 'former_friend' : 'friend' + type: c.type === 'former_friend' ? 'former_friend' : 'friend', + postCount: snsPostCountMap.get(c.username) || 0 }) } } @@ -278,7 +285,7 @@ export default function SnsPage() { if (snsResult.success && snsResult.usernames) { for (const u of snsResult.usernames) { if (!contactMap.has(u)) { - contactMap.set(u, { username: u, displayName: u, type: 'sns_only' }) + contactMap.set(u, { username: u, displayName: u, type: 'sns_only', postCount: snsPostCountMap.get(u) || 0 }) } } } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index f7a1e57..e24c3d5 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -591,6 +591,7 @@ export interface ElectronAPI { onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }> getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }> + getUserPostCounts: () => Promise<{ success: boolean; data?: Record; error?: string }> getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> getExportStats: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> installBlockDeleteTrigger: () => Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }>