重写板块资金流向前端:行业/概念 Tab 切换与资金流入/涨幅排序

This commit is contained in:
Sakurasan
2026-07-13 20:57:00 +08:00
parent 2db6bb8519
commit 65013dad3e
+62 -53
View File
@@ -1,29 +1,28 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useState, useMemo } from "react";
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchSectors, type SectorItem, type SectorType } from "@/lib/stock-api";
import { Card, CardContent } from "@/components/ui/card";
import { ArrowLeft, TrendingUp, TrendingDown, RefreshCw } from "lucide-react";
import { ArrowLeft, RefreshCw, ArrowDown, ArrowUp, TrendingUp, TrendingDown } from "lucide-react";
export const Route = createFileRoute("/sectors")({
component: SectorsPage,
});
/* ── Tab 定义 ── */
/* ── Tabs:行业 / 概念 ── */
const TABS: { key: SectorType; label: string }[] = [
{ key: "industry", label: "行业板块" },
{ key: "concept", label: "概念板块" },
{ key: "industry", label: "行业" },
{ key: "concept", label: "概念" },
];
type SortMode = "mainNetInflow" | "changePercent";
/* ── 排序维度 ── */
type SortKey = "mainNetInflow" | "changePercent";
/* ── 页面组件 ── */
function SectorsPage() {
const [tab, setTab] = useState<SectorType>("industry");
const [sort, setSort] = useState<SortMode>("mainNetInflow");
const [sortKey, setSortKey] = useState<SortKey>("mainNetInflow");
const [asc, setAsc] = useState(false);
// 使用 useQueryqueryKey 包含 tab,切换 tab 时 queryKey 变化
// React Query 自动隔离 industry / concept 的数据,不会混用
const { data = [], isLoading, isFetching, refetch } = useQuery({
queryKey: ["sectors", tab],
queryFn: ({ signal }) => fetchSectors(tab, signal),
@@ -31,14 +30,22 @@ function SectorsPage() {
retry: false,
});
const sorted = useMemo(
() =>
[...data].sort((a, b) => {
if (sort === "mainNetInflow") return b.mainNetInflow - a.mainNetInflow;
return (b.changePercent ?? 0) - (a.changePercent ?? 0);
}),
[data, sort],
);
const sorted = useMemo(() => {
const dir = asc ? 1 : -1;
return [...data].sort((a, b) => {
const av = sortKey === "mainNetInflow" ? a.mainNetInflow : a.changePercent ?? -Infinity;
const bv = sortKey === "mainNetInflow" ? b.mainNetInflow : b.changePercent ?? -Infinity;
return (bv - av) * dir;
});
}, [data, sortKey, asc]);
const toggleSort = (key: SortKey) => {
if (key === sortKey) setAsc((v) => !v);
else {
setSortKey(key);
setAsc(false);
}
};
return (
<div className="min-h-screen bg-background">
@@ -60,9 +67,9 @@ function SectorsPage() {
</div>
</header>
{/* Tab + 排序切换 */}
<div className="max-w-5xl mx-auto px-4 mt-4 flex items-center gap-2">
<div className="flex gap-1 bg-muted rounded-lg p-1 flex-1">
{/* Tab 切换:行业 / 概念 */}
<div className="max-w-5xl mx-auto px-4 mt-4">
<div className="flex gap-1 bg-muted rounded-lg p-1">
{TABS.map((t) => (
<button
key={t.key}
@@ -73,39 +80,21 @@ function SectorsPage() {
: "text-muted-foreground hover:text-foreground"
}`}
>
{t.label}
{t.label}
</button>
))}
</div>
<div className="flex gap-0.5 text-xs border rounded overflow-hidden shrink-0">
<button
onClick={() => setSort("mainNetInflow")}
className={`px-2 py-1 transition-colors ${
sort === "mainNetInflow"
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
</button>
<button
onClick={() => setSort("changePercent")}
className={`px-2 py-1 transition-colors ${
sort === "changePercent"
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
</button>
</div>
</div>
{/* 数据概览 */}
<div className="max-w-5xl mx-auto px-4 mt-2">
{/* 排序切换 */}
<div className="max-w-5xl mx-auto px-4 mt-3 flex items-center justify-between">
<p className="text-[10px] text-muted-foreground">
{sorted.length} · {sort === "mainNetInflow" ? "主力净流入" : "涨跌幅"}
{sorted.length} · {tab === "industry" ? "行业" : "概念"}
</p>
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
<SortButton label="资金流入" active={sortKey === "mainNetInflow"} asc={asc} onClick={() => toggleSort("mainNetInflow")} />
<SortButton label="涨幅%" active={sortKey === "changePercent"} asc={asc} onClick={() => toggleSort("changePercent")} />
</div>
</div>
{/* 卡片列表 */}
@@ -130,7 +119,31 @@ function SectorsPage() {
);
}
/* ── 工具函数 ── */
function SortButton({
label,
active,
asc,
onClick,
}: {
label: string;
active: boolean;
asc: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={`px-2.5 py-1 flex items-center gap-0.5 transition-colors ${
active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
}`}
>
{label}
{active && (asc ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
</button>
);
}
/* ── 数值格式化 ── */
function formatInflow(val: number | null | undefined): string {
if (val == null) return "--";
const abs = Math.abs(val);
@@ -202,11 +215,7 @@ function SectorBlock({ item }: { item: SectorItem }) {
change >= 0 ? "text-red-500" : "text-green-500"
}`}
>
{change >= 0 ? (
<TrendingUp className="h-3 w-3" />
) : (
<TrendingDown className="h-3 w-3" />
)}
{change >= 0 ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}
{change >= 0 ? "+" : ""}
{change.toFixed(2)}%
</span>