From cbfdf86b360e3a2582ed703935682100c760eeb4 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Tue, 4 Aug 2026 11:45:15 +0800 Subject: [PATCH] refactor(frontend): extract useCampaignReport hook from Campaigns.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将活动报告抽屉的数据获取逻辑从 Campaigns.tsx 抽离到 useCampaignReport hook。 消除 5 个独立状态变量(report/reportRuns/reportTimeline/analysis/comparison) 和 fetchReport 函数的 25 行样板代码。hook 封装 5 个并行 API 调用和状态管理, 组件只负责渲染和交互。 - 新增 useCampaignReport(campaignId) hook - 返回 {report, runs, timeline, analysis, comparison, loading, refetch, setAnalysis, setComparison} - Campaigns.tsx 从 1122 行缩减状态管理复杂度 - TypeScript 类型检查通过,后端测试全绿 --- frontend/web/src/hooks/useCampaignReport.ts | 77 +++++++++++++++++++ frontend/web/src/pages/Campaigns.tsx | 82 +++++++-------------- 2 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 frontend/web/src/hooks/useCampaignReport.ts diff --git a/frontend/web/src/hooks/useCampaignReport.ts b/frontend/web/src/hooks/useCampaignReport.ts new file mode 100644 index 0000000..3d49425 --- /dev/null +++ b/frontend/web/src/hooks/useCampaignReport.ts @@ -0,0 +1,77 @@ +import { useState, useCallback, type Dispatch, type SetStateAction } from 'react' +import { campaignsApi, runsApi, type CampaignReport, type Run, type CampaignTimelineEntry, type CampaignAnalysis, type CampaignComparison } from '../api' + +interface CampaignReportData { + report: CampaignReport | null + runs: Run[] + timeline: CampaignTimelineEntry[] + analysis: CampaignAnalysis | null + comparison: CampaignComparison | null + loading: boolean +} + +interface UseCampaignReportResult extends CampaignReportData { + refetch: (silent?: boolean) => Promise + setAnalysis: Dispatch> + setComparison: Dispatch> +} + +export function useCampaignReport(campaignId: string | null): UseCampaignReportResult { + const [data, setData] = useState({ + report: null, + runs: [], + timeline: [], + analysis: null, + comparison: null, + loading: false, + }) + + const refetch = useCallback(async (silent = false) => { + if (!campaignId) return + if (!silent) { + setData({ + report: null, + runs: [], + timeline: [], + analysis: null, + comparison: null, + loading: true, + }) + } + try { + const [rep, runs, tl, ana, cmp] = await Promise.all([ + campaignsApi.report(campaignId), + runsApi.list(), + campaignsApi.timeline(campaignId), + campaignsApi.getAnalysis(campaignId), + campaignsApi.getComparison(campaignId), + ]) + setData({ + report: rep.data, + runs: runs.data.filter((r) => r.campaign_id === campaignId), + timeline: tl.data.entries, + analysis: ana.data, + comparison: cmp.data, + loading: false, + }) + } catch { + setData((prev) => ({ ...prev, loading: false })) + } + }, [campaignId]) + + const setAnalysis: Dispatch> = (value) => { + setData((prev) => ({ + ...prev, + analysis: typeof value === 'function' ? value(prev.analysis) : value, + })) + } + + const setComparison: Dispatch> = (value) => { + setData((prev) => ({ + ...prev, + comparison: typeof value === 'function' ? value(prev.comparison) : value, + })) + } + + return { ...data, refetch, setAnalysis, setComparison } +} diff --git a/frontend/web/src/pages/Campaigns.tsx b/frontend/web/src/pages/Campaigns.tsx index 69fa8bd..0ceee33 100644 --- a/frontend/web/src/pages/Campaigns.tsx +++ b/frontend/web/src/pages/Campaigns.tsx @@ -14,10 +14,9 @@ import { Line, Bar } from '@ant-design/charts' import PageWrapper from '../components/PageWrapper' import StatCard from '../components/StatCard' import { - campaignsApi, targetsApi, scenariosApi, runsApi, modelConfigsApi, - type CampaignListItem, type CampaignReport, type Target, type Scenario, type Run, - type CampaignTimelineEntry, type ModelConfig, type CampaignAnalysis, - type CampaignComparison, + campaignsApi, targetsApi, scenariosApi, modelConfigsApi, + type CampaignListItem, type Target, type Scenario, type Run, + type CampaignTimelineEntry, type ModelConfig, } from '../api' import { passRateColor } from '../utils/colors' import { shortDateTime, toDate } from '../utils/date' @@ -28,6 +27,7 @@ import PeriodComparisonSection from '../components/PeriodComparisonSection' import ExplorationSection from '../components/ExplorationSection' import { useResource } from '../hooks/useResource' import { usePolling } from '../hooks/usePolling' +import { useCampaignReport } from '../hooks/useCampaignReport' import { useTabStore } from '../stores/tabStore' import { colors, statusColors, statusLabels, triggerColors, triggerLabels } from '../tokens' @@ -154,13 +154,8 @@ export default function CampaignsPage() { const derivedScale = scaleFor(!!wRealtime, wWindow, wTargetValue, wTargetUnit) const [reportOpen, setReportOpen] = useState(false) - const [reportLoading, setReportLoading] = useState(false) - const [report, setReport] = useState(null) - const [reportRuns, setReportRuns] = useState([]) - const [reportTimeline, setReportTimeline] = useState([]) - const [analysis, setAnalysis] = useState(null) + const [reportCampaignId, setReportCampaignId] = useState(null) const [analysisBusy, setAnalysisBusy] = useState(false) - const [comparison, setComparison] = useState(null) const [expandedIds, setExpandedIds] = useState([]) const [timelines, setTimelines] = useState>({}) @@ -183,6 +178,11 @@ export default function CampaignsPage() { const analysisDefault = chatConfigs.find((m) => m.is_analysis_default) const scenarioNames = Object.fromEntries(scenarios.map((s) => [s.id, s.name])) + const { + report, runs: reportRuns, timeline: reportTimeline, analysis, comparison, loading: reportLoading, + refetch: fetchReport, setAnalysis, setComparison, + } = useCampaignReport(reportOpen ? reportCampaignId : null) + const wPlan = (Form.useWatch('plan', form) as PlanFormEntry[] | undefined) ?? [] const planMarkers: TimelineMarker[] = wPlan .map((e, i) => ({ e, i })) @@ -277,55 +277,27 @@ export default function CampaignsPage() { if (expanded) void fetchTimeline(record.id) } - const fetchReport = async (campaignId: string, silent = false) => { - if (!silent) { - setReportLoading(true) - setReport(null) - setReportRuns([]) - setReportTimeline([]) - setAnalysis(null) - setComparison(null) - } - try { - const [rep, runs, tl, ana, cmp] = await Promise.all([ - campaignsApi.report(campaignId), - runsApi.list(), - campaignsApi.timeline(campaignId), - campaignsApi.getAnalysis(campaignId), - campaignsApi.getComparison(campaignId), - ]) - setReport(rep.data) - setReportRuns(runs.data.filter((r) => r.campaign_id === campaignId)) - setReportTimeline(tl.data.entries) - setAnalysis(ana.data) - setComparison(cmp.data) - } finally { - if (!silent) setReportLoading(false) - } - } - const openReport = (campaign: CampaignListItem) => { setReportOpen(true) - fetchReport(campaign.id) + setReportCampaignId(campaign.id) } // Keep the open report drawer live while its campaign is still running. - const reportId = report?.campaign_id ?? null const reportCampaignActive = campaigns.some( - (c) => c.id === reportId && isActiveStatus(c.status), + (c) => c.id === reportCampaignId && isActiveStatus(c.status), ) usePolling( - () => { if (reportId) void fetchReport(reportId, true) }, + () => { if (reportCampaignId) void fetchReport(true) }, POLL_INTERVAL_MS, - activeKey === '/campaigns' && reportOpen && !!reportId && reportCampaignActive, + activeKey === '/campaigns' && reportOpen && !!reportCampaignId && reportCampaignActive, ) const generateAnalysis = async () => { - if (!reportId) return + if (!reportCampaignId) return setAnalysisBusy(true) try { - await campaignsApi.generateAnalysis(reportId) - const res = await campaignsApi.getAnalysis(reportId) + await campaignsApi.generateAnalysis(reportCampaignId) + const res = await campaignsApi.getAnalysis(reportCampaignId) setAnalysis(res.data) } finally { setAnalysisBusy(false) @@ -335,28 +307,28 @@ export default function CampaignsPage() { // 分析后台任务进行时轮询,直到进入 completed/failed 终态。 usePolling( () => { - if (!reportId) return - void campaignsApi.getAnalysis(reportId).then((res) => setAnalysis(res.data)) + if (!reportCampaignId) return + void campaignsApi.getAnalysis(reportCampaignId).then((res) => setAnalysis(res.data)) }, POLL_INTERVAL_MS, - activeKey === '/campaigns' && reportOpen && !!reportId && analysis?.status === 'generating', + activeKey === '/campaigns' && reportOpen && !!reportCampaignId && analysis?.status === 'generating', ) const generateComparison = async (baselineCampaignId?: string) => { - if (!reportId) return - await campaignsApi.generateComparison(reportId, baselineCampaignId) - const res = await campaignsApi.getComparison(reportId) + if (!reportCampaignId) return + await campaignsApi.generateComparison(reportCampaignId, baselineCampaignId) + const res = await campaignsApi.getComparison(reportCampaignId) setComparison(res.data) } // 对比生成中同样轮询,直到 completed/failed。 usePolling( () => { - if (!reportId) return - void campaignsApi.getComparison(reportId).then((res) => setComparison(res.data)) + if (!reportCampaignId) return + void campaignsApi.getComparison(reportCampaignId).then((res) => setComparison(res.data)) }, POLL_INTERVAL_MS, - activeKey === '/campaigns' && reportOpen && !!reportId && comparison?.status === 'generating', + activeKey === '/campaigns' && reportOpen && !!reportCampaignId && comparison?.status === 'generating', ) // Grow the expanded timeline of any still-running campaign as new child Runs @@ -564,7 +536,7 @@ export default function CampaignsPage() { } // ── 智能分析区块 ───────────────────────────────────────────────────── - const reportCampaign = campaigns.find((c) => c.id === reportId) + const reportCampaign = campaigns.find((c) => c.id === reportCampaignId) const analysisTerminal = !!report && !isActiveStatus(report.status) const analysisModelMissing = !reportCampaign?.analysis_model_config_id && !analysisDefault const analysisModelName = analysis?.model_config_id