"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { fetchAPI, postAPI } from "@/lib/api"; import type { LatestResult, SectorData, IndexOverview, OpsStatusResponse, StrategyBoard } from "@/lib/api"; import MarketTemp from "@/components/market-temp"; import SectorHeatmap from "@/components/sector-heatmap"; import { useWebSocket } from "@/hooks/use-websocket"; import { useAuth } from "@/hooks/use-auth"; import { ThemeToggle } from "@/components/theme-toggle"; interface ScanStatus { is_trading: boolean; scan_mode: string; description: string; } const SCAN_TIMEOUT_MS = 120_000; // 扫描超时:120秒后自动刷新数据 export default function DashboardPage() { const { user } = useAuth(); const [data, setData] = useState(null); const [sectors, setSectors] = useState([]); const [scanStatus, setScanStatus] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [refreshResult, setRefreshResult] = useState(null); const [indices, setIndices] = useState([]); const [strategyBoard, setStrategyBoard] = useState(null); const [opsStatus, setOpsStatus] = useState(null); const [opsRunning, setOpsRunning] = useState(null); const scanTimeoutRef = useRef | null>(null); const loadData = useCallback(async () => { try { const [latest, sectorData, status, overview, board, ops] = await Promise.all([ fetchAPI("/api/recommendations/latest"), fetchAPI("/api/sectors/hot?limit=8"), fetchAPI("/api/recommendations/status"), fetchAPI("/api/market/overview").catch(() => []), fetchAPI("/api/market/strategy-board").catch(() => null), fetchAPI("/api/market/ops-status").catch(() => null), ]); setData(latest); setSectors(sectorData); setScanStatus(status); setIndices(overview); setStrategyBoard(board); setOpsStatus(ops); } catch (e) { console.error("加载数据失败:", e); } finally { setLoading(false); } }, []); // 清除扫描超时计时器 const clearScanTimeout = useCallback(() => { if (scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); scanTimeoutRef.current = null; } }, []); useEffect(() => { loadData(); }, [loadData]); useWebSocket( useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => { clearScanTimeout(); if (msg.type === "scan_update") { const modeLabel = msg.scan_mode === "intraday" ? "盘中实时" : "盘后"; setRefreshResult(`${modeLabel}扫描完成,发现 ${msg.count ?? 0} 只股票`); setRefreshing(false); loadData(); setTimeout(() => setRefreshResult(null), 5000); } else if (msg.type === "scan_error") { setRefreshResult("扫描失败,请重试"); setRefreshing(false); setTimeout(() => setRefreshResult(null), 5000); } else { // 其他消息类型(如 llm_analysis_ready),刷新数据 loadData(); } }, [loadData, clearScanTimeout]), ["scan_update", "scan_error", "llm_analysis_ready", "sector_scan_ready", "scan_complete"] ); const handleRefresh = async () => { setRefreshing(true); setRefreshResult(null); try { const res = await postAPI<{ status: string; message?: string; is_trading: boolean; }>("/api/recommendations/refresh?scan_session=manual"); if (res.status === "already_running") { setRefreshResult(res.message || "扫描正在执行中,请稍候"); // 保持 refreshing,等待 WS 推送完成 } else if (res.status === "scanning") { setRefreshResult("扫描已启动,完成后自动刷新..."); // 保持 refreshing,等待 WS 推送 } // 设置超时:如果 120 秒内没收到 WebSocket 消息,自动停止加载状态并刷新数据 scanTimeoutRef.current = setTimeout(() => { setRefreshResult("扫描超时,已自动刷新数据"); setRefreshing(false); loadData(); setTimeout(() => setRefreshResult(null), 5000); }, SCAN_TIMEOUT_MS); } catch (e) { console.error("触发扫描失败:", e); setRefreshResult("触发扫描失败,请重试"); setRefreshing(false); setTimeout(() => setRefreshResult(null), 5000); } }; const handleAdminAction = async (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => { setOpsRunning(action); setRefreshResult(null); try { if (action === "update_tracking") { const result = await postAPI<{ tracked?: number; win_rate?: number; avg_return?: number }>("/api/recommendations/update-tracking"); setRefreshResult(`跟踪已更新,样本 ${result.tracked ?? 0},胜率 ${(result.win_rate ?? 0).toFixed(1)}%`); } else if (action === "generate_strategy_board") { await postAPI("/api/market/generate-strategy-board"); setRefreshResult("策略板已生成"); } else { await postAPI("/api/market/generate-strategy-iteration"); setRefreshResult("策略复盘已生成"); } await loadData(); setTimeout(() => setRefreshResult(null), 5000); } catch (error) { console.error("管理员操作失败:", error); setRefreshResult("管理员操作失败,请重试"); setTimeout(() => setRefreshResult(null), 5000); } finally { setOpsRunning(null); } }; // 清理超时计时器 useEffect(() => { return () => clearScanTimeout(); }, [clearScanTimeout]); if (loading) { return (
); } return (
{/* Header bar */}

今日作战

{scanStatus && (

{scanStatus.is_trading ? ( 交易中 · 实时行情 ) : ( 已收盘 · Tushare 日级数据 )}

)}
{user?.role === "admin" && ( )}
{/* Scan result toast */} {refreshResult && (
{refreshResult}
)} {opsStatus && ( user?.role === "admin" ? ( ) : null )}
); } function OpsPanel({ opsStatus, isAdmin, refreshing, onRefresh, opsRunning, onAction, }: { opsStatus: OpsStatusResponse; isAdmin: boolean; refreshing: boolean; onRefresh: () => void; opsRunning: string | null; onAction: (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => void; }) { return (
Admin Ops
{opsStatus.data_freshness.message}
市场 {opsStatus.data_freshness.market_trade_date || "暂无"} 板块 {opsStatus.data_freshness.sector_trade_date || "暂无"} 跟踪 {opsStatus.data_freshness.tracking_trade_date || "暂无"} {opsStatus.scan_running ? "扫描中" : "空闲"}
{isAdmin ? (
查看策略复盘 查看推荐闭环
) : null}
); } function EvidenceHeader({ title }: { title: string }) { return (

{title}

); } function MissionControl({ board, recommendations, sectors, strategyProfile, }: { board: StrategyBoard | null; recommendations: LatestResult["recommendations"]; sectors: SectorData[]; strategyProfile: LatestResult["strategy_profile"]; }) { const actionable = recommendations.filter((rec) => rec.action_plan === "可操作"); const watch = recommendations.filter((rec) => rec.action_plan === "重点关注"); const observe = recommendations.filter((rec) => !["可操作", "重点关注"].includes(rec.action_plan ?? "")); const topSectors = sectors.slice(0, 3); const risks = board?.avoid_rules?.length ? board.avoid_rules : ["等待市场状态和推荐结果更新后生成风险约束。"]; const primaryQueue = (actionable.length ? actionable : watch).slice(0, 3); const laneTitle = actionable.length ? "优先执行" : watch.length ? "候选观察" : "等待信号"; const strategyName = strategyProfile?.name ?? board?.recommended_mode ?? "待定"; const strategyHint = strategyProfile?.description ?? "系统判断今天更适合采用的出手方式"; const riskText = risks.slice(0, 2).join(" / "); return (
今日结论 {board?.generated_by === "rules+llm" && ( AI增强 )}
推荐池

{board?.market_regime ?? "等待市场状态"}

{board?.summary ?? "系统尚未生成今日作战结论。触发扫描后,将基于市场温度、板块主线、推荐生命周期和策略复盘生成操作框架。"}

sector.sector_name).join(" / ") : "暂无"} extra={riskText || "暂无风险约束"} />
{topSectors.length ? ( topSectors.map((sector) => ( {sector.sector_name} = 0 ? "text-red-400" : "text-emerald-400"}`}> {sector.pct_change > 0 ? "+" : ""}{sector.pct_change.toFixed(2)}% )) ) : ( 暂无主线板块 )}
{primaryQueue.length}只
{primaryQueue.length ? ( primaryQueue.map((rec) => ( )) ) : (
暂无执行标的
等待扫描生成可操作或重点关注列表
)}
查看策略复盘
); } function CompactDecision({ label, value, extra }: { label: string; value: string; extra: string }) { return (
{label}
{value}
{extra ?
{extra}
: null}
); } function CompactMissionStock({ rec }: { rec: LatestResult["recommendations"][number] }) { const actionStyle: Record = { "可操作": "bg-red-500/15 text-red-400 border-red-500/20", "重点关注": "bg-amber-500/15 text-amber-400 border-amber-500/20", "观察": "bg-surface-3 text-text-muted border-border-default", }; return (
{rec.name} {rec.action_plan ?? "观察"}
{rec.ts_code} · {rec.sector}
{rec.score}
参考
{rec.trigger_condition ?? rec.reasons?.[0] ?? "等待触发条件确认"}
); } function CommandMetric({ label, value, description }: { label: string; value: number; description: string }) { return (
{label}
{value}
{description}
); } function SectionTitle({ title }: { title: string }) { return (
{title}
); }