import { lazy, Suspense, useState } from "react"
import { Button } from "@/shared/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog"
import { useSectorRadarDetail } from "../api/sector-radar.query"
import type {
RadarDetailResponse,
RadarMemberMetric,
RadarMetricKind,
RadarRankingRow,
RadarSectorQuery,
} from "../api/sector-radar.types"
import {
formatRadarValue,
radarMembersCsv,
radarValueTone,
} from "./radar-format"
import { RadarHistoryGrid } from "./radar-history"
const RadarRankChart = lazy(() =>
import("./radar-rank-chart").then((module) => ({
default: module.RadarRankChart,
})),
)
const metricLabels = {
swing: "波段资金率",
ratio: "单日流入率",
amount: "单日净额",
} as const
const memberLabels = {
pct_change: "涨跌幅",
net_amount_yuan: "主力净额",
active_buy_net_amount_yuan: "主买净额",
} as const
/** Keep the trigger inside the dialog primitive so Escape and close restore its focus. */
export function RadarDetailDialog({ row }: { row: RadarRankingRow }) {
const [open, setOpen] = useState(false)
return (
)
}
function RadarDetail({
query,
initialMetric,
}: {
query: RadarSectorQuery
initialMetric: RadarMetricKind
}) {
const detail = useSectorRadarDetail(query)
if (detail.isPending) return
正在加载板块详情…
if (detail.isError)
return (
板块详情加载失败。
)
if (!detail.data || detail.data.status === "no_data")
return 暂无板块详情数据。
return
}
function RadarDetailBody({
data,
initialMetric,
}: {
data: RadarDetailResponse
initialMetric: RadarMetricKind
}) {
const [metric, setMetric] = useState(initialMetric)
const [memberMetric, setMemberMetric] =
useState("pct_change")
const [memberSide, setMemberSide] = useState<"top" | "bottom">("top")
const [exportStatus, setExportStatus] = useState("")
const leaders = data.leaders[memberMetric]
async function copyMembers() {
try {
await navigator.clipboard.writeText(
data.members
.map((member) => `${member.ts_code}\t${member.name}`)
.join("\n"),
)
setExportStatus(`已复制 ${data.members.length} 只成员。`)
} catch {
setExportStatus("复制失败,请使用导出成员。")
}
}
function exportMembers() {
const url = URL.createObjectURL(
new Blob(["\ufeff", radarMembersCsv(data.members)], {
type: "text/csv;charset=utf-8",
}),
)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = `${data.sector_code}-${data.trade_date}-members.csv`
anchor.click()
URL.revokeObjectURL(url)
setExportStatus(`已导出 ${data.members.length} 只成员。`)
}
return (
{exportStatus ?
{exportStatus}
: null}
实际交易日:{data.trade_date ?? "—"}
当日涨跌幅:
{formatRadarValue(data.pct_change, "percent")}
领涨股:
{data.leading_stock
? `${data.leading_stock.name ?? ""} ${data.leading_stock.ts_code}`
: "暂无数据"}
{(["swing", "ratio", "amount"] as const).map((key) => (
))}
{data.history.points.some(
(point) =>
!point[metric].missing && point[metric].rank_position !== null,
) ? (
正在加载排名曲线…}>
) : (
暂无{metricLabels[metric]}排名轨迹,当前历史记录均缺失。
)}
成分强弱
{(Object.keys(memberLabels) as RadarMemberMetric[]).map((key) => (
))}
{(["top", "bottom"] as const).map((side) => (
))}
{[memberSide].map((side) => (
{side === "top" ? "前 5 名" : "后 5 名"}
{leaders[side].length ? (
{leaders[side].map((member) => (
-
{member.name}{" "}
{member.ts_code}
{formatRadarValue(
member[memberMetric],
memberMetric === "pct_change" ? "percent" : "yuan",
)}
))}
) : (
暂无有效数据
)}
))}
仅展示有该指标的成员,不足 5
只按实际数量显示。主买净额与主力净额为不同来源指标。
相似板块
本系统口径:同日可确认成员交集 / 并集(Jaccard 重合度)。
{data.similar_sectors.length ? (
{data.similar_sectors.map((sector) => (
{sector.sector_name}{" "}
{sector.sector_type === "concept" ? "概念" : "行业"}
{(sector.overlap_ratio * 100).toFixed(1)}%{" "}
({sector.intersection_count}/{sector.union_count})
))}
) : (
暂无相似板块数据
)}
板块成员({data.members.length})
{data.members.length
? data.members
.map((member) => `${member.name} (${member.ts_code})`)
.join("、")
: "暂无成员数据"}
)
}