mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
refactor(sns): remove contact post-count stats flow
This commit is contained in:
@@ -1260,10 +1260,6 @@ 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()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -337,7 +337,6 @@ 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),
|
||||||
|
|||||||
@@ -701,44 +701,6 @@ class SnsService {
|
|||||||
return { success: false, error: primary.error || fallback.error || '获取朋友圈联系人失败' }
|
return { success: false, error: primary.error || fallback.error || '获取朋友圈联系人失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
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 || rows.length === 0) {
|
|
||||||
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 || fallback.rows.length === 0) {
|
|
||||||
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
|
|
||||||
const countRaw = row?.total ?? row?.count ?? row?.cnt
|
|
||||||
const parsedCount = Number(countRaw)
|
|
||||||
counts[username] = Number.isFinite(parsedCount) && parsedCount > 0
|
|
||||||
? Math.floor(parsedCount)
|
|
||||||
: this.parseCountValue(row)
|
|
||||||
}
|
|
||||||
return { success: true, data: counts }
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, error: String(e) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getExportStatsFromTableCount(myWxid?: string): Promise<{ totalPosts: number; totalFriends: number; myPosts: number | null }> {
|
private async getExportStatsFromTableCount(myWxid?: string): Promise<{ totalPosts: number; totalFriends: number; myPosts: number | null }> {
|
||||||
let totalPosts = 0
|
let totalPosts = 0
|
||||||
let totalFriends = 0
|
let totalFriends = 0
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ interface Contact {
|
|||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
postCount?: number
|
|
||||||
postCountStatus?: 'loading' | 'ready' | 'error'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsFilterPanelProps {
|
interface SnsFilterPanelProps {
|
||||||
@@ -59,22 +57,12 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
setJumpTargetDate(undefined)
|
setJumpTargetDate(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPostCountDisplay = (contact: Contact) => {
|
|
||||||
if (contact.postCountStatus === 'error') {
|
|
||||||
return { text: '统计失败', className: 'is-error' }
|
|
||||||
}
|
|
||||||
if (contact.postCountStatus !== 'ready') {
|
|
||||||
return { text: '统计中', className: 'is-loading' }
|
|
||||||
}
|
|
||||||
return { text: `${Math.max(0, Number(contact.postCount || 0))} 条`, className: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEmptyStateText = () => {
|
const getEmptyStateText = () => {
|
||||||
if (loading && contacts.length === 0) {
|
if (loading && contacts.length === 0) {
|
||||||
return '正在加载联系人...'
|
return '正在加载联系人...'
|
||||||
}
|
}
|
||||||
if (contacts.length === 0) {
|
if (contacts.length === 0) {
|
||||||
return '有朋友圈数据的好友或者曾经的好友数量为 0'
|
return '暂无好友或曾经的好友'
|
||||||
}
|
}
|
||||||
return '没有找到联系人'
|
return '没有找到联系人'
|
||||||
}
|
}
|
||||||
@@ -166,7 +154,6 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
|
|
||||||
<div className="contact-list-scroll">
|
<div className="contact-list-scroll">
|
||||||
{filteredContacts.map(contact => {
|
{filteredContacts.map(contact => {
|
||||||
const countDisplay = getPostCountDisplay(contact)
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={contact.username}
|
key={contact.username}
|
||||||
@@ -176,7 +163,6 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||||
<div className="contact-meta">
|
<div className="contact-meta">
|
||||||
<span className="contact-name">{contact.displayName}</span>
|
<span className="contact-name">{contact.displayName}</span>
|
||||||
<span className={`contact-post-count ${countDisplay.className}`}>{countDisplay.text}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1083,11 +1083,6 @@
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
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 */
|
||||||
@@ -1124,20 +1119,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-post-count {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
line-height: 1.2;
|
|
||||||
|
|
||||||
&.is-loading {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-error {
|
|
||||||
color: var(--color-error, #f44336);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ import * as configService from '../services/config'
|
|||||||
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||||
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
||||||
const SNS_PAGE_CACHE_SCOPE_FALLBACK = '__default__'
|
const SNS_PAGE_CACHE_SCOPE_FALLBACK = '__default__'
|
||||||
const CONTACTS_PRUNE_BATCH_SIZE = 40
|
|
||||||
const CONTACTS_PRUNE_INTERVAL_MS = 80
|
|
||||||
|
|
||||||
interface Contact {
|
interface Contact {
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
type?: 'friend' | 'former_friend' | 'sns_only'
|
type?: 'friend' | 'former_friend' | 'sns_only'
|
||||||
postCount?: number
|
|
||||||
postCountStatus: 'loading' | 'ready' | 'error'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsOverviewStats {
|
interface SnsOverviewStats {
|
||||||
@@ -93,7 +89,6 @@ export default function SnsPage() {
|
|||||||
const cacheScopeKeyRef = useRef('')
|
const cacheScopeKeyRef = useRef('')
|
||||||
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
||||||
const contactsLoadTokenRef = useRef(0)
|
const contactsLoadTokenRef = useRef(0)
|
||||||
const contactsPruneTimerRef = useRef<number | null>(null)
|
|
||||||
|
|
||||||
// Sync posts ref
|
// Sync posts ref
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -114,14 +109,6 @@ export default function SnsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jumpTargetDateRef.current = jumpTargetDate
|
jumpTargetDateRef.current = jumpTargetDate
|
||||||
}, [jumpTargetDate])
|
}, [jumpTargetDate])
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (contactsPruneTimerRef.current !== null) {
|
|
||||||
window.clearTimeout(contactsPruneTimerRef.current)
|
|
||||||
contactsPruneTimerRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
// 在 DOM 更新后、浏览器绘制前同步调整滚动位置,防止向上加载时页面跳动
|
// 在 DOM 更新后、浏览器绘制前同步调整滚动位置,防止向上加载时页面跳动
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const snapshot = scrollAdjustmentRef.current;
|
const snapshot = scrollAdjustmentRef.current;
|
||||||
@@ -403,12 +390,8 @@ export default function SnsPage() {
|
|||||||
}
|
}
|
||||||
}, [jumpTargetDate, persistSnsPageCache, searchKeyword, selectedUsernames])
|
}, [jumpTargetDate, persistSnsPageCache, searchKeyword, selectedUsernames])
|
||||||
|
|
||||||
// Load Contacts(先展示全量好友/曾经好友,再按朋友圈条数逐步剔除 0 条联系人)
|
// Load Contacts(仅加载好友/曾经好友,不再统计朋友圈条数)
|
||||||
const loadContacts = useCallback(async () => {
|
const loadContacts = useCallback(async () => {
|
||||||
if (contactsPruneTimerRef.current !== null) {
|
|
||||||
window.clearTimeout(contactsPruneTimerRef.current)
|
|
||||||
contactsPruneTimerRef.current = null
|
|
||||||
}
|
|
||||||
const requestToken = ++contactsLoadTokenRef.current
|
const requestToken = ++contactsLoadTokenRef.current
|
||||||
setContactsLoading(true)
|
setContactsLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -422,8 +405,7 @@ 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'
|
||||||
postCountStatus: 'loading'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,62 +435,9 @@ export default function SnsPage() {
|
|||||||
setContacts(contactsList)
|
setContacts(contactsList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const snsCountsResult = await window.electronAPI.sns.getUserPostCounts()
|
|
||||||
if (requestToken !== contactsLoadTokenRef.current) return
|
|
||||||
|
|
||||||
if (snsCountsResult.success && snsCountsResult.data) {
|
|
||||||
const snsPostCountMap = new Map<string, number>(
|
|
||||||
Object.entries(snsCountsResult.data).map(([username, count]) => [username, Math.max(0, Number(count || 0))])
|
|
||||||
)
|
|
||||||
const contactsWithCounts: Contact[] = contactsList.map(contact => ({
|
|
||||||
...contact,
|
|
||||||
postCount: snsPostCountMap.get(contact.username) ?? 0,
|
|
||||||
postCountStatus: 'ready' as const
|
|
||||||
}))
|
|
||||||
setContacts(contactsWithCounts)
|
|
||||||
|
|
||||||
const zeroCountUsernames = contactsWithCounts
|
|
||||||
.filter(contact => (contact.postCount || 0) <= 0)
|
|
||||||
.map(contact => contact.username)
|
|
||||||
|
|
||||||
if (zeroCountUsernames.length > 0) {
|
|
||||||
let cursor = 0
|
|
||||||
const pruneNextBatch = () => {
|
|
||||||
if (requestToken !== contactsLoadTokenRef.current) {
|
|
||||||
contactsPruneTimerRef.current = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const batch = zeroCountUsernames.slice(cursor, cursor + CONTACTS_PRUNE_BATCH_SIZE)
|
|
||||||
if (batch.length === 0) {
|
|
||||||
contactsPruneTimerRef.current = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const batchSet = new Set(batch)
|
|
||||||
setContacts(prev => prev.filter(contact => !batchSet.has(contact.username)))
|
|
||||||
cursor += CONTACTS_PRUNE_BATCH_SIZE
|
|
||||||
if (cursor < zeroCountUsernames.length) {
|
|
||||||
contactsPruneTimerRef.current = window.setTimeout(pruneNextBatch, CONTACTS_PRUNE_INTERVAL_MS)
|
|
||||||
} else {
|
|
||||||
contactsPruneTimerRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contactsPruneTimerRef.current = window.setTimeout(pruneNextBatch, CONTACTS_PRUNE_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load SNS contact post counts:', snsCountsResult.error)
|
|
||||||
setContacts(prev => prev.map(contact => ({
|
|
||||||
...contact,
|
|
||||||
postCountStatus: 'error'
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestToken !== contactsLoadTokenRef.current) return
|
if (requestToken !== contactsLoadTokenRef.current) return
|
||||||
console.error('Failed to load contacts:', error)
|
console.error('Failed to load contacts:', error)
|
||||||
setContacts(prev => prev.map(contact => ({
|
|
||||||
...contact,
|
|
||||||
postCountStatus: 'error'
|
|
||||||
})))
|
|
||||||
} finally {
|
} finally {
|
||||||
if (requestToken === contactsLoadTokenRef.current) {
|
if (requestToken === contactsLoadTokenRef.current) {
|
||||||
setContactsLoading(false)
|
setContactsLoading(false)
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -770,7 +770,6 @@ 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; myPosts: number | null }; error?: string }>
|
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }>
|
||||||
getExportStats: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }>
|
getExportStats: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; 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