mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(export): include sns count loading progress in load detail
This commit is contained in:
@@ -171,6 +171,8 @@ const SESSION_MEDIA_METRIC_BATCH_SIZE = 12
|
|||||||
const SESSION_MEDIA_METRIC_BACKGROUND_FEED_SIZE = 48
|
const SESSION_MEDIA_METRIC_BACKGROUND_FEED_SIZE = 48
|
||||||
const SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS = 120
|
const SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS = 120
|
||||||
const SESSION_MEDIA_METRIC_CACHE_FLUSH_DELAY_MS = 1200
|
const SESSION_MEDIA_METRIC_CACHE_FLUSH_DELAY_MS = 1200
|
||||||
|
const SNS_USER_POST_COUNT_BATCH_SIZE = 12
|
||||||
|
const SNS_USER_POST_COUNT_BATCH_INTERVAL_MS = 120
|
||||||
const contentTypeLabels: Record<ContentType, string> = {
|
const contentTypeLabels: Record<ContentType, string> = {
|
||||||
text: '聊天文本',
|
text: '聊天文本',
|
||||||
voice: '语音',
|
voice: '语音',
|
||||||
@@ -725,6 +727,7 @@ interface SessionLoadStageState {
|
|||||||
interface SessionLoadTraceState {
|
interface SessionLoadTraceState {
|
||||||
messageCount: SessionLoadStageState
|
messageCount: SessionLoadStageState
|
||||||
mediaMetrics: SessionLoadStageState
|
mediaMetrics: SessionLoadStageState
|
||||||
|
snsPostCounts: SessionLoadStageState
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionLoadStageSummary {
|
interface SessionLoadStageSummary {
|
||||||
@@ -959,7 +962,8 @@ const createDefaultSessionLoadStage = (): SessionLoadStageState => ({ status: 'p
|
|||||||
|
|
||||||
const createDefaultSessionLoadTrace = (): SessionLoadTraceState => ({
|
const createDefaultSessionLoadTrace = (): SessionLoadTraceState => ({
|
||||||
messageCount: createDefaultSessionLoadStage(),
|
messageCount: createDefaultSessionLoadStage(),
|
||||||
mediaMetrics: createDefaultSessionLoadStage()
|
mediaMetrics: createDefaultSessionLoadStage(),
|
||||||
|
snsPostCounts: createDefaultSessionLoadStage()
|
||||||
})
|
})
|
||||||
|
|
||||||
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
||||||
@@ -1419,6 +1423,8 @@ function ExportPage() {
|
|||||||
const sessionSnsTimelinePostsRef = useRef<SnsPost[]>([])
|
const sessionSnsTimelinePostsRef = useRef<SnsPost[]>([])
|
||||||
const sessionSnsTimelineLoadingRef = useRef(false)
|
const sessionSnsTimelineLoadingRef = useRef(false)
|
||||||
const sessionSnsTimelineRequestTokenRef = useRef(0)
|
const sessionSnsTimelineRequestTokenRef = useRef(0)
|
||||||
|
const snsUserPostCountsHydrationTokenRef = useRef(0)
|
||||||
|
const snsUserPostCountsBatchTimerRef = useRef<number | null>(null)
|
||||||
const sessionPreciseRefreshAtRef = useRef<Record<string, number>>({})
|
const sessionPreciseRefreshAtRef = useRef<Record<string, number>>({})
|
||||||
const sessionLoadProgressSnapshotRef = useRef<Record<string, { loaded: number; total: number }>>({})
|
const sessionLoadProgressSnapshotRef = useRef<Record<string, { loaded: number; total: number }>>({})
|
||||||
const sessionMediaMetricQueueRef = useRef<string[]>([])
|
const sessionMediaMetricQueueRef = useRef<string[]>([])
|
||||||
@@ -1955,28 +1961,86 @@ function ExportPage() {
|
|||||||
if (snsUserPostCountsStatus === 'loading') return
|
if (snsUserPostCountsStatus === 'loading') return
|
||||||
if (!options?.force && snsUserPostCountsStatus === 'ready') return
|
if (!options?.force && snsUserPostCountsStatus === 'ready') return
|
||||||
|
|
||||||
setSnsUserPostCountsStatus('loading')
|
const targetSessionIds = sessionsRef.current
|
||||||
try {
|
.filter((session) => session.hasSession && isSingleContactSession(session.username))
|
||||||
const result = await window.electronAPI.sns.getUserPostCounts()
|
.map((session) => session.username)
|
||||||
if (result.success && result.counts) {
|
|
||||||
const normalized: Record<string, number> = {}
|
snsUserPostCountsHydrationTokenRef.current += 1
|
||||||
for (const [rawUsername, rawCount] of Object.entries(result.counts)) {
|
const runToken = snsUserPostCountsHydrationTokenRef.current
|
||||||
const username = String(rawUsername || '').trim()
|
if (snsUserPostCountsBatchTimerRef.current) {
|
||||||
if (!username) continue
|
window.clearTimeout(snsUserPostCountsBatchTimerRef.current)
|
||||||
const value = Number(rawCount)
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
normalized[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
|
|
||||||
}
|
}
|
||||||
setSnsUserPostCounts(normalized)
|
|
||||||
|
if (targetSessionIds.length === 0) {
|
||||||
setSnsUserPostCountsStatus('ready')
|
setSnsUserPostCountsStatus('ready')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'pending', { force: true })
|
||||||
|
patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'loading')
|
||||||
|
setSnsUserPostCountsStatus('loading')
|
||||||
|
|
||||||
|
let normalizedCounts: Record<string, number> = {}
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.sns.getUserPostCounts()
|
||||||
|
if (runToken !== snsUserPostCountsHydrationTokenRef.current) return
|
||||||
|
|
||||||
|
if (!result.success || !result.counts) {
|
||||||
|
patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'failed', {
|
||||||
|
error: result.error || '朋友圈条数统计失败'
|
||||||
|
})
|
||||||
setSnsUserPostCountsStatus('error')
|
setSnsUserPostCountsStatus('error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [rawUsername, rawCount] of Object.entries(result.counts)) {
|
||||||
|
const username = String(rawUsername || '').trim()
|
||||||
|
if (!username) continue
|
||||||
|
const value = Number(rawCount)
|
||||||
|
normalizedCounts[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载朋友圈用户条数失败:', error)
|
console.error('加载朋友圈用户条数失败:', error)
|
||||||
|
if (runToken !== snsUserPostCountsHydrationTokenRef.current) return
|
||||||
|
patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'failed', {
|
||||||
|
error: String(error)
|
||||||
|
})
|
||||||
setSnsUserPostCountsStatus('error')
|
setSnsUserPostCountsStatus('error')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, [snsUserPostCountsStatus])
|
|
||||||
|
let cursor = 0
|
||||||
|
const applyBatch = () => {
|
||||||
|
if (runToken !== snsUserPostCountsHydrationTokenRef.current) return
|
||||||
|
|
||||||
|
const batchSessionIds = targetSessionIds.slice(cursor, cursor + SNS_USER_POST_COUNT_BATCH_SIZE)
|
||||||
|
if (batchSessionIds.length === 0) {
|
||||||
|
setSnsUserPostCountsStatus('ready')
|
||||||
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchCounts: Record<string, number> = {}
|
||||||
|
for (const sessionId of batchSessionIds) {
|
||||||
|
const nextCount = normalizedCounts[sessionId]
|
||||||
|
batchCounts[sessionId] = Number.isFinite(nextCount) ? Math.max(0, Math.floor(nextCount)) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnsUserPostCounts(prev => ({ ...prev, ...batchCounts }))
|
||||||
|
patchSessionLoadTraceStage(batchSessionIds, 'snsPostCounts', 'done')
|
||||||
|
|
||||||
|
cursor += batchSessionIds.length
|
||||||
|
if (cursor < targetSessionIds.length) {
|
||||||
|
snsUserPostCountsBatchTimerRef.current = window.setTimeout(applyBatch, SNS_USER_POST_COUNT_BATCH_INTERVAL_MS)
|
||||||
|
} else {
|
||||||
|
setSnsUserPostCountsStatus('ready')
|
||||||
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBatch()
|
||||||
|
}, [patchSessionLoadTraceStage, snsUserPostCountsStatus])
|
||||||
|
|
||||||
const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => {
|
const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => {
|
||||||
const reset = Boolean(options?.reset)
|
const reset = Boolean(options?.reset)
|
||||||
@@ -2080,7 +2144,7 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loadSessionSnsTimelinePosts(target, { reset: true })
|
void loadSessionSnsTimelinePosts(target, { reset: true })
|
||||||
void loadSnsUserPostCounts({ force: true })
|
void loadSnsUserPostCounts()
|
||||||
}, [
|
}, [
|
||||||
loadSessionSnsTimelinePosts,
|
loadSessionSnsTimelinePosts,
|
||||||
loadSnsUserPostCounts,
|
loadSnsUserPostCounts,
|
||||||
@@ -2568,6 +2632,13 @@ function ExportPage() {
|
|||||||
setSessionLoadTraceMap({})
|
setSessionLoadTraceMap({})
|
||||||
setSessionLoadProgressPulseMap({})
|
setSessionLoadProgressPulseMap({})
|
||||||
sessionLoadProgressSnapshotRef.current = {}
|
sessionLoadProgressSnapshotRef.current = {}
|
||||||
|
snsUserPostCountsHydrationTokenRef.current += 1
|
||||||
|
if (snsUserPostCountsBatchTimerRef.current) {
|
||||||
|
window.clearTimeout(snsUserPostCountsBatchTimerRef.current)
|
||||||
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
|
}
|
||||||
|
setSnsUserPostCounts({})
|
||||||
|
setSnsUserPostCountsStatus('idle')
|
||||||
setIsLoadingSessionCounts(false)
|
setIsLoadingSessionCounts(false)
|
||||||
setIsSessionCountStageReady(false)
|
setIsSessionCountStageReady(false)
|
||||||
|
|
||||||
@@ -2895,8 +2966,14 @@ function ExportPage() {
|
|||||||
// 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。
|
// 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。
|
||||||
sessionLoadTokenRef.current = Date.now()
|
sessionLoadTokenRef.current = Date.now()
|
||||||
sessionCountRequestIdRef.current += 1
|
sessionCountRequestIdRef.current += 1
|
||||||
|
snsUserPostCountsHydrationTokenRef.current += 1
|
||||||
|
if (snsUserPostCountsBatchTimerRef.current) {
|
||||||
|
window.clearTimeout(snsUserPostCountsBatchTimerRef.current)
|
||||||
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
|
}
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
setIsLoadingSessionCounts(false)
|
setIsLoadingSessionCounts(false)
|
||||||
|
setSnsUserPostCountsStatus('idle')
|
||||||
}, [isExportRoute])
|
}, [isExportRoute])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -4087,18 +4164,31 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
}, [getLoadDetailStatusLabel, sessionLoadTraceMap])
|
}, [getLoadDetailStatusLabel, sessionLoadTraceMap])
|
||||||
|
|
||||||
|
const createNotApplicableLoadSummary = useCallback((): SessionLoadStageSummary => {
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
loaded: 0,
|
||||||
|
statusLabel: '不适用'
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const sessionLoadDetailRows = useMemo(() => {
|
const sessionLoadDetailRows = useMemo(() => {
|
||||||
const tabOrder: ConversationTab[] = ['private', 'group', 'official', 'former_friend']
|
const tabOrder: ConversationTab[] = ['private', 'group', 'official', 'former_friend']
|
||||||
return tabOrder.map((tab) => {
|
return tabOrder.map((tab) => {
|
||||||
const sessionIds = loadDetailTargetsByTab[tab] || []
|
const sessionIds = loadDetailTargetsByTab[tab] || []
|
||||||
|
const snsSessionIds = sessionIds.filter((sessionId) => isSingleContactSession(sessionId))
|
||||||
|
const snsPostCounts = tab === 'private' || tab === 'former_friend'
|
||||||
|
? summarizeLoadTraceForTab(snsSessionIds, 'snsPostCounts')
|
||||||
|
: createNotApplicableLoadSummary()
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
label: conversationTabLabels[tab],
|
label: conversationTabLabels[tab],
|
||||||
messageCount: summarizeLoadTraceForTab(sessionIds, 'messageCount'),
|
messageCount: summarizeLoadTraceForTab(sessionIds, 'messageCount'),
|
||||||
mediaMetrics: summarizeLoadTraceForTab(sessionIds, 'mediaMetrics')
|
mediaMetrics: summarizeLoadTraceForTab(sessionIds, 'mediaMetrics'),
|
||||||
|
snsPostCounts
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [loadDetailTargetsByTab, summarizeLoadTraceForTab])
|
}, [createNotApplicableLoadSummary, loadDetailTargetsByTab, summarizeLoadTraceForTab])
|
||||||
|
|
||||||
const formatLoadDetailPulseTime = useCallback((value?: number): string => {
|
const formatLoadDetailPulseTime = useCallback((value?: number): string => {
|
||||||
if (!value || !Number.isFinite(value)) return '--'
|
if (!value || !Number.isFinite(value)) return '--'
|
||||||
@@ -4115,7 +4205,7 @@ function ExportPage() {
|
|||||||
const nextSnapshot: Record<string, { loaded: number; total: number }> = {}
|
const nextSnapshot: Record<string, { loaded: number; total: number }> = {}
|
||||||
const resetKeys: string[] = []
|
const resetKeys: string[] = []
|
||||||
const updates: Array<{ key: string; at: number; delta: number }> = []
|
const updates: Array<{ key: string; at: number; delta: number }> = []
|
||||||
const stageKeys: Array<keyof SessionLoadTraceState> = ['messageCount', 'mediaMetrics']
|
const stageKeys: Array<keyof SessionLoadTraceState> = ['messageCount', 'mediaMetrics', 'snsPostCounts']
|
||||||
|
|
||||||
for (const row of sessionLoadDetailRows) {
|
for (const row of sessionLoadDetailRows) {
|
||||||
for (const stageKey of stageKeys) {
|
for (const stageKey of stageKeys) {
|
||||||
@@ -4255,6 +4345,11 @@ function ExportPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
snsUserPostCountsHydrationTokenRef.current += 1
|
||||||
|
if (snsUserPostCountsBatchTimerRef.current) {
|
||||||
|
window.clearTimeout(snsUserPostCountsBatchTimerRef.current)
|
||||||
|
snsUserPostCountsBatchTimerRef.current = null
|
||||||
|
}
|
||||||
if (sessionMediaMetricBackgroundFeedTimerRef.current) {
|
if (sessionMediaMetricBackgroundFeedTimerRef.current) {
|
||||||
window.clearTimeout(sessionMediaMetricBackgroundFeedTimerRef.current)
|
window.clearTimeout(sessionMediaMetricBackgroundFeedTimerRef.current)
|
||||||
sessionMediaMetricBackgroundFeedTimerRef.current = null
|
sessionMediaMetricBackgroundFeedTimerRef.current = null
|
||||||
@@ -4607,6 +4702,15 @@ function ExportPage() {
|
|||||||
snsUserPostCountsStatus
|
snsUserPostCountsStatus
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isExportRoute || !isSessionCountStageReady) return
|
||||||
|
if (snsUserPostCountsStatus !== 'idle') return
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
void loadSnsUserPostCounts()
|
||||||
|
}, 260)
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}, [isExportRoute, isSessionCountStageReady, loadSnsUserPostCounts, snsUserPostCountsStatus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sessionSnsTimelineTarget) return
|
if (!sessionSnsTimelineTarget) return
|
||||||
if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') {
|
if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') {
|
||||||
@@ -4654,7 +4758,7 @@ function ExportPage() {
|
|||||||
detailStatsPriorityRef.current = true
|
detailStatsPriorityRef.current = true
|
||||||
setShowSessionDetailPanel(true)
|
setShowSessionDetailPanel(true)
|
||||||
if (isSingleContactSession(sessionId)) {
|
if (isSingleContactSession(sessionId)) {
|
||||||
void loadSnsUserPostCounts({ force: true })
|
void loadSnsUserPostCounts()
|
||||||
}
|
}
|
||||||
void loadSessionDetail(sessionId)
|
void loadSessionDetail(sessionId)
|
||||||
}, [loadSessionDetail, loadSnsUserPostCounts])
|
}, [loadSessionDetail, loadSnsUserPostCounts])
|
||||||
@@ -4672,6 +4776,9 @@ function ExportPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSessionLoadDetailModal) return
|
if (!showSessionLoadDetailModal) return
|
||||||
|
if (snsUserPostCountsStatus === 'idle') {
|
||||||
|
void loadSnsUserPostCounts()
|
||||||
|
}
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
setShowSessionLoadDetailModal(false)
|
setShowSessionLoadDetailModal(false)
|
||||||
@@ -4679,7 +4786,7 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [showSessionLoadDetailModal])
|
}, [loadSnsUserPostCounts, showSessionLoadDetailModal, snsUserPostCountsStatus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sessionSnsTimelineTarget) return
|
if (!sessionSnsTimelineTarget) return
|
||||||
@@ -4811,7 +4918,8 @@ function ExportPage() {
|
|||||||
for (const row of sessionLoadDetailRows) {
|
for (const row of sessionLoadDetailRows) {
|
||||||
const candidateTimes = [
|
const candidateTimes = [
|
||||||
row.messageCount.finishedAt || row.messageCount.startedAt || 0,
|
row.messageCount.finishedAt || row.messageCount.startedAt || 0,
|
||||||
row.mediaMetrics.finishedAt || row.mediaMetrics.startedAt || 0
|
row.mediaMetrics.finishedAt || row.mediaMetrics.startedAt || 0,
|
||||||
|
row.snsPostCounts.finishedAt || row.snsPostCounts.startedAt || 0
|
||||||
]
|
]
|
||||||
for (const candidate of candidateTimes) {
|
for (const candidate of candidateTimes) {
|
||||||
if (candidate > latest) {
|
if (candidate > latest) {
|
||||||
@@ -5432,6 +5540,40 @@ function ExportPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="session-load-detail-block">
|
||||||
|
<h5>朋友圈条数统计</h5>
|
||||||
|
<div className="session-load-detail-table">
|
||||||
|
<div className="session-load-detail-row header">
|
||||||
|
<span>会话类型</span>
|
||||||
|
<span>加载状态</span>
|
||||||
|
<span>开始时间</span>
|
||||||
|
<span>完成时间</span>
|
||||||
|
</div>
|
||||||
|
{sessionLoadDetailRows.map((row) => {
|
||||||
|
const pulse = sessionLoadProgressPulseMap[`snsPostCounts:${row.tab}`]
|
||||||
|
const isLoading = row.snsPostCounts.statusLabel.startsWith('加载中')
|
||||||
|
return (
|
||||||
|
<div className="session-load-detail-row" key={`sns-count-${row.tab}`}>
|
||||||
|
<span>{row.label}</span>
|
||||||
|
<span className="session-load-detail-status-cell">
|
||||||
|
<span>{row.snsPostCounts.statusLabel}</span>
|
||||||
|
{isLoading && (
|
||||||
|
<Loader2 size={12} className="spin session-load-detail-status-icon" aria-label="加载中" />
|
||||||
|
)}
|
||||||
|
{isLoading && pulse && pulse.delta > 0 && (
|
||||||
|
<span className="session-load-detail-progress-pulse">
|
||||||
|
{formatLoadDetailPulseTime(pulse.at)} +{pulse.delta}条
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>{formatLoadDetailTime(row.snsPostCounts.startedAt)}</span>
|
||||||
|
<span>{formatLoadDetailTime(row.snsPostCounts.finishedAt)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user