- Fix `simple_test_analysis` template in E2E test setup to align with Orchestrator's data fetch logic.
- Implement and verify additional E2E scenarios:
- Scenario C: Partial Provider Failure (verified error propagation fix in Orchestrator).
- Scenario D: Invalid Symbol input.
- Scenario E: Analysis Module failure.
- Update `WorkflowStateMachine::handle_report_failed` to correctly scope error broadcasting to the specific task instead of failing effectively silently or broadly.
- Update testing strategy documentation to reflect completed Phase 4 testing.
- Skip Scenario B (Orchestrator Restart) as persistence is not yet implemented (decision made to defer persistence).
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
interface ReportHeaderProps {
|
|
unifiedSymbol: string;
|
|
displayMarket: string;
|
|
isLoading: boolean;
|
|
financials: any;
|
|
snapshot: any;
|
|
snapshotLoading: boolean;
|
|
triggering: boolean;
|
|
hasRunningTask: boolean;
|
|
isAnalysisRunning: boolean;
|
|
onStartAnalysis: () => void;
|
|
onStopAnalysis: () => void;
|
|
onContinueAnalysis: () => void;
|
|
// Template props
|
|
templateSets: any;
|
|
selectedTemplateId: string;
|
|
onSelectTemplate: (id: string) => void;
|
|
}
|
|
|
|
export function ReportHeader({
|
|
unifiedSymbol,
|
|
displayMarket,
|
|
isLoading,
|
|
financials,
|
|
snapshot,
|
|
snapshotLoading,
|
|
triggering,
|
|
hasRunningTask,
|
|
isAnalysisRunning,
|
|
onStartAnalysis,
|
|
onStopAnalysis,
|
|
onContinueAnalysis,
|
|
templateSets,
|
|
selectedTemplateId,
|
|
onSelectTemplate,
|
|
}: ReportHeaderProps) {
|
|
return (
|
|
<>
|
|
<Card className="flex-1">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl">报告页面</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground min-w-20">股票代码:</span>
|
|
<span className="font-medium">{unifiedSymbol}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground min-w-20">交易市场:</span>
|
|
<span className="font-medium">{displayMarket}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground min-w-20">公司名称:</span>
|
|
<span className="font-medium">
|
|
{isLoading ? (
|
|
<span className="flex items-center gap-1">
|
|
<Spinner className="size-3" />
|
|
<span className="text-muted-foreground">加载中...</span>
|
|
</span>
|
|
) : financials?.name ? (
|
|
financials.name
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="w-80 flex-shrink-0">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">分析控制</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-3">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">选择模板</label>
|
|
<Select value={selectedTemplateId} onValueChange={onSelectTemplate} disabled={triggering}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="选择分析模板" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{templateSets && Object.entries(templateSets).map(([id, set]: [string, any]) => (
|
|
<SelectItem key={id} value={id}>
|
|
{set.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
onClick={onStartAnalysis}
|
|
disabled={triggering || !selectedTemplateId}
|
|
className="flex-1"
|
|
>
|
|
{triggering ? '触发中…' : '触发分析'}
|
|
</Button>
|
|
<Button variant="destructive" onClick={onStopAnalysis} disabled={!hasRunningTask}>
|
|
停止
|
|
</Button>
|
|
<Button variant="outline" onClick={onContinueAnalysis} disabled={isAnalysisRunning}>
|
|
继续
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="w-64 flex-shrink-0">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">昨日快照</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-sm">
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
|
<SnapshotItem
|
|
label="PB"
|
|
value={snapshot?.pb != null ? `${Number(snapshot.pb).toFixed(2)}` : undefined}
|
|
loading={snapshotLoading}
|
|
/>
|
|
<SnapshotItem
|
|
label="股价"
|
|
value={snapshot?.close != null ? `${Number(snapshot.close).toFixed(2)}` : undefined}
|
|
loading={snapshotLoading}
|
|
/>
|
|
<SnapshotItem
|
|
label="PE"
|
|
value={snapshot?.pe != null ? `${Number(snapshot.pe).toFixed(2)}` : undefined}
|
|
loading={snapshotLoading}
|
|
/>
|
|
<SnapshotItem
|
|
label="市值"
|
|
value={snapshot?.total_mv != null ? `${Math.round((snapshot.total_mv as number) / 10000).toLocaleString('zh-CN')} 亿元` : undefined}
|
|
loading={snapshotLoading}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SnapshotItem({ label, value, loading }: { label: string; value?: string; loading: boolean }) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground min-w-8">{label}:</span>
|
|
<span className="font-medium">
|
|
{loading ? (
|
|
<span className="flex items-center gap-1">
|
|
<Spinner className="size-3" />
|
|
</span>
|
|
) : value ? (
|
|
value
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|