feat(web): 用紧凑卡片替换选股表格并精简资金雷达列
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user