修复导出页头像缺失

This commit is contained in:
xuncha
2026-03-22 09:37:19 +08:00
parent 354f3fd8e2
commit e92df66bef
3 changed files with 159 additions and 17 deletions

View File

@@ -5055,7 +5055,17 @@ class ChatService {
const contact = await this.getContact(username) const contact = await this.getContact(username)
const avatarResult = await wcdbService.getAvatarUrls([username]) const avatarResult = await wcdbService.getAvatarUrls([username])
const avatarUrl = avatarResult.success && avatarResult.map ? avatarResult.map[username] : undefined let avatarUrl = avatarResult.success && avatarResult.map ? avatarResult.map[username] : undefined
if (!this.isValidAvatarUrl(avatarUrl)) {
avatarUrl = undefined
}
if (!avatarUrl) {
const headImageAvatars = await this.getAvatarsFromHeadImageDb([username])
const fallbackAvatarUrl = headImageAvatars[username]
if (this.isValidAvatarUrl(fallbackAvatarUrl)) {
avatarUrl = fallbackAvatarUrl
}
}
const displayName = contact?.remark || contact?.nickName || contact?.alias || cached?.displayName || username const displayName = contact?.remark || contact?.nickName || contact?.alias || cached?.displayName || username
const cacheEntry: ContactCacheEntry = { const cacheEntry: ContactCacheEntry = {
avatarUrl, avatarUrl,
@@ -5523,6 +5533,13 @@ class ChatService {
avatarUrl = avatarCandidate avatarUrl = avatarCandidate
} }
} }
if (!avatarUrl) {
const headImageAvatars = await this.getAvatarsFromHeadImageDb([normalizedSessionId])
const fallbackAvatarUrl = headImageAvatars[normalizedSessionId]
if (this.isValidAvatarUrl(fallbackAvatarUrl)) {
avatarUrl = fallbackAvatarUrl
}
}
if (!Number.isFinite(messageCount)) { if (!Number.isFinite(messageCount)) {
messageCount = messageCountResult.status === 'fulfilled' && messageCount = messageCountResult.status === 'fulfilled' &&

View File

@@ -49,6 +49,7 @@ import { SnsPostItem } from '../components/Sns/SnsPostItem'
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog' import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog' import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm' import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm'
import { Avatar } from '../components/Avatar'
import type { SnsPost } from '../types/sns' import type { SnsPost } from '../types/sns'
import { import {
cloneExportDateRange, cloneExportDateRange,
@@ -538,6 +539,14 @@ const getAvatarLetter = (name: string): string => {
return [...name][0] || '?' return [...name][0] || '?'
} }
const normalizeExportAvatarUrl = (value?: string | null): string | undefined => {
const normalized = String(value || '').trim()
if (!normalized) return undefined
const lower = normalized.toLowerCase()
if (lower === 'null' || lower === 'undefined') return undefined
return normalized
}
const toComparableNameSet = (values: Array<string | undefined | null>): Set<string> => { const toComparableNameSet = (values: Array<string | undefined | null>): Set<string> => {
const set = new Set<string>() const set = new Set<string>()
for (const value of values) { for (const value of values) {
@@ -1713,6 +1722,7 @@ function ExportPage() {
startIndex: 0, startIndex: 0,
endIndex: -1 endIndex: -1
}) })
const avatarHydrationRequestedRef = useRef<Set<string>>(new Set())
const sessionMutualFriendsMetricsRef = useRef<Record<string, SessionMutualFriendsMetric>>({}) const sessionMutualFriendsMetricsRef = useRef<Record<string, SessionMutualFriendsMetric>>({})
const sessionMutualFriendsDirectMetricsRef = useRef<Record<string, SessionMutualFriendsMetric>>({}) const sessionMutualFriendsDirectMetricsRef = useRef<Record<string, SessionMutualFriendsMetric>>({})
const sessionMutualFriendsQueueRef = useRef<string[]>([]) const sessionMutualFriendsQueueRef = useRef<string[]>([])
@@ -1957,6 +1967,7 @@ function ExportPage() {
displayName: contact.displayName, displayName: contact.displayName,
remark: contact.remark, remark: contact.remark,
nickname: contact.nickname, nickname: contact.nickname,
alias: contact.alias,
type: contact.type type: contact.type
})) }))
).catch((error) => { ).catch((error) => {
@@ -1998,6 +2009,94 @@ function ExportPage() {
} }
}, [ensureExportCacheScope, syncContactTypeCounts]) }, [ensureExportCacheScope, syncContactTypeCounts])
const hydrateVisibleContactAvatars = useCallback(async (usernames: string[]) => {
const targets = Array.from(new Set(
(usernames || [])
.map((username) => String(username || '').trim())
.filter(Boolean)
)).filter((username) => {
if (avatarHydrationRequestedRef.current.has(username)) return false
const contact = contactsList.find((item) => item.username === username)
const session = sessions.find((item) => item.username === username)
const existingAvatarUrl = normalizeExportAvatarUrl(contact?.avatarUrl || session?.avatarUrl)
return !existingAvatarUrl
})
if (targets.length === 0) return
targets.forEach((username) => avatarHydrationRequestedRef.current.add(username))
const settled = await Promise.allSettled(
targets.map(async (username) => {
const profile = await window.electronAPI.chat.getContactAvatar(username)
return {
username,
avatarUrl: normalizeExportAvatarUrl(profile?.avatarUrl),
displayName: profile?.displayName ? String(profile.displayName).trim() : undefined
}
})
)
const avatarPatches = new Map<string, { avatarUrl?: string; displayName?: string }>()
for (const item of settled) {
if (item.status !== 'fulfilled') continue
const { username, avatarUrl, displayName } = item.value
if (!avatarUrl && !displayName) continue
avatarPatches.set(username, { avatarUrl, displayName })
}
if (avatarPatches.size === 0) return
const now = Date.now()
setContactsList((prev) => prev.map((contact) => {
const patch = avatarPatches.get(contact.username)
if (!patch) return contact
return {
...contact,
displayName: patch.displayName || contact.displayName,
avatarUrl: patch.avatarUrl || contact.avatarUrl
}
}))
setSessions((prev) => prev.map((session) => {
const patch = avatarPatches.get(session.username)
if (!patch) return session
return {
...session,
displayName: patch.displayName || session.displayName,
avatarUrl: patch.avatarUrl || session.avatarUrl
}
}))
setSessionDetail((prev) => {
if (!prev) return prev
const patch = avatarPatches.get(prev.wxid)
if (!patch) return prev
return {
...prev,
displayName: patch.displayName || prev.displayName,
avatarUrl: patch.avatarUrl || prev.avatarUrl
}
})
let avatarCacheChanged = false
for (const [username, patch] of avatarPatches.entries()) {
if (!patch.avatarUrl) continue
const previous = contactsAvatarCacheRef.current[username]
if (previous?.avatarUrl === patch.avatarUrl) continue
contactsAvatarCacheRef.current[username] = {
avatarUrl: patch.avatarUrl,
updatedAt: now,
checkedAt: now
}
avatarCacheChanged = true
}
if (avatarCacheChanged) {
setAvatarCacheUpdatedAt(now)
const scopeKey = exportCacheScopeRef.current
if (scopeKey) {
void configService.setContactsAvatarCache(scopeKey, contactsAvatarCacheRef.current).catch(() => {})
}
}
}, [contactsList, sessions])
useEffect(() => { useEffect(() => {
if (!isExportRoute) return if (!isExportRoute) return
let cancelled = false let cancelled = false
@@ -3824,10 +3923,12 @@ function ExportPage() {
displayName: contact.displayName || contact.username, displayName: contact.displayName || contact.username,
remark: contact.remark, remark: contact.remark,
nickname: contact.nickname, nickname: contact.nickname,
alias: contact.alias,
type: contact.type type: contact.type
})) }))
const persistAt = Date.now() const persistAt = Date.now()
setContactsList(contactsForPersist)
setSessions(nextSessions) setSessions(nextSessions)
sessionsHydratedAtRef.current = persistAt sessionsHydratedAtRef.current = persistAt
if (hasNetworkContactsSnapshot && contactsCachePayload.length > 0) { if (hasNetworkContactsSnapshot && contactsCachePayload.length > 0) {
@@ -5380,6 +5481,11 @@ function ExportPage() {
const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex
sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex } sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex }
sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex } sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex }
void hydrateVisibleContactAvatars(
filteredContacts
.slice(startIndex, endIndex + 1)
.map((contact) => contact.username)
)
const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts) const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts)
if (visibleTargets.length === 0) return if (visibleTargets.length === 0) return
enqueueSessionMediaMetricRequests(visibleTargets, { front: true }) enqueueSessionMediaMetricRequests(visibleTargets, { front: true })
@@ -5395,10 +5501,23 @@ function ExportPage() {
enqueueSessionMediaMetricRequests, enqueueSessionMediaMetricRequests,
enqueueSessionMutualFriendsRequests, enqueueSessionMutualFriendsRequests,
filteredContacts, filteredContacts,
hydrateVisibleContactAvatars,
scheduleSessionMediaMetricWorker, scheduleSessionMediaMetricWorker,
scheduleSessionMutualFriendsWorker scheduleSessionMutualFriendsWorker
]) ])
useEffect(() => {
if (filteredContacts.length === 0) return
const bootstrapTargets = filteredContacts.slice(0, 24).map((contact) => contact.username)
void hydrateVisibleContactAvatars(bootstrapTargets)
}, [filteredContacts, hydrateVisibleContactAvatars])
useEffect(() => {
const sessionId = String(sessionDetail?.wxid || '').trim()
if (!sessionId) return
void hydrateVisibleContactAvatars([sessionId])
}, [hydrateVisibleContactAvatars, sessionDetail?.wxid])
useEffect(() => { useEffect(() => {
if (activeTaskCount > 0) return if (activeTaskCount > 0) return
if (filteredContacts.length === 0) return if (filteredContacts.length === 0) return
@@ -5750,7 +5869,7 @@ function ExportPage() {
displayName: mappedSession?.displayName || mappedContact?.displayName || prev?.displayName || normalizedSessionId, displayName: mappedSession?.displayName || mappedContact?.displayName || prev?.displayName || normalizedSessionId,
remark: sameSession ? prev?.remark : mappedContact?.remark, remark: sameSession ? prev?.remark : mappedContact?.remark,
nickName: sameSession ? prev?.nickName : mappedContact?.nickname, nickName: sameSession ? prev?.nickName : mappedContact?.nickname,
alias: sameSession ? prev?.alias : undefined, alias: sameSession ? prev?.alias : mappedContact?.alias,
avatarUrl: mappedSession?.avatarUrl || mappedContact?.avatarUrl || (sameSession ? prev?.avatarUrl : undefined), avatarUrl: mappedSession?.avatarUrl || mappedContact?.avatarUrl || (sameSession ? prev?.avatarUrl : undefined),
messageCount: initialMessageCount ?? (sameSession ? prev.messageCount : Number.NaN), messageCount: initialMessageCount ?? (sameSession ? prev.messageCount : Number.NaN),
voiceMessages: metricVoice ?? (sameSession ? prev?.voiceMessages : undefined), voiceMessages: metricVoice ?? (sameSession ? prev?.voiceMessages : undefined),
@@ -6627,11 +6746,12 @@ function ExportPage() {
</button> </button>
</div> </div>
<div className="contact-avatar"> <div className="contact-avatar">
{contact.avatarUrl ? ( <Avatar
<img src={contact.avatarUrl} alt="" loading="lazy" /> src={normalizeExportAvatarUrl(contact.avatarUrl)}
) : ( name={contact.displayName}
<span>{getAvatarLetter(contact.displayName)}</span> size="100%"
)} shape="rounded"
/>
</div> </div>
<div className="contact-info"> <div className="contact-info">
<div className="contact-name">{contact.displayName}</div> <div className="contact-name">{contact.displayName}</div>
@@ -7514,11 +7634,12 @@ function ExportPage() {
<div className="session-mutual-friends-header"> <div className="session-mutual-friends-header">
<div className="session-mutual-friends-header-main"> <div className="session-mutual-friends-header-main">
<div className="session-mutual-friends-avatar"> <div className="session-mutual-friends-avatar">
{sessionMutualFriendsDialogTarget.avatarUrl ? ( <Avatar
<img src={sessionMutualFriendsDialogTarget.avatarUrl} alt="" /> src={normalizeExportAvatarUrl(sessionMutualFriendsDialogTarget.avatarUrl)}
) : ( name={sessionMutualFriendsDialogTarget.displayName}
<span>{getAvatarLetter(sessionMutualFriendsDialogTarget.displayName)}</span> size="100%"
)} shape="rounded"
/>
</div> </div>
<div className="session-mutual-friends-meta"> <div className="session-mutual-friends-meta">
<h4>{sessionMutualFriendsDialogTarget.displayName} </h4> <h4>{sessionMutualFriendsDialogTarget.displayName} </h4>
@@ -7599,11 +7720,12 @@ function ExportPage() {
<div className="detail-header"> <div className="detail-header">
<div className="detail-header-main"> <div className="detail-header-main">
<div className="detail-header-avatar"> <div className="detail-header-avatar">
{sessionDetail?.avatarUrl ? ( <Avatar
<img src={sessionDetail.avatarUrl} alt="" /> src={normalizeExportAvatarUrl(sessionDetail?.avatarUrl)}
) : ( name={sessionDetail?.displayName || sessionDetail?.wxid || ''}
<span>{getAvatarLetter(sessionDetail?.displayName || sessionDetail?.wxid || '')}</span> size="100%"
)} shape="rounded"
/>
</div> </div>
<div className="detail-header-meta"> <div className="detail-header-meta">
<h4>{sessionDetail?.displayName || '会话详情'}</h4> <h4>{sessionDetail?.displayName || '会话详情'}</h4>

View File

@@ -662,6 +662,7 @@ export interface ContactsListCacheContact {
displayName: string displayName: string
remark?: string remark?: string
nickname?: string nickname?: string
alias?: string
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
} }
@@ -1174,6 +1175,7 @@ export async function getContactsListCache(scopeKey: string): Promise<ContactsLi
displayName, displayName,
remark: typeof item.remark === 'string' ? item.remark : undefined, remark: typeof item.remark === 'string' ? item.remark : undefined,
nickname: typeof item.nickname === 'string' ? item.nickname : undefined, nickname: typeof item.nickname === 'string' ? item.nickname : undefined,
alias: typeof item.alias === 'string' ? item.alias : undefined,
type: (type === 'friend' || type === 'group' || type === 'official' || type === 'former_friend' || type === 'other') type: (type === 'friend' || type === 'group' || type === 'official' || type === 'former_friend' || type === 'other')
? type ? type
: 'other' : 'other'
@@ -1207,6 +1209,7 @@ export async function setContactsListCache(scopeKey: string, contacts: ContactsL
displayName, displayName,
remark: contact?.remark ? String(contact.remark) : undefined, remark: contact?.remark ? String(contact.remark) : undefined,
nickname: contact?.nickname ? String(contact.nickname) : undefined, nickname: contact?.nickname ? String(contact.nickname) : undefined,
alias: contact?.alias ? String(contact.alias) : undefined,
type type
}) })
} }