import { createFileRoute, Link } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { fetchThemeDetail, fetchThemeNews, fetchThemeQuote, fetchThemeStocks, type ThemeStock, type ThemeNewsItem, } from "@/lib/theme-api"; import { getStockBoard } from "@/lib/stock-api"; import { formatMoney } from "@/lib/utils"; import { Card, CardContent } from "@/components/ui/card"; import { ArrowLeft, RefreshCw, TrendingUp, TrendingDown, Flame, Newspaper, ChevronDown, ChevronUp, Info, } from "lucide-react"; export const Route = createFileRoute("/theme/$code")({ component: ThemeDetailPage, }); function ThemeDetailPage() { const { code } = Route.useParams(); const detailQ = useQuery({ queryKey: ["themeDetail", code], queryFn: () => fetchThemeDetail(code), staleTime: 60_000, retry: false, }); const stocksQ = useQuery({ queryKey: ["themeStocks", code], queryFn: () => fetchThemeStocks(code), staleTime: 30_000, retry: false, }); // 相关新闻:pageNum 偏移分页(东财接口 maxEuTime 是增量游标,翻页靠 pageNum 递增) const [newsPage, setNewsPage] = useState(1); const [newsItems, setNewsItems] = useState([]); const newsQ = useQuery({ queryKey: ["themeNews", code, newsPage], queryFn: () => fetchThemeNews(code, newsPage, 10), staleTime: 60_000, retry: false, }); const quoteQ = useQuery({ queryKey: ["themeQuote", code], queryFn: () => fetchThemeQuote(code), staleTime: 30_000, retry: false, }); // 分页追加:首页重置列表,翻页拼接 useEffect(() => { if (!newsQ.data?.list) return; setNewsItems((prev) => (newsPage === 1 ? newsQ.data!.list : [...prev, ...newsQ.data!.list])); }, [newsQ.data, newsPage]); const isLoading = detailQ.isLoading || stocksQ.isLoading; const isError = detailQ.isError || stocksQ.isError; const isFetching = detailQ.isFetching || stocksQ.isFetching; const detail = detailQ.data; const stocks = stocksQ.data?.data ?? []; const statistic = stocksQ.data?.statistic; const total = stocksQ.data?.total ?? 0; const refresh = () => { detailQ.refetch(); stocksQ.refetch(); quoteQ.refetch(); setNewsPage(1); newsQ.refetch(); }; const baseInfo = detail?.baseInfo; const hotEvent = detail?.hotEvent; return (
{/* ── 顶栏 ── */}

{baseInfo?.themeName ?? "题材详情"}

{isLoading ? (
) : isError ? (

数据加载失败

) : ( <> {/* ── 题材简介 ── */} {baseInfo?.introduction && (

题材简介

{baseInfo.introduction}

)} {/* ── 热点事件 ── */} {hotEvent?.newsTitle && (

热点事件

{hotEvent.newsMediaName && ( {hotEvent.newsMediaName} {hotEvent.newsPublishTimeFormat ? ` · ${hotEvent.newsPublishTimeFormat}` : ""} )}

{hotEvent.newsTitle}

{hotEvent.newsSummary && (

{hotEvent.newsSummary}

)}
)} {/* ── 板块统计 ── */} {/* ── 相关新闻(分页加载) ── */} {newsItems.length > 0 && ( 1} onLoadMore={() => setNewsPage((p) => p + 1)} /> )} {/* ── 相关股票 ── */}

相关股票

共 {total} 只
{stocks.length === 0 ? ( 暂无相关股票 ) : (
{stocks.map((s) => ( ))}
)}
)}
); } /* ============================================================ 板块统计条 ============================================================ */ function StatBar({ f3, up, down, flat, fex5, total, strength, hotValue, hotValueUpLimit, }: { f3: number | null | undefined; up: number | null | undefined; down: number | null | undefined; flat: number | null | undefined; fex5: number | null | undefined; total: number; strength: number | null; hotValue: number; hotValueUpLimit: number; }) { const isPos = (f3 ?? 0) >= 0; const hotPct = hotValueUpLimit > 0 ? Math.min((hotValue / hotValueUpLimit) * 100, 100) : 0; const showQuote = strength != null || hotValueUpLimit > 0; return (

板块涨幅

{f3 != null ? `${isPos ? "+" : ""}${f3.toFixed(2)}%` : "--"}

上涨

{up ?? "--"}

下跌

{down ?? "--"}

成交额

{fex5 != null ? formatMoney(fex5) : "--"}

{(flat != null && flat > 0) && (

平盘 {flat} 只

)} {/* 强度 + 热度(来自单题材实时行情接口) */} {showQuote && (
{strength != null && ( 强度 {strength} )} {hotValueUpLimit > 0 && ( 热度 {hotValue}/{hotValueUpLimit} )}
)}
); } /* ============================================================ 相关新闻(分页加载) ============================================================ */ function NewsList({ items, total, loadingMore, onLoadMore, }: { items: ThemeNewsItem[]; total: number; loadingMore: boolean; onLoadMore: () => void; }) { const hasMore = items.length < total; return (

相关新闻

共 {total} 条
{items.map((n, idx) => (

{n.newsTitle}

{n.newsMediaName} {n.showDateTimeFormat ? ` · ${n.showDateTimeFormat}` : ""} {n.commentCount > 0 && · {n.commentCount} 评论}

))}
{hasMore && ( )}
); } /* ============================================================ 相关股票行 ============================================================ */ function ThemeStockRow({ stock }: { stock: ThemeStock }) { const [showReason, setShowReason] = useState(true); // 入选理由默认展开 const board = getStockBoard(stock.securityCode); const isPos = (stock.f3 ?? 0) >= 0; const reasons = stock.keywordList ?? []; // 换手率:接口返回放大 100 倍的值(如 3733 = 37.33%) const turnoverRate = stock.f8 != null ? (stock.f8 > 100 ? stock.f8 / 100 : stock.f8) : null; return ( {/* 名称 + 现价 + 涨幅 */}

{stock.securityName}

{board.label && ( {board.label} )} {stock.label && ( {stock.label} )}

现价

{stock.f2 != null ? stock.f2.toFixed(2) : "--"}

涨跌

{isPos ? "+" : ""} {stock.f3 != null ? stock.f3.toFixed(2) : "--"}%

{/* 行业 + 换手 + 主力 + 成交额 */}
{stock.f100 && ( {stock.f100} )} 换手 {turnoverRate != null ? turnoverRate.toFixed(2) : "--"}% 主力 {formatMoney(stock.f62)} 成交 {formatMoney(stock.f6)}
{/* 入选理由 */} {reasons.length > 0 && (
{showReason && (

{reasons.map((r) => r.introduction).filter(Boolean).join(" ")}

)}
)}
); }