diff --git a/backend/routes/core_stocks.py b/backend/routes/core_stocks.py index d92b848..b39cb7a 100644 --- a/backend/routes/core_stocks.py +++ b/backend/routes/core_stocks.py @@ -59,6 +59,19 @@ async def active_core_stocks(): stocks.sort(key=lambda x: x.get("lastAppear") or "", reverse=True) stocks.sort(key=lambda x: -x["appearCount"]) + # 一次性取窗口内全部题材关联,按 stock_code 分组(跨天题材去重) + themes_rows = conn.execute( + f"""SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes + WHERE trade_date IN ({placeholders})""", + dates, + ).fetchall() + themes_by_stock: dict[str, dict[str, dict]] = {} + for t in themes_rows: + per = themes_by_stock.setdefault(t["stock_code"], {}) + per.setdefault(t["theme_code"], {"theme_code": t["theme_code"], "theme_name": t["theme_name"]}) + for s in stocks: + s["themes"] = list(themes_by_stock.get(s["stockCode"], {}).values()) + # daysSinceLastAppear:最近上榜距窗口最新交易日的自然日差(简单口径) latest = dates[-1] if dates else None for s in stocks: diff --git a/src/lib/core-stock-api.ts b/src/lib/core-stock-api.ts index 8cbba8b..49fb647 100644 --- a/src/lib/core-stock-api.ts +++ b/src/lib/core-stock-api.ts @@ -9,6 +9,7 @@ export interface ActiveCoreStock { appearCount: number; lastAppear: string | null; daysSinceLastAppear: number; // 最近上榜距窗口最新交易日的自然日差 + themes: { theme_code: string; theme_name: string }[]; // 所属题材列表 } export interface ActiveCoreStocksResponse { diff --git a/src/routes/core-stocks.tsx b/src/routes/core-stocks.tsx index 570fc37..756af6d 100644 --- a/src/routes/core-stocks.tsx +++ b/src/routes/core-stocks.tsx @@ -1,7 +1,8 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; +import { Fragment, useState } from "react"; import { fetchActiveCoreStocks } from "@/lib/core-stock-api"; -import { ArrowLeft, RefreshCw, Flame } from "lucide-react"; +import { ArrowLeft, RefreshCw, Flame, ChevronDown, ChevronRight } from "lucide-react"; export const Route = createFileRoute("/core-stocks")({ component: CoreStocksPage, @@ -24,6 +25,7 @@ function CoreStocksPage() { const dates = data?.dates ?? []; const stocks = data?.stocks ?? []; + const [expanded, setExpanded] = useState(null); return (
@@ -74,35 +76,67 @@ function CoreStocksPage() { - {stocks.map((s) => ( - - - {s.stockName} - {s.stockCode} - - {dates.map((d) => { - const g = s.dailyGains[d]; - const cls = g == null ? "text-muted-foreground/40" : g >= 0 ? "text-red-500" : "text-green-500"; - return ( - - {formatGain(g)} + {stocks.map((s) => { + const isOpen = expanded === s.stockCode; + return ( + + setExpanded(isOpen ? null : s.stockCode)} + className="border-b last:border-0 hover:bg-muted/30 cursor-pointer" + > + + + {isOpen ? : } + {s.stockName} + {s.stockCode} + - ); - })} - - {s.coverCount ?? "·"} - - - {s.lastAppear ? s.lastAppear.slice(5) : "·"} - - - - - {s.appearCount} - - - - ))} + {dates.map((d) => { + const g = s.dailyGains[d]; + const cls = g == null ? "text-muted-foreground/40" : g >= 0 ? "text-red-500" : "text-green-500"; + return ( + + {formatGain(g)} + + ); + })} + + {s.coverCount ?? "·"} + + + {s.lastAppear ? s.lastAppear.slice(5) : "·"} + + + + + {s.appearCount} + + + + {isOpen && ( + + +
所属题材
+ {s.themes && s.themes.length > 0 ? ( +
+ {s.themes.map((t) => ( + + {t.theme_name} + + ))} +
+ ) : ( + 暂无题材数据 + )} + + + )} +
+ ); + })}