mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
fix(export): correct profile name, sns stats, avatars and sorting
This commit is contained in:
@@ -242,6 +242,43 @@ class SnsService {
|
|||||||
return Number.isFinite(num) && num > 0 ? Math.floor(num) : 0
|
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<string>()
|
||||||
|
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[] {
|
private parseLikesFromXml(xml: string): string[] {
|
||||||
if (!xml) return []
|
if (!xml) return []
|
||||||
const likes: string[] = []
|
const likes: string[] = []
|
||||||
@@ -369,12 +406,14 @@ class SnsService {
|
|||||||
async getExportStats(): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> {
|
async getExportStats(): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> {
|
||||||
try {
|
try {
|
||||||
let totalPosts = 0
|
let totalPosts = 0
|
||||||
|
let totalFriends = 0
|
||||||
|
|
||||||
const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine')
|
const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine')
|
||||||
if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) {
|
if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) {
|
||||||
totalPosts = this.parseCountValue(postCountResult.rows[0])
|
totalPosts = this.parseCountValue(postCountResult.rows[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalFriends = 0
|
if (totalPosts > 0) {
|
||||||
const friendCountPrimary = await wcdbService.execQuery(
|
const friendCountPrimary = await wcdbService.execQuery(
|
||||||
'sns',
|
'sns',
|
||||||
null,
|
null,
|
||||||
@@ -392,6 +431,18 @@ class SnsService {
|
|||||||
totalFriends = this.parseCountValue(friendCountFallback.rows[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, data: { totalPosts, totalFriends } }
|
return { success: true, data: { totalPosts, totalFriends } }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -32,10 +32,37 @@ function Sidebar() {
|
|||||||
const wxid = await configService.getMyWxid()
|
const wxid = await configService.getMyWxid()
|
||||||
let displayName = wxid || '未识别用户'
|
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) {
|
if (wxid) {
|
||||||
const myContact = await window.electronAPI.chat.getContact(wxid)
|
const [myContact, enrichedResult] = await Promise.all([
|
||||||
const bestName = [myContact?.remark, myContact?.nickName, myContact?.alias, wxid].find(Boolean)
|
window.electronAPI.chat.getContact(wxid),
|
||||||
if (bestName) displayName = bestName
|
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
|
let avatarUrl: string | undefined
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const formatAbsoluteDate = (timestamp: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatRecentExportTime = (timestamp?: number, now = Date.now()): string => {
|
const formatRecentExportTime = (timestamp?: number, now = Date.now()): string => {
|
||||||
if (!timestamp) return '未导出'
|
if (!timestamp) return ''
|
||||||
const diff = Math.max(0, now - timestamp)
|
const diff = Math.max(0, now - timestamp)
|
||||||
const minute = 60 * 1000
|
const minute = 60 * 1000
|
||||||
const hour = 60 * minute
|
const hour = 60 * minute
|
||||||
@@ -290,6 +290,7 @@ function ExportPage() {
|
|||||||
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||||
const runningTaskIdRef = useRef<string | null>(null)
|
const runningTaskIdRef = useRef<string | null>(null)
|
||||||
const tasksRef = useRef<ExportTask[]>([])
|
const tasksRef = useRef<ExportTask[]>([])
|
||||||
|
const sessionMetricsRef = useRef<Record<string, SessionMetrics>>({})
|
||||||
const loadingMetricsRef = useRef<Set<string>>(new Set())
|
const loadingMetricsRef = useRef<Set<string>>(new Set())
|
||||||
const preselectAppliedRef = useRef(false)
|
const preselectAppliedRef = useRef(false)
|
||||||
|
|
||||||
@@ -297,6 +298,10 @@ function ExportPage() {
|
|||||||
tasksRef.current = tasks
|
tasksRef.current = tasks
|
||||||
}, [tasks])
|
}, [tasks])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sessionMetricsRef.current = sessionMetrics
|
||||||
|
}, [sessionMetrics])
|
||||||
|
|
||||||
const preselectSessionIds = useMemo(() => {
|
const preselectSessionIds = useMemo(() => {
|
||||||
const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null
|
const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null
|
||||||
const rawList = Array.isArray(state?.preselectSessionIds)
|
const rawList = Array.isArray(state?.preselectSessionIds)
|
||||||
@@ -393,7 +398,7 @@ function ExportPage() {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
if (sessionsResult.success && sessionsResult.sessions) {
|
if (sessionsResult.success && sessionsResult.sessions) {
|
||||||
const nextSessions = sessionsResult.sessions
|
const baseSessions = sessionsResult.sessions
|
||||||
.map((session) => {
|
.map((session) => {
|
||||||
const contact = nextContactMap[session.username]
|
const contact = nextContactMap[session.username]
|
||||||
const kind = toKindByContactType(session, contact)
|
const kind = toKindByContactType(session, contact)
|
||||||
@@ -405,7 +410,29 @@ function ExportPage() {
|
|||||||
avatarUrl: session.avatarUrl || contact?.avatarUrl
|
avatarUrl: session.avatarUrl || contact?.avatarUrl
|
||||||
} as SessionRow
|
} 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)
|
setSessions(nextSessions)
|
||||||
}
|
}
|
||||||
@@ -441,7 +468,8 @@ function ExportPage() {
|
|||||||
|
|
||||||
const visibleSessions = useMemo(() => {
|
const visibleSessions = useMemo(() => {
|
||||||
const keyword = searchKeyword.trim().toLowerCase()
|
const keyword = searchKeyword.trim().toLowerCase()
|
||||||
return sessions.filter((session) => {
|
return sessions
|
||||||
|
.filter((session) => {
|
||||||
if (session.kind !== activeTab) return false
|
if (session.kind !== activeTab) return false
|
||||||
if (!keyword) return true
|
if (!keyword) return true
|
||||||
return (
|
return (
|
||||||
@@ -449,10 +477,22 @@ function ExportPage() {
|
|||||||
session.username.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 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
|
if (pending.length === 0) return
|
||||||
|
|
||||||
const updates: Record<string, SessionMetrics> = {}
|
const updates: Record<string, SessionMetrics> = {}
|
||||||
@@ -494,13 +534,18 @@ function ExportPage() {
|
|||||||
if (Object.keys(updates).length > 0) {
|
if (Object.keys(updates).length > 0) {
|
||||||
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
||||||
}
|
}
|
||||||
}, [sessionMetrics])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const targets = visibleSessions.slice(0, 40)
|
const targets = visibleSessions.slice(0, 40)
|
||||||
void ensureSessionMetrics(targets)
|
void ensureSessionMetrics(targets)
|
||||||
}, [visibleSessions, ensureSessionMetrics])
|
}, [visibleSessions, ensureSessionMetrics])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessions.length === 0) return
|
||||||
|
void ensureSessionMetrics(sessions)
|
||||||
|
}, [sessions, ensureSessionMetrics])
|
||||||
|
|
||||||
const selectedCount = selectedSessions.size
|
const selectedCount = selectedSessions.size
|
||||||
|
|
||||||
const toggleSelectSession = (sessionId: string) => {
|
const toggleSelectSession = (sessionId: string) => {
|
||||||
@@ -1042,7 +1087,7 @@ function ExportPage() {
|
|||||||
</>
|
</>
|
||||||
) : isQueued ? '排队中' : '导出'}
|
) : isQueued ? '排队中' : '导出'}
|
||||||
</button>
|
</button>
|
||||||
<span className="row-export-time">{recent}</span>
|
{recent && <span className="row-export-time">{recent}</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user