feat: 题材热点历史页展示最近10个交易日题材涨幅矩阵
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchActiveThemes } from "@/lib/theme-api";
|
||||
import { ArrowLeft, RefreshCw, Flame } from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/theme-history")({
|
||||
component: ThemeHistoryPage,
|
||||
});
|
||||
|
||||
/** 格式化涨幅,红涨绿跌 */
|
||||
function formatGain(v: number | null | undefined): string {
|
||||
if (v == null) return "·";
|
||||
const s = v >= 0 ? `+${v.toFixed(2)}%` : `${v.toFixed(2)}%`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function ThemeHistoryPage() {
|
||||
const { data, isLoading, isFetching, refetch } = useQuery({
|
||||
queryKey: ["themes", "active"],
|
||||
queryFn: fetchActiveThemes,
|
||||
staleTime: 60_000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const dates = data?.dates ?? [];
|
||||
const themes = data?.themes ?? [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* 顶栏 */}
|
||||
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b">
|
||||
<div className="max-w-5xl mx-auto px-4 h-12 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link to="/themes" className="hover:opacity-70 transition-opacity" aria-label="返回">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<h1 className="text-base font-semibold">题材热点历史</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="刷新"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-4 mt-3 pb-8">
|
||||
<p className="text-[10px] text-muted-foreground mb-2">
|
||||
活跃题材(最近 10 个交易日内上榜涨幅前20)· 按上榜次数排序 · 共 {themes.length} 个
|
||||
</p>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="animate-pulse rounded-xl bg-muted h-32" />
|
||||
) : dates.length === 0 || themes.length === 0 ? (
|
||||
<div className="text-center text-sm text-muted-foreground py-16">
|
||||
暂无数据,数据将在每日收盘后自动采集
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th scope="col" className="px-3 py-2 text-left font-medium whitespace-nowrap">题材</th>
|
||||
{dates.map((d) => (
|
||||
<th key={d} scope="col" title={d} className="px-2 py-2 text-right font-medium tabular-nums whitespace-nowrap">
|
||||
{d.slice(5)}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="px-2 py-2 text-right font-medium">上榜</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{themes.map((t) => (
|
||||
<tr key={t.themeCode} className="border-b last:border-0 hover:bg-muted/30">
|
||||
<td className="px-3 py-1.5 whitespace-nowrap">
|
||||
<Link
|
||||
to="/theme/$code"
|
||||
params={{ code: t.themeCode }}
|
||||
className="font-medium hover:text-primary hover:underline"
|
||||
>
|
||||
{t.themeName}
|
||||
</Link>
|
||||
</td>
|
||||
{dates.map((d) => {
|
||||
const g = t.dailyGains[d];
|
||||
const cls = g == null ? "text-muted-foreground/40" : g >= 0 ? "text-red-500" : "text-green-500";
|
||||
return (
|
||||
<td key={d} className={`px-2 py-1.5 text-right tabular-nums whitespace-nowrap ${cls}`}>
|
||||
{formatGain(g)}
|
||||
</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" />
|
||||
{t.appearCount}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TrendingDown,
|
||||
Flame,
|
||||
Network,
|
||||
History,
|
||||
} from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/themes")({
|
||||
@@ -78,6 +79,13 @@ function ThemesPage() {
|
||||
<Flame className="h-3.5 w-3.5" />
|
||||
热点股
|
||||
</Link>
|
||||
<Link
|
||||
to="/theme-history"
|
||||
className="text-xs text-primary flex items-center gap-1 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<History className="h-3.5 w-3.5" />
|
||||
历史
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user