mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(export): reuse contacts cache for session names and avatars
This commit is contained in:
@@ -472,6 +472,22 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-cache-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item.syncing {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table-tabs {
|
.table-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ const getAvatarLetter = (name: string): string => {
|
|||||||
const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
||||||
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
||||||
|
const EXPORT_AVATAR_RECHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
||||||
|
type SessionDataSource = 'cache' | 'network' | null
|
||||||
|
|
||||||
const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<T | null> => {
|
const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<T | null> => {
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -236,6 +238,39 @@ const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toContactMapFromCaches = (
|
||||||
|
contacts: configService.ContactsListCacheContact[],
|
||||||
|
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
|
||||||
|
): Record<string, ContactInfo> => {
|
||||||
|
const map: Record<string, ContactInfo> = {}
|
||||||
|
for (const contact of contacts || []) {
|
||||||
|
if (!contact?.username) continue
|
||||||
|
map[contact.username] = {
|
||||||
|
...contact,
|
||||||
|
avatarUrl: avatarEntries[contact.username]?.avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSessionRowsWithContacts = (
|
||||||
|
sessions: AppChatSession[],
|
||||||
|
contactMap: Record<string, ContactInfo>
|
||||||
|
): SessionRow[] => {
|
||||||
|
return sessions
|
||||||
|
.map((session) => {
|
||||||
|
const contact = contactMap[session.username]
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
kind: toKindByContactType(session, contact),
|
||||||
|
wechatId: contact?.username || session.username,
|
||||||
|
displayName: contact?.displayName || session.displayName || session.username,
|
||||||
|
avatarUrl: contact?.avatarUrl || session.avatarUrl
|
||||||
|
} as SessionRow
|
||||||
|
})
|
||||||
|
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
||||||
|
}
|
||||||
|
|
||||||
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
||||||
writeLayout,
|
writeLayout,
|
||||||
onChange
|
onChange
|
||||||
@@ -300,6 +335,9 @@ function ExportPage() {
|
|||||||
const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true)
|
const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true)
|
||||||
const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false)
|
const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false)
|
||||||
const [sessions, setSessions] = useState<SessionRow[]>([])
|
const [sessions, setSessions] = useState<SessionRow[]>([])
|
||||||
|
const [sessionDataSource, setSessionDataSource] = useState<SessionDataSource>(null)
|
||||||
|
const [sessionContactsUpdatedAt, setSessionContactsUpdatedAt] = useState<number | null>(null)
|
||||||
|
const [sessionAvatarUpdatedAt, setSessionAvatarUpdatedAt] = useState<number | null>(null)
|
||||||
const [searchKeyword, setSearchKeyword] = useState('')
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
const [activeTab, setActiveTab] = useState<ConversationTab>('private')
|
const [activeTab, setActiveTab] = useState<ConversationTab>('private')
|
||||||
const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set())
|
const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set())
|
||||||
@@ -360,6 +398,22 @@ function ExportPage() {
|
|||||||
const exportCacheScopeRef = useRef('default')
|
const exportCacheScopeRef = useRef('default')
|
||||||
const exportCacheScopeReadyRef = useRef(false)
|
const exportCacheScopeReadyRef = useRef(false)
|
||||||
|
|
||||||
|
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
||||||
|
if (exportCacheScopeReadyRef.current) {
|
||||||
|
return exportCacheScopeRef.current
|
||||||
|
}
|
||||||
|
const [myWxid, dbPath] = await Promise.all([
|
||||||
|
configService.getMyWxid(),
|
||||||
|
configService.getDbPath()
|
||||||
|
])
|
||||||
|
const scopeKey = dbPath || myWxid
|
||||||
|
? `${dbPath || ''}::${myWxid || ''}`
|
||||||
|
: 'default'
|
||||||
|
exportCacheScopeRef.current = scopeKey
|
||||||
|
exportCacheScopeReadyRef.current = true
|
||||||
|
return scopeKey
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tasksRef.current = tasks
|
tasksRef.current = tasks
|
||||||
}, [tasks])
|
}, [tasks])
|
||||||
@@ -389,7 +443,7 @@ function ExportPage() {
|
|||||||
const loadBaseConfig = useCallback(async () => {
|
const loadBaseConfig = useCallback(async () => {
|
||||||
setIsBaseConfigLoading(true)
|
setIsBaseConfigLoading(true)
|
||||||
try {
|
try {
|
||||||
const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedWriteLayout, savedSessionMap, savedContentMap, savedSnsPostCount, myWxid, dbPath] = await Promise.all([
|
const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedWriteLayout, savedSessionMap, savedContentMap, savedSnsPostCount, exportCacheScope] = await Promise.all([
|
||||||
configService.getExportPath(),
|
configService.getExportPath(),
|
||||||
configService.getExportDefaultFormat(),
|
configService.getExportDefaultFormat(),
|
||||||
configService.getExportDefaultMedia(),
|
configService.getExportDefaultMedia(),
|
||||||
@@ -401,12 +455,8 @@ function ExportPage() {
|
|||||||
configService.getExportLastSessionRunMap(),
|
configService.getExportLastSessionRunMap(),
|
||||||
configService.getExportLastContentRunMap(),
|
configService.getExportLastContentRunMap(),
|
||||||
configService.getExportLastSnsPostCount(),
|
configService.getExportLastSnsPostCount(),
|
||||||
configService.getMyWxid(),
|
ensureExportCacheScope()
|
||||||
configService.getDbPath()
|
|
||||||
])
|
])
|
||||||
const exportCacheScope = `${dbPath || ''}::${myWxid || ''}` || 'default'
|
|
||||||
exportCacheScopeRef.current = exportCacheScope
|
|
||||||
exportCacheScopeReadyRef.current = true
|
|
||||||
|
|
||||||
const cachedSnsStats = await configService.getExportSnsStatsCache(exportCacheScope)
|
const cachedSnsStats = await configService.getExportSnsStatsCache(exportCacheScope)
|
||||||
|
|
||||||
@@ -446,7 +496,7 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsBaseConfigLoading(false)
|
setIsBaseConfigLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [ensureExportCacheScope])
|
||||||
|
|
||||||
const loadSnsStats = useCallback(async (options?: { full?: boolean; silent?: boolean }) => {
|
const loadSnsStats = useCallback(async (options?: { full?: boolean; silent?: boolean }) => {
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
@@ -506,6 +556,24 @@ function ExportPage() {
|
|||||||
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const scopeKey = await ensureExportCacheScope()
|
||||||
|
if (isStale()) return
|
||||||
|
|
||||||
|
const [cachedContactsItem, cachedAvatarItem] = await Promise.all([
|
||||||
|
configService.getContactsListCache(scopeKey),
|
||||||
|
configService.getContactsAvatarCache(scopeKey)
|
||||||
|
])
|
||||||
|
if (isStale()) return
|
||||||
|
|
||||||
|
const cachedContacts = cachedContactsItem?.contacts || []
|
||||||
|
const cachedAvatarEntries = cachedAvatarItem?.avatars || {}
|
||||||
|
const cachedContactMap = toContactMapFromCaches(cachedContacts, cachedAvatarEntries)
|
||||||
|
if (cachedContacts.length > 0) {
|
||||||
|
syncContactTypeCounts(Object.values(cachedContactMap))
|
||||||
|
}
|
||||||
|
setSessionContactsUpdatedAt(cachedContactsItem?.updatedAt || null)
|
||||||
|
setSessionAvatarUpdatedAt(cachedAvatarItem?.updatedAt || null)
|
||||||
|
|
||||||
const connectResult = await window.electronAPI.chat.connect()
|
const connectResult = await window.electronAPI.chat.connect()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
console.error('连接失败:', connectResult.error)
|
console.error('连接失败:', connectResult.error)
|
||||||
@@ -517,42 +585,54 @@ function ExportPage() {
|
|||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
if (sessionsResult.success && sessionsResult.sessions) {
|
if (sessionsResult.success && sessionsResult.sessions) {
|
||||||
const baseSessions = sessionsResult.sessions
|
const rawSessions = sessionsResult.sessions
|
||||||
.map((session) => {
|
const baseSessions = toSessionRowsWithContacts(rawSessions, cachedContactMap)
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
kind: toKindByContactType(session),
|
|
||||||
wechatId: session.username,
|
|
||||||
displayName: session.displayName || session.username,
|
|
||||||
avatarUrl: session.avatarUrl
|
|
||||||
} as SessionRow
|
|
||||||
})
|
|
||||||
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
|
||||||
|
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
setSessions(baseSessions)
|
setSessions(baseSessions)
|
||||||
|
setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network')
|
||||||
|
if (cachedContacts.length === 0) {
|
||||||
|
setSessionContactsUpdatedAt(Date.now())
|
||||||
|
}
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
||||||
// 后台补齐联系人字段(昵称、头像、类型),不阻塞首屏会话列表渲染。
|
// 后台补齐联系人字段(昵称、头像、类型),不阻塞首屏会话列表渲染。
|
||||||
setIsSessionEnriching(true)
|
setIsSessionEnriching(true)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
if (isStale()) return
|
let contactMap = { ...cachedContactMap }
|
||||||
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
let avatarEntries = { ...cachedAvatarEntries }
|
||||||
if (isStale()) return
|
let hasFreshNetworkData = false
|
||||||
|
|
||||||
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
if (isStale()) return
|
||||||
if (contacts.length > 0) {
|
if (cachedContacts.length === 0) {
|
||||||
syncContactTypeCounts(contacts)
|
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
||||||
|
if (isStale()) return
|
||||||
|
|
||||||
|
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
||||||
|
if (contacts.length > 0) {
|
||||||
|
hasFreshNetworkData = true
|
||||||
|
syncContactTypeCounts(contacts)
|
||||||
|
const nextContactMap = contacts.reduce<Record<string, ContactInfo>>((map, contact) => {
|
||||||
|
map[contact.username] = contact
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
|
contactMap = nextContactMap
|
||||||
|
setSessionContactsUpdatedAt(Date.now())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const nextContactMap = contacts.reduce<Record<string, ContactInfo>>((map, contact) => {
|
|
||||||
map[contact.username] = contact
|
|
||||||
return map
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
const needsEnrichment = baseSessions
|
const needsEnrichment = baseSessions
|
||||||
.filter(session => !session.avatarUrl || !session.displayName || session.displayName === session.username)
|
.filter((session) => {
|
||||||
.map(session => session.username)
|
const contact = contactMap[session.username]
|
||||||
|
const avatarEntry = avatarEntries[session.username]
|
||||||
|
const displayName = contact?.displayName || session.displayName || session.username
|
||||||
|
const avatarUrl = contact?.avatarUrl || session.avatarUrl || avatarEntry?.avatarUrl
|
||||||
|
const shouldRecheckAvatar = !avatarEntry || (now - (avatarEntry.checkedAt || 0) >= EXPORT_AVATAR_RECHECK_INTERVAL_MS)
|
||||||
|
return !avatarUrl || displayName === session.username || shouldRecheckAvatar
|
||||||
|
})
|
||||||
|
.map((session) => session.username)
|
||||||
|
|
||||||
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
||||||
if (needsEnrichment.length > 0) {
|
if (needsEnrichment.length > 0) {
|
||||||
@@ -563,27 +643,87 @@ function ExportPage() {
|
|||||||
)
|
)
|
||||||
if (enrichResult?.success && enrichResult.contacts) {
|
if (enrichResult?.success && enrichResult.contacts) {
|
||||||
extraContactMap = enrichResult.contacts
|
extraContactMap = enrichResult.contacts
|
||||||
|
hasFreshNetworkData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistAt = Date.now()
|
||||||
|
for (const contact of Object.values(contactMap)) {
|
||||||
|
const avatarUrl = String(contact.avatarUrl || '').trim()
|
||||||
|
if (!avatarUrl) continue
|
||||||
|
const prev = avatarEntries[contact.username]
|
||||||
|
avatarEntries[contact.username] = {
|
||||||
|
avatarUrl,
|
||||||
|
updatedAt: prev?.avatarUrl === avatarUrl ? prev.updatedAt : persistAt,
|
||||||
|
checkedAt: prev?.checkedAt || persistAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const username of needsEnrichment) {
|
||||||
|
const extra = extraContactMap[username]
|
||||||
|
const prev = avatarEntries[username]
|
||||||
|
if (extra?.avatarUrl) {
|
||||||
|
avatarEntries[username] = {
|
||||||
|
avatarUrl: extra.avatarUrl,
|
||||||
|
updatedAt: !prev || prev.avatarUrl !== extra.avatarUrl ? persistAt : prev.updatedAt,
|
||||||
|
checkedAt: persistAt
|
||||||
|
}
|
||||||
|
} else if (prev) {
|
||||||
|
avatarEntries[username] = {
|
||||||
|
...prev,
|
||||||
|
checkedAt: persistAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extra) continue
|
||||||
|
const current = contactMap[username]
|
||||||
|
if (!current) continue
|
||||||
|
const nextDisplayName = extra.displayName || current.displayName
|
||||||
|
const nextAvatarUrl = extra.avatarUrl || current.avatarUrl
|
||||||
|
if (nextDisplayName !== current.displayName || nextAvatarUrl !== current.avatarUrl) {
|
||||||
|
contactMap[username] = {
|
||||||
|
...current,
|
||||||
|
displayName: nextDisplayName,
|
||||||
|
avatarUrl: nextAvatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
const nextSessions = baseSessions
|
const nextSessions = toSessionRowsWithContacts(rawSessions, contactMap)
|
||||||
.map((session) => {
|
.map((session) => {
|
||||||
const contact = nextContactMap[session.username]
|
|
||||||
const extra = extraContactMap[session.username]
|
const extra = extraContactMap[session.username]
|
||||||
const displayName = extra?.displayName || contact?.displayName || session.displayName || session.username
|
const displayName = extra?.displayName || session.displayName || session.username
|
||||||
const avatarUrl = extra?.avatarUrl || session.avatarUrl || contact?.avatarUrl
|
const avatarUrl = extra?.avatarUrl || session.avatarUrl
|
||||||
|
if (displayName === session.displayName && avatarUrl === session.avatarUrl) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
kind: toKindByContactType(session, contact),
|
|
||||||
wechatId: contact?.username || session.wechatId || session.username,
|
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
||||||
|
|
||||||
|
const contactsCachePayload = Object.values(contactMap).map((contact) => ({
|
||||||
|
username: contact.username,
|
||||||
|
displayName: contact.displayName || contact.username,
|
||||||
|
remark: contact.remark,
|
||||||
|
nickname: contact.nickname,
|
||||||
|
type: contact.type
|
||||||
|
}))
|
||||||
|
|
||||||
setSessions(nextSessions)
|
setSessions(nextSessions)
|
||||||
|
if (contactsCachePayload.length > 0) {
|
||||||
|
await configService.setContactsListCache(scopeKey, contactsCachePayload)
|
||||||
|
setSessionContactsUpdatedAt(persistAt)
|
||||||
|
}
|
||||||
|
await configService.setContactsAvatarCache(scopeKey, avatarEntries)
|
||||||
|
setSessionAvatarUpdatedAt(persistAt)
|
||||||
|
if (hasFreshNetworkData) {
|
||||||
|
setSessionDataSource('network')
|
||||||
|
}
|
||||||
} catch (enrichError) {
|
} catch (enrichError) {
|
||||||
console.error('导出页补充会话联系人信息失败:', enrichError)
|
console.error('导出页补充会话联系人信息失败:', enrichError)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -599,7 +739,7 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!isStale()) setIsLoading(false)
|
if (!isStale()) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [syncContactTypeCounts])
|
}, [ensureExportCacheScope, syncContactTypeCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRoute) return
|
||||||
@@ -1151,6 +1291,20 @@ function ExportPage() {
|
|||||||
return '公众号'
|
return '公众号'
|
||||||
}, [activeTab])
|
}, [activeTab])
|
||||||
|
|
||||||
|
const sessionContactsUpdatedAtLabel = useMemo(() => {
|
||||||
|
if (!sessionContactsUpdatedAt) return ''
|
||||||
|
return new Date(sessionContactsUpdatedAt).toLocaleString()
|
||||||
|
}, [sessionContactsUpdatedAt])
|
||||||
|
|
||||||
|
const sessionAvatarUpdatedAtLabel = useMemo(() => {
|
||||||
|
if (!sessionAvatarUpdatedAt) return ''
|
||||||
|
return new Date(sessionAvatarUpdatedAt).toLocaleString()
|
||||||
|
}, [sessionAvatarUpdatedAt])
|
||||||
|
|
||||||
|
const sessionAvatarCachedCount = useMemo(() => {
|
||||||
|
return sessions.reduce((count, session) => (session.avatarUrl ? count + 1 : count), 0)
|
||||||
|
}, [sessions])
|
||||||
|
|
||||||
const renderSessionName = (session: SessionRow) => {
|
const renderSessionName = (session: SessionRow) => {
|
||||||
return (
|
return (
|
||||||
<div className="session-cell">
|
<div className="session-cell">
|
||||||
@@ -1452,6 +1606,23 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="table-cache-meta">
|
||||||
|
{sessionContactsUpdatedAt && (
|
||||||
|
<span className="meta-item">
|
||||||
|
{sessionDataSource === 'cache' ? '缓存' : '最新'} · 更新于 {sessionContactsUpdatedAtLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{sessions.length > 0 && (
|
||||||
|
<span className="meta-item">
|
||||||
|
头像缓存 {sessionAvatarCachedCount}/{sessions.length}
|
||||||
|
{sessionAvatarUpdatedAtLabel ? ` · 更新于 ${sessionAvatarUpdatedAtLabel}` : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(isLoading || isSessionEnriching) && sessions.length > 0 && (
|
||||||
|
<span className="meta-item syncing">后台同步中...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!showInitialSkeleton && (isLoading || isSessionEnriching) && (
|
{!showInitialSkeleton && (isLoading || isSessionEnriching) && (
|
||||||
<div className="table-stage-hint">
|
<div className="table-stage-hint">
|
||||||
<Loader2 size={14} className="spin" />
|
<Loader2 size={14} className="spin" />
|
||||||
|
|||||||
Reference in New Issue
Block a user