fix(export): correct profile name, sns stats, avatars and sorting

This commit is contained in:
tisonhuang
2026-03-01 15:53:01 +08:00
parent 596baad296
commit 0444ca143e
3 changed files with 148 additions and 25 deletions

View File

@@ -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,27 +406,41 @@ 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',
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(
'sns', 'sns',
null, 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) { if (friendCountPrimary.success && friendCountPrimary.rows && friendCountPrimary.rows.length > 0) {
totalFriends = this.parseCountValue(friendCountFallback.rows[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
} }
} }

View File

@@ -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

View File

@@ -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,18 +468,31 @@ 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 (
(session.displayName || '').toLowerCase().includes(keyword) || (session.displayName || '').toLowerCase().includes(keyword) ||
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>
) )
} }