refactor(frontend): extract useCampaignReport hook from Campaigns.tsx
将活动报告抽屉的数据获取逻辑从 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 类型检查通过,后端测试全绿
This commit is contained in:
parent
c24998c762
commit
cbfdf86b36
77
frontend/web/src/hooks/useCampaignReport.ts
Normal file
77
frontend/web/src/hooks/useCampaignReport.ts
Normal file
@ -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<void>
|
||||||
|
setAnalysis: Dispatch<SetStateAction<CampaignAnalysis | null>>
|
||||||
|
setComparison: Dispatch<SetStateAction<CampaignComparison | null>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCampaignReport(campaignId: string | null): UseCampaignReportResult {
|
||||||
|
const [data, setData] = useState<CampaignReportData>({
|
||||||
|
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<SetStateAction<CampaignAnalysis | null>> = (value) => {
|
||||||
|
setData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
analysis: typeof value === 'function' ? value(prev.analysis) : value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const setComparison: Dispatch<SetStateAction<CampaignComparison | null>> = (value) => {
|
||||||
|
setData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
comparison: typeof value === 'function' ? value(prev.comparison) : value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...data, refetch, setAnalysis, setComparison }
|
||||||
|
}
|
||||||
@ -14,10 +14,9 @@ import { Line, Bar } from '@ant-design/charts'
|
|||||||
import PageWrapper from '../components/PageWrapper'
|
import PageWrapper from '../components/PageWrapper'
|
||||||
import StatCard from '../components/StatCard'
|
import StatCard from '../components/StatCard'
|
||||||
import {
|
import {
|
||||||
campaignsApi, targetsApi, scenariosApi, runsApi, modelConfigsApi,
|
campaignsApi, targetsApi, scenariosApi, modelConfigsApi,
|
||||||
type CampaignListItem, type CampaignReport, type Target, type Scenario, type Run,
|
type CampaignListItem, type Target, type Scenario, type Run,
|
||||||
type CampaignTimelineEntry, type ModelConfig, type CampaignAnalysis,
|
type CampaignTimelineEntry, type ModelConfig,
|
||||||
type CampaignComparison,
|
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { passRateColor } from '../utils/colors'
|
import { passRateColor } from '../utils/colors'
|
||||||
import { shortDateTime, toDate } from '../utils/date'
|
import { shortDateTime, toDate } from '../utils/date'
|
||||||
@ -28,6 +27,7 @@ import PeriodComparisonSection from '../components/PeriodComparisonSection'
|
|||||||
import ExplorationSection from '../components/ExplorationSection'
|
import ExplorationSection from '../components/ExplorationSection'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
import { usePolling } from '../hooks/usePolling'
|
import { usePolling } from '../hooks/usePolling'
|
||||||
|
import { useCampaignReport } from '../hooks/useCampaignReport'
|
||||||
import { useTabStore } from '../stores/tabStore'
|
import { useTabStore } from '../stores/tabStore'
|
||||||
import { colors, statusColors, statusLabels, triggerColors, triggerLabels } from '../tokens'
|
import { colors, statusColors, statusLabels, triggerColors, triggerLabels } from '../tokens'
|
||||||
|
|
||||||
@ -154,13 +154,8 @@ export default function CampaignsPage() {
|
|||||||
const derivedScale = scaleFor(!!wRealtime, wWindow, wTargetValue, wTargetUnit)
|
const derivedScale = scaleFor(!!wRealtime, wWindow, wTargetValue, wTargetUnit)
|
||||||
|
|
||||||
const [reportOpen, setReportOpen] = useState(false)
|
const [reportOpen, setReportOpen] = useState(false)
|
||||||
const [reportLoading, setReportLoading] = useState(false)
|
const [reportCampaignId, setReportCampaignId] = useState<string | null>(null)
|
||||||
const [report, setReport] = useState<CampaignReport | null>(null)
|
|
||||||
const [reportRuns, setReportRuns] = useState<Run[]>([])
|
|
||||||
const [reportTimeline, setReportTimeline] = useState<CampaignTimelineEntry[]>([])
|
|
||||||
const [analysis, setAnalysis] = useState<CampaignAnalysis | null>(null)
|
|
||||||
const [analysisBusy, setAnalysisBusy] = useState(false)
|
const [analysisBusy, setAnalysisBusy] = useState(false)
|
||||||
const [comparison, setComparison] = useState<CampaignComparison | null>(null)
|
|
||||||
|
|
||||||
const [expandedIds, setExpandedIds] = useState<string[]>([])
|
const [expandedIds, setExpandedIds] = useState<string[]>([])
|
||||||
const [timelines, setTimelines] = useState<Record<string, CampaignTimelineEntry[]>>({})
|
const [timelines, setTimelines] = useState<Record<string, CampaignTimelineEntry[]>>({})
|
||||||
@ -183,6 +178,11 @@ export default function CampaignsPage() {
|
|||||||
const analysisDefault = chatConfigs.find((m) => m.is_analysis_default)
|
const analysisDefault = chatConfigs.find((m) => m.is_analysis_default)
|
||||||
const scenarioNames = Object.fromEntries(scenarios.map((s) => [s.id, s.name]))
|
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 wPlan = (Form.useWatch('plan', form) as PlanFormEntry[] | undefined) ?? []
|
||||||
const planMarkers: TimelineMarker[] = wPlan
|
const planMarkers: TimelineMarker[] = wPlan
|
||||||
.map((e, i) => ({ e, i }))
|
.map((e, i) => ({ e, i }))
|
||||||
@ -277,55 +277,27 @@ export default function CampaignsPage() {
|
|||||||
if (expanded) void fetchTimeline(record.id)
|
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) => {
|
const openReport = (campaign: CampaignListItem) => {
|
||||||
setReportOpen(true)
|
setReportOpen(true)
|
||||||
fetchReport(campaign.id)
|
setReportCampaignId(campaign.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the open report drawer live while its campaign is still running.
|
// Keep the open report drawer live while its campaign is still running.
|
||||||
const reportId = report?.campaign_id ?? null
|
|
||||||
const reportCampaignActive = campaigns.some(
|
const reportCampaignActive = campaigns.some(
|
||||||
(c) => c.id === reportId && isActiveStatus(c.status),
|
(c) => c.id === reportCampaignId && isActiveStatus(c.status),
|
||||||
)
|
)
|
||||||
usePolling(
|
usePolling(
|
||||||
() => { if (reportId) void fetchReport(reportId, true) },
|
() => { if (reportCampaignId) void fetchReport(true) },
|
||||||
POLL_INTERVAL_MS,
|
POLL_INTERVAL_MS,
|
||||||
activeKey === '/campaigns' && reportOpen && !!reportId && reportCampaignActive,
|
activeKey === '/campaigns' && reportOpen && !!reportCampaignId && reportCampaignActive,
|
||||||
)
|
)
|
||||||
|
|
||||||
const generateAnalysis = async () => {
|
const generateAnalysis = async () => {
|
||||||
if (!reportId) return
|
if (!reportCampaignId) return
|
||||||
setAnalysisBusy(true)
|
setAnalysisBusy(true)
|
||||||
try {
|
try {
|
||||||
await campaignsApi.generateAnalysis(reportId)
|
await campaignsApi.generateAnalysis(reportCampaignId)
|
||||||
const res = await campaignsApi.getAnalysis(reportId)
|
const res = await campaignsApi.getAnalysis(reportCampaignId)
|
||||||
setAnalysis(res.data)
|
setAnalysis(res.data)
|
||||||
} finally {
|
} finally {
|
||||||
setAnalysisBusy(false)
|
setAnalysisBusy(false)
|
||||||
@ -335,28 +307,28 @@ export default function CampaignsPage() {
|
|||||||
// 分析后台任务进行时轮询,直到进入 completed/failed 终态。
|
// 分析后台任务进行时轮询,直到进入 completed/failed 终态。
|
||||||
usePolling(
|
usePolling(
|
||||||
() => {
|
() => {
|
||||||
if (!reportId) return
|
if (!reportCampaignId) return
|
||||||
void campaignsApi.getAnalysis(reportId).then((res) => setAnalysis(res.data))
|
void campaignsApi.getAnalysis(reportCampaignId).then((res) => setAnalysis(res.data))
|
||||||
},
|
},
|
||||||
POLL_INTERVAL_MS,
|
POLL_INTERVAL_MS,
|
||||||
activeKey === '/campaigns' && reportOpen && !!reportId && analysis?.status === 'generating',
|
activeKey === '/campaigns' && reportOpen && !!reportCampaignId && analysis?.status === 'generating',
|
||||||
)
|
)
|
||||||
|
|
||||||
const generateComparison = async (baselineCampaignId?: string) => {
|
const generateComparison = async (baselineCampaignId?: string) => {
|
||||||
if (!reportId) return
|
if (!reportCampaignId) return
|
||||||
await campaignsApi.generateComparison(reportId, baselineCampaignId)
|
await campaignsApi.generateComparison(reportCampaignId, baselineCampaignId)
|
||||||
const res = await campaignsApi.getComparison(reportId)
|
const res = await campaignsApi.getComparison(reportCampaignId)
|
||||||
setComparison(res.data)
|
setComparison(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对比生成中同样轮询,直到 completed/failed。
|
// 对比生成中同样轮询,直到 completed/failed。
|
||||||
usePolling(
|
usePolling(
|
||||||
() => {
|
() => {
|
||||||
if (!reportId) return
|
if (!reportCampaignId) return
|
||||||
void campaignsApi.getComparison(reportId).then((res) => setComparison(res.data))
|
void campaignsApi.getComparison(reportCampaignId).then((res) => setComparison(res.data))
|
||||||
},
|
},
|
||||||
POLL_INTERVAL_MS,
|
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
|
// 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 analysisTerminal = !!report && !isActiveStatus(report.status)
|
||||||
const analysisModelMissing = !reportCampaign?.analysis_model_config_id && !analysisDefault
|
const analysisModelMissing = !reportCampaign?.analysis_model_config_id && !analysisDefault
|
||||||
const analysisModelName = analysis?.model_config_id
|
const analysisModelName = analysis?.model_config_id
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user