mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(sns): show per-contact post counts in filter panel
This commit is contained in:
@@ -1044,6 +1044,10 @@ function registerIpcHandlers() {
|
|||||||
return snsService.getSnsUsernames()
|
return snsService.getSnsUsernames()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('sns:getUserPostCounts', async () => {
|
||||||
|
return snsService.getUserPostCounts()
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('sns:getExportStats', async () => {
|
ipcMain.handle('sns:getExportStats', async () => {
|
||||||
return snsService.getExportStats()
|
return snsService.getExportStats()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) =>
|
getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) =>
|
||||||
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime),
|
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime),
|
||||||
getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'),
|
getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'),
|
||||||
|
getUserPostCounts: () => ipcRenderer.invoke('sns:getUserPostCounts'),
|
||||||
getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'),
|
getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'),
|
||||||
getExportStats: () => ipcRenderer.invoke('sns:getExportStats'),
|
getExportStats: () => ipcRenderer.invoke('sns:getExportStats'),
|
||||||
debugResource: (url: string) => ipcRenderer.invoke('sns:debugResource', url),
|
debugResource: (url: string) => ipcRenderer.invoke('sns:debugResource', url),
|
||||||
|
|||||||
@@ -407,6 +407,40 @@ class SnsService {
|
|||||||
return { success: true, usernames: result.rows.map((r: any) => r.user_name).filter(Boolean) }
|
return { success: true, usernames: result.rows.map((r: any) => r.user_name).filter(Boolean) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserPostCounts(): Promise<{ success: boolean; data?: Record<string, number>; error?: string }> {
|
||||||
|
try {
|
||||||
|
const counts: Record<string, number> = {}
|
||||||
|
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 }> {
|
private async getExportStatsFromTableCount(): Promise<{ totalPosts: number; totalFriends: number }> {
|
||||||
let totalPosts = 0
|
let totalPosts = 0
|
||||||
let totalFriends = 0
|
let totalFriends = 0
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface Contact {
|
|||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
|
postCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsFilterPanelProps {
|
interface SnsFilterPanelProps {
|
||||||
@@ -150,7 +151,10 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
onClick={() => toggleUserSelection(contact.username)}
|
onClick={() => toggleUserSelection(contact.username)}
|
||||||
>
|
>
|
||||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||||
<span className="contact-name">{contact.displayName}</span>
|
<div className="contact-meta">
|
||||||
|
<span className="contact-name">{contact.displayName}</span>
|
||||||
|
<span className="contact-post-count">{Math.max(0, Number(contact.postCount || 0))} 条</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{filteredContacts.length === 0 && (
|
{filteredContacts.length === 0 && (
|
||||||
|
|||||||
@@ -1055,9 +1055,16 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
/* Remove margin to merge */
|
/* Remove margin to merge */
|
||||||
|
|
||||||
.contact-name {
|
.contact-meta {
|
||||||
color: var(--primary);
|
.contact-name {
|
||||||
font-weight: 600;
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-post-count {
|
||||||
|
color: var(--primary);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If the NEXT item is also selected */
|
/* If the NEXT item is also selected */
|
||||||
@@ -1080,13 +1087,26 @@
|
|||||||
/* Compensate for missing border */
|
/* Compensate for missing border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-name {
|
.contact-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
min-width: 0;
|
||||||
color: var(--text-secondary);
|
display: flex;
|
||||||
white-space: nowrap;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
gap: 2px;
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface Contact {
|
|||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
type?: 'friend' | 'former_friend' | 'sns_only'
|
type?: 'friend' | 'former_friend' | 'sns_only'
|
||||||
|
postCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsOverviewStats {
|
interface SnsOverviewStats {
|
||||||
@@ -251,11 +252,16 @@ export default function SnsPage() {
|
|||||||
const loadContacts = useCallback(async () => {
|
const loadContacts = useCallback(async () => {
|
||||||
setContactsLoading(true)
|
setContactsLoading(true)
|
||||||
try {
|
try {
|
||||||
// 并行获取联系人列表和朋友圈发布者列表
|
// 并行获取联系人列表、朋友圈发布者列表和每个发布者的动态条数
|
||||||
const [contactsResult, snsResult] = await Promise.all([
|
const [contactsResult, snsResult, snsCountsResult] = await Promise.all([
|
||||||
window.electronAPI.chat.getContacts(),
|
window.electronAPI.chat.getContacts(),
|
||||||
window.electronAPI.sns.getSnsUsernames()
|
window.electronAPI.sns.getSnsUsernames(),
|
||||||
|
window.electronAPI.sns.getUserPostCounts()
|
||||||
])
|
])
|
||||||
|
const snsPostCountMap = new Map<string, number>(
|
||||||
|
Object.entries(snsCountsResult.success ? (snsCountsResult.data || {}) : {})
|
||||||
|
.map(([username, count]) => [username, Math.max(0, Number(count || 0))])
|
||||||
|
)
|
||||||
|
|
||||||
// 以联系人为基础,按 username 去重
|
// 以联系人为基础,按 username 去重
|
||||||
const contactMap = new Map<string, Contact>()
|
const contactMap = new Map<string, Contact>()
|
||||||
@@ -268,7 +274,8 @@ export default function SnsPage() {
|
|||||||
username: c.username,
|
username: c.username,
|
||||||
displayName: c.displayName,
|
displayName: c.displayName,
|
||||||
avatarUrl: c.avatarUrl,
|
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) {
|
if (snsResult.success && snsResult.usernames) {
|
||||||
for (const u of snsResult.usernames) {
|
for (const u of snsResult.usernames) {
|
||||||
if (!contactMap.has(u)) {
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -591,6 +591,7 @@ export interface ElectronAPI {
|
|||||||
onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void
|
onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void
|
||||||
selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }>
|
selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }>
|
||||||
getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }>
|
getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }>
|
||||||
|
getUserPostCounts: () => Promise<{ success: boolean; data?: Record<string, number>; error?: string }>
|
||||||
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }>
|
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }>
|
||||||
getExportStats: () => 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 }>
|
installBlockDeleteTrigger: () => Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }>
|
||||||
|
|||||||
Reference in New Issue
Block a user