feat: 核心股追踪行点击展开显示所属题材列表
- active 接口返回每只核心股 themes(窗口内按题材去重) - 页面行点击展开题材标签,chevron 指示展开状态 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
23644a9c77
commit
6d3c470b7b
3 files changed
+77
-29
No files matched your search
@@ -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.get("lastAppear") or "", reverse=True)
|
||||||
stocks.sort(key=lambda x: -x["appearCount"])
|
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:最近上榜距窗口最新交易日的自然日差(简单口径)
|
# daysSinceLastAppear:最近上榜距窗口最新交易日的自然日差(简单口径)
|
||||||
latest = dates[-1] if dates else None
|
latest = dates[-1] if dates else None
|
||||||
for s in stocks:
|
for s in stocks:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ActiveCoreStock {
|
|||||||
appearCount: number;
|
appearCount: number;
|
||||||
lastAppear: string | null;
|
lastAppear: string | null;
|
||||||
daysSinceLastAppear: number; // 最近上榜距窗口最新交易日的自然日差
|
daysSinceLastAppear: number; // 最近上榜距窗口最新交易日的自然日差
|
||||||
|
themes: { theme_code: string; theme_name: string }[]; // 所属题材列表
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActiveCoreStocksResponse {
|
export interface ActiveCoreStocksResponse {
|
||||||
|
|||||||
+63
-29
@@ -1,7 +1,8 @@
|
|||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
import { fetchActiveCoreStocks } from "@/lib/core-stock-api";
|
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")({
|
export const Route = createFileRoute("/core-stocks")({
|
||||||
component: CoreStocksPage,
|
component: CoreStocksPage,
|
||||||
@@ -24,6 +25,7 @@ function CoreStocksPage() {
|
|||||||
|
|
||||||
const dates = data?.dates ?? [];
|
const dates = data?.dates ?? [];
|
||||||
const stocks = data?.stocks ?? [];
|
const stocks = data?.stocks ?? [];
|
||||||
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
@@ -74,35 +76,67 @@ function CoreStocksPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stocks.map((s) => (
|
{stocks.map((s) => {
|
||||||
<tr key={s.stockCode} className="border-b last:border-0 hover:bg-muted/30">
|
const isOpen = expanded === s.stockCode;
|
||||||
<td className="px-3 py-1.5 whitespace-nowrap">
|
return (
|
||||||
<span className="font-medium">{s.stockName}</span>
|
<Fragment key={s.stockCode}>
|
||||||
<span className="ml-1 text-[10px] text-muted-foreground">{s.stockCode}</span>
|
<tr
|
||||||
</td>
|
onClick={() => setExpanded(isOpen ? null : s.stockCode)}
|
||||||
{dates.map((d) => {
|
className="border-b last:border-0 hover:bg-muted/30 cursor-pointer"
|
||||||
const g = s.dailyGains[d];
|
>
|
||||||
const cls = g == null ? "text-muted-foreground/40" : g >= 0 ? "text-red-500" : "text-green-500";
|
<td className="px-3 py-1.5 whitespace-nowrap">
|
||||||
return (
|
<span className="inline-flex items-center gap-1">
|
||||||
<td key={d} className={`px-2 py-1.5 text-right tabular-nums whitespace-nowrap ${cls}`}>
|
{isOpen ? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" /> : <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||||
{formatGain(g)}
|
<span className="font-medium">{s.stockName}</span>
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground">{s.stockCode}</span>
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
);
|
{dates.map((d) => {
|
||||||
})}
|
const g = s.dailyGains[d];
|
||||||
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap text-muted-foreground">
|
const cls = g == null ? "text-muted-foreground/40" : g >= 0 ? "text-red-500" : "text-green-500";
|
||||||
{s.coverCount ?? "·"}
|
return (
|
||||||
</td>
|
<td key={d} className={`px-2 py-1.5 text-right tabular-nums whitespace-nowrap ${cls}`}>
|
||||||
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap text-muted-foreground">
|
{formatGain(g)}
|
||||||
{s.lastAppear ? s.lastAppear.slice(5) : "·"}
|
</td>
|
||||||
</td>
|
);
|
||||||
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap">
|
})}
|
||||||
<span className="inline-flex items-center gap-0.5 text-orange-500">
|
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap text-muted-foreground">
|
||||||
<Flame className="h-3 w-3" />
|
{s.coverCount ?? "·"}
|
||||||
{s.appearCount}
|
</td>
|
||||||
</span>
|
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap text-muted-foreground">
|
||||||
</td>
|
{s.lastAppear ? s.lastAppear.slice(5) : "·"}
|
||||||
</tr>
|
</td>
|
||||||
))}
|
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap">
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-orange-500">
|
||||||
|
<Flame className="h-3 w-3" />
|
||||||
|
{s.appearCount}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isOpen && (
|
||||||
|
<tr className="border-b bg-muted/20">
|
||||||
|
<td colSpan={dates.length + 4} className="px-3 py-2">
|
||||||
|
<div className="text-[11px] text-muted-foreground mb-1">所属题材</div>
|
||||||
|
{s.themes && s.themes.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{s.themes.map((t) => (
|
||||||
|
<span
|
||||||
|
key={t.theme_code}
|
||||||
|
className="px-1.5 py-0.5 rounded bg-primary/10 text-primary text-[11px]"
|
||||||
|
>
|
||||||
|
{t.theme_name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">暂无题材数据</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user