"use client"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { fetchAPI } from "@/lib/api"; import type { RecommendationData, DayGroup } from "@/lib/api"; import { getScoreColor } from "@/lib/utils"; import KlineChart from "@/components/kline-chart"; import CapitalFlowChart from "@/components/capital-flow"; import { ErrorBoundary } from "@/components/error-boundary"; interface StockSignals { ts_code: string; name: string; score: number; trend_score: number; signal_count: number; ma_bullish: boolean; volume_breakout: boolean; macd_golden: boolean; rsi_healthy: boolean; pullback_support: boolean; big_yang: boolean; boll_support: boolean; support_price: number | null; resist_price: number | null; stop_loss_price: number | null; rally_pct_5d: number; rally_pct_10d: number; distance_from_high: number; position_score: number; } interface RecScore { supply_demand_score: number; price_action_score: number; technical_score: number; position_score: number; score: number; } interface QuoteData { ts_code: string; name: string; price: number; pct_chg: number; volume: number; amount: number; turnover_rate: number; pe: number | null; pb: number | null; circ_mv: number | null; total_mv: number | null; volume_ratio: number | null; high: number | null; low: number | null; open: number | null; pre_close: number | null; limit_up: number | null; limit_down: number | null; amplitude: number | null; } interface FlowRecord { trade_date: string; main_net_inflow: number; net_mf_amount: number; elg_net: number; lg_net: number; md_net: number; sm_net: number; } export default function StockDetailPage() { const params = useParams(); const code = params.code as string; const [quote, setQuote] = useState(null); const [signals, setSignals] = useState(null); const [recScore, setRecScore] = useState(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [kline, setKline] = useState([]); const [capitalFlow, setCapitalFlow] = useState([]); useEffect(() => { if (!code) return; Promise.all([ fetchAPI(`/api/stocks/${code}/quote`).catch(() => null), fetchAPI(`/api/stocks/${code}/signals`).catch(() => null), fetchAPI(`/api/stocks/${code}/kline?days=120`).catch(() => []), fetchAPI(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []), ]).then(([q, s, k, c]) => { setQuote(q); setSignals(s); setKline(k); setCapitalFlow(c as FlowRecord[]); }); // 尝试从推荐历史中获取该股票的评分 fetchAPI(`/api/recommendations/history?days=14`).then((history) => { for (const group of history) { const rec = group.recommendations?.find((r) => r.ts_code === code); if (rec && rec.supply_demand_score) { setRecScore({ supply_demand_score: rec.supply_demand_score, price_action_score: rec.price_action_score ?? 0, technical_score: rec.technical_score, position_score: rec.position_score ?? 50, score: rec.score, }); break; } } }).catch(() => null); }, [code]); const latestFlow = capitalFlow.length > 0 ? capitalFlow[capitalFlow.length - 1] : null; return (
{/* Back */} 返回 {/* Quote header */}
{quote && (
{quote.name} {quote.ts_code}
AI 诊断
0 ? "text-red-400" : quote.pct_chg < 0 ? "text-emerald-400" : "text-text-primary" }`} > {quote.price.toFixed(2)} 0 ? "text-red-400" : "text-emerald-400" }`} > {quote.pct_chg > 0 ? "+" : ""} {quote.pct_chg.toFixed(2)}% {quote.pre_close && ( 昨收 {quote.pre_close.toFixed(2)} )}
{/* OHLC row */}
0 ? quote.open.toFixed(2) : "-"} color={ quote.open && quote.open > 0 && quote.pre_close ? quote.open >= quote.pre_close ? "text-red-400" : "text-emerald-400" : undefined } /> 0 ? quote.high.toFixed(2) : "-"} color="text-red-400" /> 0 ? quote.low.toFixed(2) : "-"} color="text-emerald-400" />
{/* Valuation row */}
2} />
{/* Market cap row */}
)}
{/* Position Safety + Capital Flow Breakdown */}
{/* Position Safety Card */} {signals && (

仓位安全评估

{Math.round(signals.position_score)} 安全分
{/* 位置安全评分条 */}
{/* Metrics */}
)} {/* Capital Flow Breakdown */} {latestFlow && ( )}
{/* Technical signals */} {signals && (

技术面分析

{recScore && (
{recScore.score} 综合分
)}
{/* ── Module 1: 核心评分维度 ── */}
核心评分维度 {recScore && ( 综合 = 供需×50% + 形态×40% + 趋势×10% )}
{recScore ? (
) : (
)}
{/* ── Module 2: 辅助信号 ── */}
辅助信号 ·触发即加分,不参与主评分 = 4 ? "text-amber-400" : signals.signal_count >= 2 ? "text-cyan-400" : "text-text-muted"}`}> {signals.signal_count}/7
{/* ── Module 3: 关键价位 ── */}
关键价位
)} {/* K-line + Capital flow chart */}
{kline.length > 0 && } {capitalFlow.length > 0 && }
); } /* ── Helper components ── */ function MiniStat({ label, value, color, highlight, }: { label: string; value: string; color?: string; highlight?: boolean; }) { return (
{label}
{value}
); } function PositionBar({ label, value, invert = false }: { label: string; value: number; invert?: boolean }) { const absVal = Math.abs(value); const maxDisplay = 30; const pct = Math.min(absVal / maxDisplay, 1) * 100; const isPositive = value > 0; const showWarning = invert ? value < -20 : value > 20; const barColor = showWarning ? "bg-amber-400" : isPositive ? "bg-red-400" : "bg-emerald-400"; const textColor = showWarning ? "text-amber-400" : isPositive ? "text-red-400" : "text-emerald-400"; return (
{label}
{isPositive ? "+" : ""} {value.toFixed(1)}%
); } function CapitalFlowBreakdown({ flow }: { flow: FlowRecord }) { const maxDisplay = Math.max( ...[flow.elg_net, flow.lg_net, flow.md_net, flow.sm_net].map(Math.abs), 1 ); const isMainInflow = flow.main_net_inflow > 0; return (

今日资金流向

{isMainInflow ? "+" : ""} {formatFlowAmount(flow.main_net_inflow)} 主力净流入
); } function FlowBar({ label, value, max }: { label: string; value: number; max: number }) { const absVal = Math.abs(value); const pct = (absVal / max) * 100; const isInflow = value > 0; return (
{label} {isInflow ? "+" : ""} {formatFlowAmount(value)}
); } function SignalChip({ label, active, points }: { label: string; active: boolean; points: number }) { return (
{label} {active ? `+${points}` : "—"}
); } function DimensionScore({ label, sublabel, value, displayValue }: { label: string; sublabel: string; value: number; displayValue?: string }) { const width = Math.min(value, 100); const gradientClass = value >= 70 ? "score-bar-gradient-high" : value >= 50 ? "score-bar-gradient-mid" : "score-bar-gradient-low"; const scoreColor = value >= 70 ? "text-amber-400" : value >= 50 ? "text-cyan-400" : "text-text-muted"; return (
{label} {sublabel}
{displayValue ?? value.toFixed(0)}
); } function PriceLevel({ label, value, color }: { label: string; value: number | null; color: string }) { return (
{label}
{value?.toFixed(2) ?? "—"}
); } /* ── Helper functions ── */ function getPositionColor(score: number) { if (score >= 70) return "#22c55e"; if (score >= 40) return "#eab308"; return "#ef4444"; } function formatBigNum(val: number): string { if (val >= 10000) return (val / 10000).toFixed(1) + "万"; return val.toFixed(0); } function formatFlowAmount(val: number): string { const absVal = Math.abs(val); if (absVal >= 10000) return (val / 10000).toFixed(1) + "亿"; return val.toFixed(0) + "万"; }