mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(export): show and stop background page tasks
This commit is contained in:
@@ -4,6 +4,12 @@ import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw,
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import { useAnalyticsStore } from '../stores/analyticsStore'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import './AnalyticsPage.scss'
|
||||
import { Avatar } from '../components/Avatar'
|
||||
|
||||
@@ -48,6 +54,13 @@ function AnalyticsPage() {
|
||||
|
||||
const loadData = useCallback(async (forceRefresh = false) => {
|
||||
if (isLoaded && !forceRefresh) return
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'analytics',
|
||||
title: forceRefresh ? '刷新分析看板' : '加载分析看板',
|
||||
detail: '准备读取整体统计数据',
|
||||
progressText: '整体统计',
|
||||
cancelable: true
|
||||
})
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setProgress(0)
|
||||
@@ -60,27 +73,70 @@ function AnalyticsPage() {
|
||||
|
||||
try {
|
||||
setLoadingStatus('正在统计消息数据...')
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在统计消息数据',
|
||||
progressText: '整体统计'
|
||||
})
|
||||
const statsResult = await window.electronAPI.analytics.getOverallStatistics(forceRefresh)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前页面分析流程已结束'
|
||||
})
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStatistics(statsResult.data)
|
||||
} else {
|
||||
setError(statsResult.error || '加载统计数据失败')
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: statsResult.error || '加载统计数据失败'
|
||||
})
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
setLoadingStatus('正在分析联系人排名...')
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在分析联系人排名',
|
||||
progressText: '联系人排名'
|
||||
})
|
||||
const rankingsResult = await window.electronAPI.analytics.getContactRankings(20)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,联系人排名后续步骤未继续'
|
||||
})
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (rankingsResult.success && rankingsResult.data) {
|
||||
setRankings(rankingsResult.data)
|
||||
}
|
||||
setLoadingStatus('正在计算时间分布...')
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在计算时间分布',
|
||||
progressText: '时间分布'
|
||||
})
|
||||
const timeResult = await window.electronAPI.analytics.getTimeDistribution()
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,时间分布结果未继续写入'
|
||||
})
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (timeResult.success && timeResult.data) {
|
||||
setTimeDistribution(timeResult.data)
|
||||
}
|
||||
markLoaded()
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '分析看板数据加载完成',
|
||||
progressText: '已完成'
|
||||
})
|
||||
} catch (e) {
|
||||
setError(String(e))
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
if (removeListener) removeListener()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Calendar, Loader2, Sparkles, Users } from 'lucide-react'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import './AnnualReportPage.scss'
|
||||
|
||||
type YearOption = number | 'all'
|
||||
@@ -49,8 +55,17 @@ function AnnualReportPage() {
|
||||
useEffect(() => {
|
||||
let disposed = false
|
||||
let taskId = ''
|
||||
let uiTaskId = ''
|
||||
|
||||
const applyLoadPayload = (payload: YearsLoadPayload) => {
|
||||
if (uiTaskId) {
|
||||
updateBackgroundTask(uiTaskId, {
|
||||
detail: payload.statusText || '正在加载可用年份',
|
||||
progressText: payload.done
|
||||
? '已完成'
|
||||
: `${Array.isArray(payload.years) ? payload.years.length : 0} 个年份`
|
||||
})
|
||||
}
|
||||
if (payload.strategy) setLoadStrategy(payload.strategy)
|
||||
if (payload.phase) setLoadPhase(payload.phase)
|
||||
if (typeof payload.statusText === 'string' && payload.statusText) setLoadStatusText(payload.statusText)
|
||||
@@ -91,6 +106,14 @@ function AnnualReportPage() {
|
||||
setIsLoadingMoreYears(false)
|
||||
setHasYearsLoadFinished(true)
|
||||
setLoadPhase('done')
|
||||
if (uiTaskId) {
|
||||
finishBackgroundTask(uiTaskId, payload.canceled ? 'canceled' : 'completed', {
|
||||
detail: payload.canceled
|
||||
? '年度报告年份加载已停止'
|
||||
: `年度报告年份加载完成,共 ${years.length} 个年份`,
|
||||
progressText: payload.canceled ? '已停止' : `${years.length} 个年份`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setIsLoadingMoreYears(true)
|
||||
setHasYearsLoadFinished(false)
|
||||
@@ -105,6 +128,18 @@ function AnnualReportPage() {
|
||||
})
|
||||
|
||||
const startLoad = async () => {
|
||||
uiTaskId = registerBackgroundTask({
|
||||
sourcePage: 'annualReport',
|
||||
title: '年度报告年份加载',
|
||||
detail: '准备使用原生快速模式加载年份',
|
||||
progressText: '初始化',
|
||||
cancelable: true,
|
||||
onCancel: async () => {
|
||||
if (taskId) {
|
||||
await window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId)
|
||||
}
|
||||
}
|
||||
})
|
||||
setIsLoading(true)
|
||||
setIsLoadingMoreYears(true)
|
||||
setHasYearsLoadFinished(false)
|
||||
@@ -120,6 +155,9 @@ function AnnualReportPage() {
|
||||
try {
|
||||
const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad()
|
||||
if (!startResult.success || !startResult.taskId) {
|
||||
finishBackgroundTask(uiTaskId, 'failed', {
|
||||
detail: startResult.error || '加载年度数据失败'
|
||||
})
|
||||
setLoadError(startResult.error || '加载年度数据失败')
|
||||
setIsLoading(false)
|
||||
setIsLoadingMoreYears(false)
|
||||
@@ -131,6 +169,9 @@ function AnnualReportPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
finishBackgroundTask(uiTaskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
setLoadError(String(e))
|
||||
setIsLoading(false)
|
||||
setIsLoadingMoreYears(false)
|
||||
|
||||
@@ -2,6 +2,12 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { Loader2, Download, Image, Check, X, SlidersHorizontal } from 'lucide-react'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import './AnnualReportWindow.scss'
|
||||
|
||||
// SVG 背景图案 (用于导出)
|
||||
@@ -164,6 +170,13 @@ function AnnualReportWindow() {
|
||||
}, [])
|
||||
|
||||
const generateReport = async (year: number) => {
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'annualReport',
|
||||
title: '年度报告生成',
|
||||
detail: `正在生成 ${formatYearLabel(year)} 年度报告`,
|
||||
progressText: '初始化',
|
||||
cancelable: true
|
||||
})
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setLoadingProgress(0)
|
||||
@@ -171,25 +184,46 @@ function AnnualReportWindow() {
|
||||
const removeProgressListener = window.electronAPI.annualReport.onProgress?.((payload: { status: string; progress: number }) => {
|
||||
setLoadingProgress(payload.progress)
|
||||
setLoadingStage(payload.status)
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: payload.status || '正在生成年度报告',
|
||||
progressText: `${Math.max(0, Math.round(payload.progress || 0))}%`
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.annualReport.generateReport(year)
|
||||
removeProgressListener?.()
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前报告结果未继续写入页面'
|
||||
})
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
setLoadingProgress(100)
|
||||
setLoadingStage('完成')
|
||||
|
||||
if (result.success && result.data) {
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '年度报告生成完成',
|
||||
progressText: '100%'
|
||||
})
|
||||
setTimeout(() => {
|
||||
setReportData(result.data!)
|
||||
setIsLoading(false)
|
||||
}, 300)
|
||||
} else {
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: result.error || '生成年度报告失败'
|
||||
})
|
||||
setError(result.error || '生成报告失败')
|
||||
setIsLoading(false)
|
||||
}
|
||||
} catch (e) {
|
||||
removeProgressListener?.()
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
setError(String(e))
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ import JumpToDatePopover from '../components/JumpToDatePopover'
|
||||
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
|
||||
import { type ContactSnsTimelineTarget, isSingleContactSession } from '../components/Sns/contactSnsTimeline'
|
||||
import * as configService from '../services/config'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import {
|
||||
emitOpenSingleExport,
|
||||
onExportSessionStatus,
|
||||
@@ -1067,6 +1073,13 @@ function ChatPage(props: ChatPageProps) {
|
||||
const loadSessionDetail = useCallback(async (sessionId: string) => {
|
||||
const normalizedSessionId = String(sessionId || '').trim()
|
||||
if (!normalizedSessionId) return
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'chat',
|
||||
title: '聊天页会话详情统计',
|
||||
detail: `准备读取 ${sessionMapRef.current.get(normalizedSessionId)?.displayName || normalizedSessionId} 的详情`,
|
||||
progressText: '基础信息',
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
const requestSeq = ++detailRequestSeqRef.current
|
||||
const mappedSession = sessionMapRef.current.get(normalizedSessionId) || sessionsRef.current.find((s) => s.username === normalizedSessionId)
|
||||
@@ -1130,8 +1143,23 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在读取会话基础详情',
|
||||
progressText: '基础信息'
|
||||
})
|
||||
const result = await window.electronAPI.chat.getSessionDetailFast(normalizedSessionId)
|
||||
if (requestSeq !== detailRequestSeqRef.current) return
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前基础查询结束后未继续补充统计'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (requestSeq !== detailRequestSeqRef.current) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '会话已切换,旧详情任务已停止'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (result.success && result.detail) {
|
||||
setSessionDetail((prev) => ({
|
||||
wxid: normalizedSessionId,
|
||||
@@ -1170,6 +1198,10 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在读取补充信息与导出统计',
|
||||
progressText: '补充统计'
|
||||
})
|
||||
const [extraResultSettled, statsResultSettled] = await Promise.allSettled([
|
||||
window.electronAPI.chat.getSessionDetailExtra(normalizedSessionId),
|
||||
window.electronAPI.chat.getExportSessionStats(
|
||||
@@ -1178,7 +1210,18 @@ function ChatPage(props: ChatPageProps) {
|
||||
)
|
||||
])
|
||||
|
||||
if (requestSeq !== detailRequestSeqRef.current) return
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,补充统计结果未继续写入'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (requestSeq !== detailRequestSeqRef.current) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '会话已切换,旧补充统计任务已停止'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (extraResultSettled.status === 'fulfilled' && extraResultSettled.value.success) {
|
||||
const detail = extraResultSettled.value.detail
|
||||
@@ -1214,8 +1257,15 @@ function ChatPage(props: ChatPageProps) {
|
||||
})
|
||||
}
|
||||
}
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '聊天页会话详情统计完成',
|
||||
progressText: '已完成'
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载会话详情补充统计失败:', e)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
} finally {
|
||||
if (requestSeq === detailRequestSeqRef.current) {
|
||||
setIsLoadingDetailExtra(false)
|
||||
@@ -1228,13 +1278,31 @@ function ChatPage(props: ChatPageProps) {
|
||||
if (!normalizedSessionId || isLoadingRelationStats) return
|
||||
|
||||
const requestSeq = detailRequestSeqRef.current
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'chat',
|
||||
title: '聊天页关系统计补算',
|
||||
detail: `正在补算 ${normalizedSessionId} 的共同好友与关联数据`,
|
||||
progressText: '关系统计',
|
||||
cancelable: true
|
||||
})
|
||||
setIsLoadingRelationStats(true)
|
||||
try {
|
||||
const relationResult = await window.electronAPI.chat.getExportSessionStats(
|
||||
[normalizedSessionId],
|
||||
{ includeRelations: true, forceRefresh: true, preferAccurateSpecialTypes: true }
|
||||
)
|
||||
if (requestSeq !== detailRequestSeqRef.current) return
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前关系统计查询结束后未继续刷新'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (requestSeq !== detailRequestSeqRef.current) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '会话已切换,旧关系统计任务已停止'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const metric = relationResult.success && relationResult.data
|
||||
? relationResult.data[normalizedSessionId] as SessionExportMetric | undefined
|
||||
@@ -1254,11 +1322,26 @@ function ChatPage(props: ChatPageProps) {
|
||||
setIsRefreshingDetailStats(true)
|
||||
void (async () => {
|
||||
try {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在刷新关系统计结果',
|
||||
progressText: '关系统计刷新'
|
||||
})
|
||||
const freshResult = await window.electronAPI.chat.getExportSessionStats(
|
||||
[normalizedSessionId],
|
||||
{ includeRelations: true, forceRefresh: true, preferAccurateSpecialTypes: true }
|
||||
)
|
||||
if (requestSeq !== detailRequestSeqRef.current) return
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,刷新结果未继续写入'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (requestSeq !== detailRequestSeqRef.current) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '会话已切换,旧关系统计刷新任务已停止'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (freshResult.success && freshResult.data) {
|
||||
const freshMetric = freshResult.data[normalizedSessionId] as SessionExportMetric | undefined
|
||||
const freshMeta = freshResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
|
||||
@@ -1266,17 +1349,32 @@ function ChatPage(props: ChatPageProps) {
|
||||
applySessionDetailStats(normalizedSessionId, freshMetric, freshMeta, true)
|
||||
}
|
||||
}
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '聊天页关系统计补算完成',
|
||||
progressText: '已完成'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('刷新会话关系统计失败:', error)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(error)
|
||||
})
|
||||
} finally {
|
||||
if (requestSeq === detailRequestSeqRef.current) {
|
||||
setIsRefreshingDetailStats(false)
|
||||
}
|
||||
}
|
||||
})()
|
||||
} else {
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '聊天页关系统计补算完成',
|
||||
progressText: '已完成'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会话关系统计失败:', error)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(error)
|
||||
})
|
||||
} finally {
|
||||
if (requestSeq === detailRequestSeqRef.current) {
|
||||
setIsLoadingRelationStats(false)
|
||||
|
||||
@@ -254,6 +254,168 @@
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-summary {
|
||||
padding: 12px 12px 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.session-load-detail-summary-text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
strong {
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-note {
|
||||
margin: 8px 12px 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.session-load-detail-stop-btn,
|
||||
.session-load-detail-task-stop-btn {
|
||||
border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border-color));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--bg-secondary));
|
||||
color: color-mix(in srgb, var(--danger, #ef4444) 85%, var(--text-primary));
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-stop-btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.session-load-detail-task-list {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.session-load-detail-task-item {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary));
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
&.status-cancel_requested {
|
||||
border-color: color-mix(in srgb, var(--warning, #f59e0b) 36%, var(--border-color));
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
border-color: color-mix(in srgb, var(--danger, #ef4444) 36%, var(--border-color));
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-task-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-task-source {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-load-detail-task-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 80%, transparent);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.status-running {
|
||||
color: var(--primary);
|
||||
background: rgba(var(--primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
&.status-cancel_requested {
|
||||
color: #b45309;
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
color: #166534;
|
||||
background: rgba(34, 197, 94, 0.14);
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
color: #b91c1c;
|
||||
background: rgba(239, 68, 68, 0.14);
|
||||
}
|
||||
}
|
||||
|
||||
.session-load-detail-task-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.session-load-detail-task-stop-btn {
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-load-detail-empty {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.session-load-detail-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
||||
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
||||
import type { BackgroundTaskRecord } from '../types/backgroundTask'
|
||||
import * as configService from '../services/config'
|
||||
import {
|
||||
emitExportSessionStatus,
|
||||
@@ -37,6 +38,11 @@ import {
|
||||
onExportSessionStatusRequest,
|
||||
onOpenSingleExport
|
||||
} from '../services/exportBridge'
|
||||
import {
|
||||
requestCancelBackgroundTask,
|
||||
requestCancelBackgroundTasks,
|
||||
subscribeBackgroundTasks
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
||||
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
|
||||
@@ -176,6 +182,24 @@ const contentTypeLabels: Record<ContentType, string> = {
|
||||
emoji: '表情包'
|
||||
}
|
||||
|
||||
const backgroundTaskSourceLabels: Record<string, string> = {
|
||||
export: '导出页',
|
||||
chat: '聊天页',
|
||||
analytics: '分析页',
|
||||
sns: '朋友圈页',
|
||||
groupAnalytics: '群分析页',
|
||||
annualReport: '年度报告',
|
||||
other: '其他页面'
|
||||
}
|
||||
|
||||
const backgroundTaskStatusLabels: Record<BackgroundTaskRecord['status'], string> = {
|
||||
running: '运行中',
|
||||
cancel_requested: '停止中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
canceled: '已停止'
|
||||
}
|
||||
|
||||
const conversationTabLabels: Record<ConversationTab, string> = {
|
||||
private: '私聊',
|
||||
group: '群聊',
|
||||
@@ -1422,6 +1446,7 @@ function ExportPage() {
|
||||
const [sessionMutualFriendsMetrics, setSessionMutualFriendsMetrics] = useState<Record<string, SessionMutualFriendsMetric>>({})
|
||||
const [sessionMutualFriendsDialogTarget, setSessionMutualFriendsDialogTarget] = useState<SessionSnsTimelineTarget | null>(null)
|
||||
const [sessionMutualFriendsSearch, setSessionMutualFriendsSearch] = useState('')
|
||||
const [backgroundTasks, setBackgroundTasks] = useState<BackgroundTaskRecord[]>([])
|
||||
|
||||
const [exportFolder, setExportFolder] = useState('')
|
||||
const [writeLayout, setWriteLayout] = useState<configService.ExportWriteLayout>('B')
|
||||
@@ -1911,6 +1936,10 @@ function ExportPage() {
|
||||
return () => window.clearInterval(timer)
|
||||
}, [contactsList.length, isContactsListLoading, contactsLoadIssue])
|
||||
|
||||
useEffect(() => {
|
||||
return subscribeBackgroundTasks(setBackgroundTasks)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
tasksRef.current = tasks
|
||||
}, [tasks])
|
||||
@@ -5499,6 +5528,16 @@ function ExportPage() {
|
||||
alert('复制失败,请手动复制诊断信息')
|
||||
}
|
||||
}, [contactsDiagnosticsText])
|
||||
const handleCancelBackgroundTask = useCallback((taskId: string) => {
|
||||
requestCancelBackgroundTask(taskId)
|
||||
}, [])
|
||||
const handleCancelAllNonExportTasks = useCallback(() => {
|
||||
requestCancelBackgroundTasks(task => (
|
||||
task.sourcePage !== 'export' &&
|
||||
task.cancelable &&
|
||||
(task.status === 'running' || task.status === 'cancel_requested')
|
||||
))
|
||||
}, [])
|
||||
|
||||
const sessionContactsUpdatedAtLabel = useMemo(() => {
|
||||
if (!sessionContactsUpdatedAt) return ''
|
||||
@@ -5586,6 +5625,21 @@ function ExportPage() {
|
||||
const contactsBottomScrollbarInnerStyle = useMemo<CSSProperties>(() => ({
|
||||
width: `${Math.max(contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth)}px`
|
||||
}), [contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth])
|
||||
const nonExportBackgroundTasks = useMemo(() => (
|
||||
backgroundTasks.filter(task => task.sourcePage !== 'export')
|
||||
), [backgroundTasks])
|
||||
const runningNonExportTaskCount = useMemo(() => (
|
||||
nonExportBackgroundTasks.filter(task => task.status === 'running' || task.status === 'cancel_requested').length
|
||||
), [nonExportBackgroundTasks])
|
||||
const cancelableNonExportTaskCount = useMemo(() => (
|
||||
nonExportBackgroundTasks.filter(task => (
|
||||
task.cancelable &&
|
||||
(task.status === 'running' || task.status === 'cancel_requested')
|
||||
)).length
|
||||
), [nonExportBackgroundTasks])
|
||||
const nonExportBackgroundTasksUpdatedAt = useMemo(() => (
|
||||
nonExportBackgroundTasks.reduce((latest, task) => Math.max(latest, task.updatedAt || 0), 0)
|
||||
), [nonExportBackgroundTasks])
|
||||
const sessionLoadDetailUpdatedAt = useMemo(() => {
|
||||
let latest = 0
|
||||
for (const row of sessionLoadDetailRows) {
|
||||
@@ -6422,6 +6476,67 @@ function ExportPage() {
|
||||
</div>
|
||||
|
||||
<div className="session-load-detail-body">
|
||||
<section className="session-load-detail-block">
|
||||
<h5>其他页面后台任务</h5>
|
||||
<div className="session-load-detail-summary">
|
||||
<div className="session-load-detail-summary-text">
|
||||
<strong>{runningNonExportTaskCount}</strong>
|
||||
<span>个任务正在占用后台读取资源</span>
|
||||
{nonExportBackgroundTasksUpdatedAt > 0 && (
|
||||
<em>最近更新时间 {new Date(nonExportBackgroundTasksUpdatedAt).toLocaleTimeString('zh-CN', { hour12: false })}</em>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="session-load-detail-stop-btn"
|
||||
onClick={handleCancelAllNonExportTasks}
|
||||
disabled={cancelableNonExportTaskCount === 0}
|
||||
>
|
||||
中断其他页面加载
|
||||
</button>
|
||||
</div>
|
||||
<p className="session-load-detail-note">
|
||||
停止请求会阻止其他页面继续发起后续统计或补算;当前已经发出的单次查询,会在返回后结束。
|
||||
</p>
|
||||
{nonExportBackgroundTasks.length > 0 ? (
|
||||
<div className="session-load-detail-task-list">
|
||||
{nonExportBackgroundTasks.map((task) => (
|
||||
<div key={task.id} className={`session-load-detail-task-item status-${task.status}`}>
|
||||
<div className="session-load-detail-task-main">
|
||||
<div className="session-load-detail-task-title-row">
|
||||
<span className="session-load-detail-task-source">
|
||||
{backgroundTaskSourceLabels[task.sourcePage] || backgroundTaskSourceLabels.other}
|
||||
</span>
|
||||
<strong>{task.title}</strong>
|
||||
<span className={`session-load-detail-task-status status-${task.status}`}>
|
||||
{backgroundTaskStatusLabels[task.status]}
|
||||
</span>
|
||||
</div>
|
||||
<p>{task.detail || '暂无详细说明'}</p>
|
||||
<div className="session-load-detail-task-meta">
|
||||
<span>开始:{formatLoadDetailTime(task.startedAt)}</span>
|
||||
<span>更新:{formatLoadDetailTime(task.updatedAt)}</span>
|
||||
{task.progressText && <span>进度:{task.progressText}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="session-load-detail-task-stop-btn"
|
||||
onClick={() => handleCancelBackgroundTask(task.id)}
|
||||
disabled={!task.cancelable || (task.status !== 'running' && task.status !== 'cancel_requested')}
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="session-load-detail-empty">
|
||||
当前没有检测到其他页面后台任务
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="session-load-detail-block">
|
||||
<h5>总消息数</h5>
|
||||
<div className="session-load-detail-table">
|
||||
|
||||
@@ -5,6 +5,12 @@ import { Avatar } from '../components/Avatar'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import DateRangePicker from '../components/DateRangePicker'
|
||||
import * as configService from '../services/config'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import './GroupAnalyticsPage.scss'
|
||||
|
||||
interface GroupChatInfo {
|
||||
@@ -176,15 +182,39 @@ function GroupAnalyticsPage() {
|
||||
}, [])
|
||||
|
||||
const loadGroups = useCallback(async () => {
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'groupAnalytics',
|
||||
title: '群列表加载',
|
||||
detail: '正在读取群聊列表',
|
||||
progressText: '群聊列表',
|
||||
cancelable: true
|
||||
})
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupChats()
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,群聊列表结果未继续写入'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) {
|
||||
setGroups(result.data)
|
||||
setFilteredGroups(result.data)
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: `群聊列表加载完成,共 ${result.data.length} 个群`,
|
||||
progressText: `${result.data.length} 个群`
|
||||
})
|
||||
} else {
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: result.error || '加载群聊列表失败'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -314,6 +344,13 @@ function GroupAnalyticsPage() {
|
||||
|
||||
const loadFunctionData = async (func: AnalysisFunction) => {
|
||||
if (!selectedGroup) return
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'groupAnalytics',
|
||||
title: `群分析:${func}`,
|
||||
detail: `正在读取 ${selectedGroup.displayName || selectedGroup.username} 的分析数据`,
|
||||
progressText: func,
|
||||
cancelable: true
|
||||
})
|
||||
setFunctionLoading(true)
|
||||
|
||||
// 计算时间戳
|
||||
@@ -323,33 +360,96 @@ function GroupAnalyticsPage() {
|
||||
try {
|
||||
switch (func) {
|
||||
case 'members': {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在读取群成员列表',
|
||||
progressText: '成员列表'
|
||||
})
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupMembers(selectedGroup.username)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,群成员列表未继续写入' })
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) setMembers(result.data)
|
||||
finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', {
|
||||
detail: result.success ? `群成员列表加载完成,共 ${result.data?.length || 0} 人` : (result.error || '读取群成员列表失败'),
|
||||
progressText: result.success ? `${result.data?.length || 0} 人` : '失败'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'memberExport': {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在读取导出成员列表',
|
||||
progressText: '成员导出'
|
||||
})
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupMembers(selectedGroup.username)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,成员导出列表未继续写入' })
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) setMembers(result.data)
|
||||
finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', {
|
||||
detail: result.success ? `成员导出列表加载完成,共 ${result.data?.length || 0} 人` : (result.error || '读取成员导出列表失败'),
|
||||
progressText: result.success ? `${result.data?.length || 0} 人` : '失败'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'ranking': {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在计算群消息排行',
|
||||
progressText: '消息排行'
|
||||
})
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupMessageRanking(selectedGroup.username, 20, startTime, endTime)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,群消息排行未继续写入' })
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) setRankings(result.data)
|
||||
finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', {
|
||||
detail: result.success ? `群消息排行加载完成,共 ${result.data?.length || 0} 条` : (result.error || '读取群消息排行失败'),
|
||||
progressText: result.success ? `${result.data?.length || 0} 条` : '失败'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'activeHours': {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在计算群活跃时段',
|
||||
progressText: '活跃时段'
|
||||
})
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupActiveHours(selectedGroup.username, startTime, endTime)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,群活跃时段未继续写入' })
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) setActiveHours(result.data.hourlyDistribution)
|
||||
finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', {
|
||||
detail: result.success ? '群活跃时段加载完成' : (result.error || '读取群活跃时段失败'),
|
||||
progressText: result.success ? '24 小时分布' : '失败'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'mediaStats': {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在统计群消息类型',
|
||||
progressText: '消息类型'
|
||||
})
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupMediaStats(selectedGroup.username, startTime, endTime)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,群消息类型统计未继续写入' })
|
||||
return
|
||||
}
|
||||
if (result.success && result.data) setMediaStats(result.data)
|
||||
finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', {
|
||||
detail: result.success ? `群消息类型统计完成,共 ${result.data?.total || 0} 条` : (result.error || '读取群消息类型统计失败'),
|
||||
progressText: result.success ? `${result.data?.total || 0} 条` : '失败'
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(e)
|
||||
})
|
||||
} finally {
|
||||
setFunctionLoading(false)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import type { ContactSnsTimelineTarget } from '../components/Sns/contactSnsTimel
|
||||
import JumpToDatePopover from '../components/JumpToDatePopover'
|
||||
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
|
||||
import * as configService from '../services/config'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import {
|
||||
createExportDateRangeSelectionFromPreset,
|
||||
getExportDateRangeLabel,
|
||||
@@ -728,9 +734,23 @@ export default function SnsPage() {
|
||||
})
|
||||
if (pendingTargets.length === 0) return
|
||||
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'sns',
|
||||
title: '朋友圈联系人计数补算',
|
||||
detail: `正在补算 ${pendingTargets.length} 个联系人朋友圈条数`,
|
||||
progressText: `${preResolved}/${totalTargets}`,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
let normalizedCounts: Record<string, number> = {}
|
||||
try {
|
||||
const result = await window.electronAPI.sns.getUserPostCounts()
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前计数查询结束后不再继续分批写入'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (runToken !== contactsCountHydrationTokenRef.current) return
|
||||
if (result.success && result.counts) {
|
||||
normalizedCounts = Object.fromEntries(
|
||||
@@ -747,12 +767,28 @@ export default function SnsPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load contact post counts:', error)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let resolved = preResolved
|
||||
let cursor = 0
|
||||
const applyBatch = () => {
|
||||
if (runToken !== contactsCountHydrationTokenRef.current) return
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: `已停止后续加载,已完成 ${resolved}/${totalTargets}`
|
||||
})
|
||||
contactsCountBatchTimerRef.current = null
|
||||
setContactsCountProgress({
|
||||
resolved,
|
||||
total: totalTargets,
|
||||
running: false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const batch = pendingTargets.slice(cursor, cursor + CONTACT_COUNT_BATCH_SIZE)
|
||||
if (batch.length === 0) {
|
||||
@@ -762,6 +798,10 @@ export default function SnsPage() {
|
||||
running: false
|
||||
})
|
||||
contactsCountBatchTimerRef.current = null
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: '联系人朋友圈条数补算完成',
|
||||
progressText: `${totalTargets}/${totalTargets}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -789,6 +829,10 @@ export default function SnsPage() {
|
||||
total: totalTargets,
|
||||
running: resolved < totalTargets
|
||||
})
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: `已完成 ${resolved}/${totalTargets} 个联系人朋友圈条数补算`,
|
||||
progressText: `${resolved}/${totalTargets}`
|
||||
})
|
||||
|
||||
if (cursor < totalTargets) {
|
||||
contactsCountBatchTimerRef.current = window.setTimeout(applyBatch, CONTACT_COUNT_SORT_DEBOUNCE_MS)
|
||||
@@ -803,6 +847,13 @@ export default function SnsPage() {
|
||||
// Load Contacts(先按最近会话显示联系人,再异步统计朋友圈条数并增量排序)
|
||||
const loadContacts = useCallback(async () => {
|
||||
const requestToken = ++contactsLoadTokenRef.current
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'sns',
|
||||
title: '朋友圈联系人列表加载',
|
||||
detail: '准备读取联系人缓存与最近会话',
|
||||
progressText: '初始化',
|
||||
cancelable: true
|
||||
})
|
||||
stopContactsCountHydration(true)
|
||||
setContactsLoading(true)
|
||||
try {
|
||||
@@ -845,10 +896,20 @@ export default function SnsPage() {
|
||||
})
|
||||
}
|
||||
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在读取联系人与最近会话数据',
|
||||
progressText: '联系人快照'
|
||||
})
|
||||
const [contactsResult, sessionsResult] = await Promise.all([
|
||||
window.electronAPI.chat.getContacts(),
|
||||
window.electronAPI.chat.getSessions()
|
||||
])
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,当前联系人查询结束后未继续补齐'
|
||||
})
|
||||
return
|
||||
}
|
||||
const contactMap = new Map<string, Contact>()
|
||||
const sessionTimestampMap = new Map<string, number>()
|
||||
|
||||
@@ -904,7 +965,17 @@ export default function SnsPage() {
|
||||
|
||||
// 用 enrichSessionsContactInfo 统一补充头像和显示名
|
||||
if (allUsernames.length > 0) {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: '正在补齐联系人显示名与头像',
|
||||
progressText: '联系人补齐'
|
||||
})
|
||||
const enriched = await window.electronAPI.chat.enrichSessionsContactInfo(allUsernames)
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '已停止后续加载,联系人补齐未继续写入'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (enriched.success && enriched.contacts) {
|
||||
contactsList = contactsList.map((contact) => {
|
||||
const extra = enriched.contacts?.[contact.username]
|
||||
@@ -931,10 +1002,17 @@ export default function SnsPage() {
|
||||
})
|
||||
}
|
||||
}
|
||||
finishBackgroundTask(taskId, 'completed', {
|
||||
detail: `朋友圈联系人列表加载完成,共 ${contactsList.length} 人`,
|
||||
progressText: `${contactsList.length} 人`
|
||||
})
|
||||
} catch (error) {
|
||||
if (requestToken !== contactsLoadTokenRef.current) return
|
||||
console.error('Failed to load contacts:', error)
|
||||
stopContactsCountHydration(true)
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: String(error)
|
||||
})
|
||||
} finally {
|
||||
if (requestToken === contactsLoadTokenRef.current) {
|
||||
setContactsLoading(false)
|
||||
|
||||
Reference in New Issue
Block a user