import { AlertTriangle, Database, RefreshCw } from "lucide-react" import { useEffect } from "react" import { useNavigate, useSearch } from "@tanstack/react-router" import { PageLayout } from "@/app/layout/page-layout" import { useSectorRadarDates, useSectorRadarRankings, } from "@/features/sector-radar/api/sector-radar.query" import type { RadarMetricQuality, RadarMetricUnit, RadarPublication, RadarRankingRow, RadarRankingsQuery, SectorRadarRouteSearch, } from "@/features/sector-radar/api/sector-radar.types" import { Badge } from "@/shared/ui/badge" import { Button } from "@/shared/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/shared/ui/card" import { Input } from "@/shared/ui/input" import { Pagination } from "@/shared/ui/pagination" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/shared/ui/select" import { Skeleton } from "@/shared/ui/skeleton" const PAGE_SIZE_OPTIONS = [10, 20, 50] as const const sectorTypeOptions = [ { label: "概念板块", value: "concept" }, { label: "行业板块", value: "industry" }, ] as const const viewOptions = [ { label: "主力净额", value: "amount" }, { label: "单日资金率", value: "ratio" }, { label: "3—10 日波段资金率", value: "swing" }, { label: "排名变化", value: "rank_change" }, ] as const const metricOptions = [ { label: "主力净额", value: "amount" }, { label: "单日资金率", value: "ratio" }, { label: "3—10 日波段资金率", value: "swing" }, ] as const const sideOptions = [ { label: "全部板块", value: "all" }, { label: "强榜", value: "top" }, { label: "弱榜", value: "bottom" }, ] as const const rankChangeDayOptions = [1, 2, 3, 4, 5].map((value) => ({ label: `${value} 日变化`, value: String(value), })) export function SectorRadarPage() { const search = useSearch({ from: "/_workspace/sector-radar" }) const navigate = useNavigate({ from: "/sector-radar" }) const query: RadarRankingsQuery = { tradeDate: search.tradeDate, sectorType: search.sectorType, view: search.view, rankChangeMetric: search.rankChangeMetric, rankChangeDays: search.rankChangeDays, side: search.side, search: search.search || undefined, page: search.page, pageSize: search.pageSize, } const dates = useSectorRadarDates() const rankings = useSectorRadarRankings(query) const total = rankings.data?.total ?? 0 const pageCount = Math.max(1, Math.ceil(total / search.pageSize)) useEffect(() => { if (rankings.data?.status === "success" && search.page > pageCount) { void navigate({ search: (previous) => ({ ...previous, page: pageCount }), }) } }, [navigate, pageCount, rankings.data?.status, search.page]) function updateSearch(next: Partial) { void navigate({ search: (previous) => ({ ...previous, ...next }) }) } const loading = dates.isPending || rankings.isPending const refreshing = !loading && (Boolean(dates.isFetching) || Boolean(rankings.isFetching)) const fatalError = (dates.isError && !dates.data) || (rankings.isError && !rankings.data) const refreshFailed = !fatalError && (Boolean(dates.isError) || Boolean(rankings.isError)) const noData = dates.data?.status === "no_data" || rankings.data?.status === "no_data" return ( } mode="bounded-workspace" >
{loading ? : null} {!loading && fatalError ? ( { void dates.refetch() void rankings.refetch() }} /> ) : null} {!loading && !fatalError && noData ? : null} {!loading && !fatalError && !noData && dates.data && rankings.data?.status === "success" && rankings.data.publication ? ( <> {refreshFailed ? ( ) : refreshing ? ( ) : null} updateSearch({ page })} onPageSizeChange={(pageSize) => updateSearch({ page: 1, pageSize }) } page={Math.min(search.page, pageCount)} pageSize={search.pageSize} response={rankings.data} /> ) : null}
) } interface RadarFiltersProps { availableDates: string[] onChange: (next: Partial) => void search: SectorRadarRouteSearch } function RadarFilters({ availableDates, onChange, search }: RadarFiltersProps) { const dateOptions = [ { label: "最近有效交易日", value: "latest" }, ...availableDates.map((value) => ({ label: value, value })), ] return (
onChange({ page: 1, tradeDate: value === "latest" ? undefined : value, }) } /> onChange({ page: 1, sectorType: value as SectorRadarRouteSearch["sectorType"], }) } /> onChange({ page: 1, view: value as SectorRadarRouteSearch["view"] }) } /> {search.view === "rank_change" ? ( <> onChange({ page: 1, rankChangeMetric: value as SectorRadarRouteSearch["rankChangeMetric"], }) } /> onChange({ page: 1, rankChangeDays: Number(value) }) } /> ) : null} onChange({ page: 1, side: value as SectorRadarRouteSearch["side"] }) } />
) } function FilterSelect({ className, label, onValueChange, options, value, }: { className?: string label: string onValueChange: (value: string) => void options: readonly { label: string; value: string }[] value: string }) { return ( ) } function RadarStatusSummary({ currentAttempt, disclaimer, lastGood, metricVersion, publication, }: { currentAttempt: RadarPublication | null disclaimer: string lastGood: RadarPublication | null metricVersion: string publication: RadarPublication }) { const degradedAttempt = currentAttempt && currentAttempt.status !== "success" && currentAttempt.publication_id !== lastGood?.publication_id return (
收盘后板块资金排名 {disclaimer}
知行独立实现 {metricVersion}
{degradedAttempt ? (
{attemptStatusLabel(currentAttempt.status)}:目标交易日{" "} {currentAttempt.target_trade_date} ,当前仍展示最近有效发布{" "} {lastGood?.target_trade_date ?? publication.target_trade_date}。
) : null}
) } function SummaryItem({ label, value }: { label: string; value: string }) { return (

{label}

{value}

) } function RadarTable({ onPageChange, onPageSizeChange, page, pageSize, response, }: { onPageChange: (page: number) => void onPageSizeChange: (pageSize: number) => void page: number pageSize: number response: Exclude< ReturnType["data"], undefined > }) { const rankChangeView = response.view === "rank_change" return (
{rankChangeView ? ( ) : null} {response.rows.map((row) => ( ))} {response.rows.length === 0 ? ( ) : null}
{response.sector_type === "concept" ? "概念" : "行业"}板块资金排名
名次 板块 {rankChangeView ? `${response.rank_change_days} 日排名变化` : response.definition.label} 参考指标值 排名百分位 样本 资金覆盖率 质量
没有符合当前筛选条件的板块。
) } function RadarTableRow({ rankChangeView, row, }: { rankChangeView: boolean row: RadarRankingRow }) { return ( {row.rank_position ?? "—"}

{row.sector_name}

{row.sector_code}

{rankChangeView ? formatRankChange(row.rank_change) : formatMetricValue(row.metric_value, row.unit)} {rankChangeView ? ( {formatMetricValue(row.metric_value, row.unit)} ) : null} {row.rank_percentile === null ? "—" : `${formatNumber(row.rank_percentile, 2)}%`} {row.valid_sample_count}/{row.member_count} {formatCoverage(row.moneyflow_coverage)} ) } function QualityBadge({ quality }: { quality: RadarMetricQuality }) { if (quality === "available") return 可用 if (quality === "available_limited_sample") { return ( 样本有限 ) } return 不可用 } function RadarLoading() { return ( 正在加载板块资金雷达 正在读取最近有效发布和板块排名。 {Array.from({ length: 7 }, (_, index) => ( ))} ) } function RadarRefreshStatus() { return (
雷达数据正在刷新,当前继续展示上一次已读取的有效结果。
) } function RadarRefreshFailureStatus() { return (
雷达数据刷新失败,当前继续展示上一次已读取的有效结果。请稍后重试。
) } function RadarError({ onRetry }: { onRetry: () => void }) { return ( 请求或响应校验失败,可能是服务、网络或接口契约暂时异常。 ) } function RadarNoData() { return ( 完成第一次收盘后构建并通过完整性门槛后,这里会显示最近有效排名。 ) } function formatMetricValue(value: number | null, unit: RadarMetricUnit) { if (value === null) return "—" return unit === "CNY_100M" ? `${formatNumber(value, 2)} 亿元` : `${formatNumber(value * 100, 2)}%` } function formatRankChange(value: number | null) { if (value === null) return "暂无可比历史" if (value > 0) return `+${value}` return String(value) } function formatCoverage(value: number) { return `${formatNumber(value * 100, 1)}%` } function formatNumber(value: number, maximumFractionDigits: number) { return value.toLocaleString("zh-CN", { maximumFractionDigits }) } function formatDateTime(value: string | null) { if (!value) return "—" return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short", timeZone: "Asia/Shanghai", }).format(new Date(value)) } function attemptStatusLabel(status: RadarPublication["status"]) { if (status === "running") return "新一期仍在构建" if (status === "partial") return "新一期数据不完整" if (status === "failed") return "新一期构建失败" return "新一期已发布" }