feat(web): 用紧凑卡片替换选股表格并精简资金雷达列

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuxuanhui
2026-09-01 14:57:27 +08:00
parent 4b8a7bd484
commit c944b219d9
12 changed files with 281 additions and 293 deletions
@@ -175,8 +175,22 @@ describe("SectorRadarPage", () => {
expect(screen.getAllByText("+12.5 亿元")[0]).toBeInTheDocument()
expect(screen.getAllByText("机器人")[0]).toBeInTheDocument()
expect(screen.getAllByText("样本有限")[0]).toBeInTheDocument()
expect(screen.getAllByText("95%")[0]).toBeInTheDocument()
expect(screen.getAllByText("19/20")[0]).toBeInTheDocument()
expect(screen.getAllByText("100%")[0]).toBeInTheDocument()
expect(
screen.getAllByRole("columnheader", { name: "排名百分位" }),
).toHaveLength(2)
expect(screen.getAllByRole("columnheader", { name: "样本" })).toHaveLength(
2,
)
expect(
screen.queryByRole("columnheader", { name: "资金覆盖率" }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("columnheader", { name: "质量" }),
).not.toBeInTheDocument()
expect(screen.queryByText("样本有限")).not.toBeInTheDocument()
expect(screen.queryByText("95%")).not.toBeInTheDocument()
expect(screen.queryByText("收盘后板块资金排名")).not.toBeInTheDocument()
expect(
screen.queryByText("知行独立实现,非 OneChartLab 原站公式。"),
@@ -9,7 +9,6 @@ import {
} from "@/features/sector-radar/api/sector-radar.query"
import type {
RadarMetricKind,
RadarMetricQuality,
RadarMetricUnit,
RadarPublication,
RadarRankingRow,
@@ -18,7 +17,6 @@ import type {
RadarView,
SectorRadarRouteSearch,
} from "@/features/sector-radar/api/sector-radar.types"
import { Badge } from "@/shared/ui/badge"
import { Button } from "@/shared/ui/button"
import {
Card,
@@ -398,83 +396,71 @@ function RadarTable({
role="region"
tabIndex={0}
>
<table className="w-full min-w-[1280px] border-collapse text-sm">
<table className="w-full min-w-[960px] border-collapse text-sm">
<caption className="sr-only">
{response.sector_type === "concept" ? "概念" : "行业"}板块资金双榜
</caption>
<colgroup>
{Array.from({ length: 12 }, (_, i) => (
{Array.from({ length: 8 }, (_, i) => (
<col
key={i}
className={i === 5 || i === 6 ? "w-[120px]" : "w-[100px]"}
className={i === 3 || i === 4 ? "w-[120px]" : "w-[100px]"}
/>
))}
</colgroup>
<thead className="sticky top-0 z-10 bg-muted/95 text-xs text-muted-foreground backdrop-blur">
<tr>
<th
className="px-3 py-2 text-left font-medium"
colSpan={5}
className="px-3 py-1 text-left font-medium"
colSpan={3}
scope="colgroup"
>
资金进攻榜 TOP 10%
</th>
<th
className="border-x border-border px-3 py-2 text-center font-medium"
className="border-x border-border px-3 py-1 text-center font-medium"
colSpan={2}
scope="colgroup"
>
{rankChangeView ? "排名变化" : response.definition.label}
</th>
<th
className="px-3 py-2 text-right font-medium"
colSpan={5}
className="px-3 py-1 text-right font-medium"
colSpan={3}
scope="colgroup"
>
BOTTOM 10% 资金撤离榜
</th>
</tr>
<tr>
<th className="px-3 py-2 text-left font-medium" scope="col">
<th className="px-3 py-1 text-left font-medium" scope="col">
排名 / 板块
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
<th className="px-3 py-1 text-right font-medium" scope="col">
排名百分位
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
<th className="px-3 py-1 text-right font-medium" scope="col">
样本
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
资金覆盖率
</th>
<th className="px-3 py-2 text-left font-medium" scope="col">
质量
</th>
<th
className="border-l border-border px-3 py-2 text-right font-medium"
className="border-l border-border px-3 py-1 text-right font-medium"
scope="col"
>
{rankChangeView ? "排名上升" : "流入"}
</th>
<th
className="border-r border-border px-3 py-2 text-left font-medium"
className="border-r border-border px-3 py-1 text-left font-medium"
scope="col"
>
{rankChangeView ? "排名下降" : "流出"}
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
质量
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
资金覆盖率
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
<th className="px-3 py-1 text-right font-medium" scope="col">
样本
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
<th className="px-3 py-1 text-right font-medium" scope="col">
排名百分位
</th>
<th className="px-3 py-2 text-right font-medium" scope="col">
<th className="px-3 py-1 text-right font-medium" scope="col">
排名 / 板块
</th>
</tr>
@@ -491,8 +477,8 @@ function RadarTable({
{rowCount === 0 ? (
<tr>
<td
className="px-3 py-12 text-center text-muted-foreground"
colSpan={12}
className="px-3 py-8 text-center text-muted-foreground"
colSpan={8}
>
没有符合当前筛选条件的板块。
</td>
@@ -582,42 +568,49 @@ function RankCells({
row?: RadarRankingRow
align: "left" | "right"
}) {
if (!row)
const rankClass = align === "right" ? "text-right" : "text-left"
const rankCell = row ? (
<td className={`px-3 py-1.5 ${rankClass} font-semibold tabular-nums`}>
{row.rank_position ?? "—"}
<span className="mx-0.5 font-normal text-muted-foreground">/</span>
<span className="font-medium text-foreground">{row.sector_name}</span>
<span className="ml-0.5 text-[11px] font-normal text-muted-foreground">
{row.sector_code}
</span>
</td>
) : (
<td className="px-3 py-1.5 text-muted-foreground">—</td>
)
const percentileCell = row ? (
<td className="px-3 py-1.5 text-right tabular-nums">
{row.rank_percentile === null
? "—"
: `${formatNumber(row.rank_percentile, 2)}%`}
</td>
) : (
<td />
)
const sampleCell = row ? (
<td className="px-3 py-1.5 text-right tabular-nums">
{row.valid_sample_count}/{row.member_count}
</td>
) : (
<td />
)
if (align === "right") {
return (
<>
<td className="px-3 py-2.5 text-muted-foreground">—</td>
<td />
<td />
<td />
<td />
{sampleCell}
{percentileCell}
{rankCell}
</>
)
}
return (
<>
<td
className={`px-3 py-2.5 ${align === "right" ? "text-right" : "text-left"} font-semibold tabular-nums`}
>
{row.rank_position ?? "—"}
<span className="mx-1 font-normal text-muted-foreground">/</span>
<span className="font-medium text-foreground">{row.sector_name}</span>
<span className="ml-1 text-xs font-normal text-muted-foreground">
{row.sector_code}
</span>
</td>
<td className="px-3 py-2.5 text-right tabular-nums">
{row.rank_percentile === null
? "—"
: `${formatNumber(row.rank_percentile, 2)}%`}
</td>
<td className="px-3 py-2.5 text-right tabular-nums">
{row.valid_sample_count}/{row.member_count}
</td>
<td className="px-3 py-2.5 text-right tabular-nums">
{formatCoverage(row.moneyflow_coverage)}
</td>
<td className="px-3 py-2.5">
<QualityBadge quality={row.quality} />
</td>
{rankCell}
{percentileCell}
{sampleCell}
</>
)
}
@@ -652,7 +645,7 @@ function MetricCell({
: "text-foreground bg-muted/30"
return (
<td
className={`${borderClass} border-border px-3 py-2.5 ${direction === "in" ? "text-right" : "text-left"} font-medium tabular-nums ${toneClass}`}
className={`${borderClass} border-border px-3 py-1.5 ${direction === "in" ? "text-right" : "text-left"} font-medium tabular-nums ${toneClass}`}
>
{value}
</td>
@@ -673,19 +666,6 @@ function formatDirectionalMetric(
return `${direction === "in" ? "+" : "−"}${formatted}`
}
function QualityBadge({ quality }: { quality: RadarMetricQuality }) {
if (quality === "available") return <Badge variant="outline">可用</Badge>
if (quality === "available_limited_sample")
return (
<Badge
className="border-amber-500/40 text-amber-700 dark:text-amber-200"
variant="outline"
>
样本有限
</Badge>
)
return <Badge variant="destructive">不可用</Badge>
}
function RadarLoading() {
return (
<Card aria-label="正在加载板块资金雷达" className="flex-1 shadow-none">
@@ -768,9 +748,6 @@ function formatRankChange(value: number | null) {
if (value < 0) return `${value} 名`
return "0 名"
}
function formatCoverage(value: number) {
return `${formatNumber(value * 100, 1)}%`
}
function formatNumber(value: number, maximumFractionDigits: number) {
return value.toLocaleString("zh-CN", { maximumFractionDigits })
}