"use client"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { fetchAPI, type SectorDetailResponse, type SectorMemberCandidate } from "@/lib/api"; import { formatNumber } from "@/lib/utils"; type MemberFilter = "all" | "actionable" | "watch" | "observe"; function stageLabel(stage?: string) { const map: Record = { early: "启动期", mid: "发展期", late: "后期", end: "尾声", intraday: "盘中" }; return map[stage || ""] || "未分层"; } function signalLabel(signal?: string) { const map: Record = { breakout: "突破", breakout_confirm: "确认", pullback: "回踩", launch: "启动", reversal: "反转", flow_momentum: "资金", none: "无信号", }; return map[signal || ""] || signal || "无信号"; } function actionTone(action: string) { if (action === "可操作") return "border-red-500/15 bg-red-500/[0.06] text-red-300"; if (action === "重点关注") return "border-amber-500/15 bg-amber-500/[0.07] text-amber-300"; return "border-border-subtle bg-surface-2 text-text-muted"; } export default function SectorDetailPage() { const params = useParams<{ name: string }>(); const sectorName = decodeURIComponent(params.name || ""); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState("all"); const loadData = useCallback(async () => { if (!sectorName) return; setLoading(true); try { const result = await fetchAPI(`/api/sectors/${encodeURIComponent(sectorName)}/detail`); setData(result); } finally { setLoading(false); } }, [sectorName]); useEffect(() => { loadData(); }, [loadData]); const sector = data?.sector; const displayPct = sector ? (sector.realtime_pct_change ?? sector.pct_change) : 0; const displayAmount = sector ? (sector.realtime_amount ?? sector.capital_inflow) : 0; const displayLimitUp = sector ? (sector.realtime_limit_up_count ?? sector.limit_up_count) : 0; const filteredMembers = useMemo(() => { const members = data?.members || []; if (filter === "actionable") return members.filter((item) => item.action_plan === "可操作"); if (filter === "watch") return members.filter((item) => item.action_plan === "重点关注"); if (filter === "observe") return members.filter((item) => item.action_plan === "观察"); return members; }, [data, filter]); const reasonCounts = useMemo(() => { const counts: Record = {}; (data?.members || []).forEach((item) => { (item.elimination_reason || "待确认").split(";").forEach((reason) => { if (!reason) return; counts[reason] = (counts[reason] || 0) + 1; }); }); return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 6); }, [data]); return (
← 返回板块主线
Theme Desk

{sector?.sector_name || sectorName}

{sector?.catalyst_reasons?.[0] || "最近扫描沉淀的板块成分候选,按主题、资金、角色和入场时机排序。"}

{stageLabel(sector?.stage)} 扫描 {data?.scan_session || "-"} {sector?.data_mode || "local"} {(sector?.theme_aliases || []).slice(0, 4).map((alias) => {alias})}
0 ? "+" : ""}${displayPct.toFixed(2)}%`} tone={displayPct >= 0 ? "up" : "down"} /> = 0 ? "+" : ""}${formatNumber(displayAmount)}`} tone={displayAmount >= 0 ? "up" : "down"} />

成分候选

{loading ? (
{[1, 2, 3, 4].map((item) =>
)}
) : filteredMembers.length === 0 ? (
暂无成分候选记录
) : (
{filteredMembers.map((member) => )}
)}
); } function MemberRow({ member }: { member: SectorMemberCandidate }) { return (
{member.name}
{member.ts_code}
{member.action_plan} {member.stock_role || "候选"}
{member.final_score.toFixed(1)}
{member.action_plan === "观察" ? member.elimination_reason || "等待资金和时机确认" : member.trigger_condition || "等待触发条件"}
); } function Badge({ children }: { children: React.ReactNode }) { return {children}; } function Metric({ label, value, tone = "default" }: { label: string; value: string; tone?: "default" | "up" | "down" }) { const color = tone === "up" ? "text-red-400" : tone === "down" ? "text-emerald-400" : "text-text-primary"; return (
{label}
{value}
); } function Summary({ label, value, tone = "default" }: { label: string; value: string | number; tone?: "default" | "red" | "amber" }) { const color = tone === "red" ? "text-red-400" : tone === "amber" ? "text-amber-400" : "text-text-primary"; return (
{label}
{value}
); } function SmallStat({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function Score({ label, value }: { label: string; value: number }) { return (
{label}
{Math.round(value)}
); }