style(sector_radar): compact detail dialog header

This commit is contained in:
yuxuanhui
2026-09-06 20:25:39 +08:00
parent 07c5b25043
commit ca3bd7a4b1
2 changed files with 127 additions and 65 deletions
@@ -1,4 +1,4 @@
import { lazy, Suspense, useState } from "react"
import { lazy, Suspense, useState, type ReactNode } from "react"
import { Button } from "@/shared/ui/button"
import {
Dialog,
@@ -47,55 +47,91 @@ export function RadarDetailDialog({ row }: { row: RadarRankingRow }) {
{row.sector_name}
</DialogTrigger>
<DialogContent className="max-h-[90dvh] grid-cols-1 overflow-y-auto sm:max-w-4xl">
<DialogTitle>{row.sector_name}</DialogTitle>
<DialogDescription>
{row.sector_type === "concept" ? "概念" : "行业"} · {row.sector_code}{" "}
· {row.trade_date}
</DialogDescription>
{open ? (
<RadarDetail
initialMetric={row.metric_kind}
query={{
sectorType: row.sector_type,
sectorCode: row.sector_code,
tradeDate: row.trade_date,
}}
/>
) : null}
{open ? <RadarDetail row={row} /> : null}
</DialogContent>
</Dialog>
)
}
function RadarDetail({
query,
initialMetric,
}: {
query: RadarSectorQuery
initialMetric: RadarMetricKind
}) {
function RadarDetail({ row }: { row: RadarRankingRow }) {
const query: RadarSectorQuery = {
sectorType: row.sector_type,
sectorCode: row.sector_code,
tradeDate: row.trade_date,
}
const detail = useSectorRadarDetail(query)
if (detail.isPending) return <p role="status">正在加载板块详情…</p>
if (detail.isPending)
return (
<>
<RadarDetailHeader row={row} />
<p role="status">正在加载板块详情…</p>
</>
)
if (detail.isError)
return (
<div role="alert">
板块详情加载失败。
<Button onClick={() => void detail.refetch()} variant="outline">
重试详情
</Button>
</div>
<>
<RadarDetailHeader row={row} />
<div role="alert">
板块详情加载失败。
<Button onClick={() => void detail.refetch()} variant="outline">
重试详情
</Button>
</div>
</>
)
if (!detail.data || detail.data.status === "no_data")
return <p>暂无板块详情数据。</p>
return <RadarDetailBody data={detail.data} initialMetric={initialMetric} />
return (
<>
<RadarDetailHeader row={row} />
<p>暂无板块详情数据。</p>
</>
)
return (
<RadarDetailBody
data={detail.data}
initialMetric={row.metric_kind}
row={row}
/>
)
}
function RadarDetailHeader({
row,
actions,
}: {
row: RadarRankingRow
actions?: ReactNode
}) {
return (
<div
role="group"
aria-label="板块详情标题栏"
className="flex min-w-0 flex-col gap-2 pr-8 sm:flex-row sm:items-center"
>
<div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
<DialogTitle className="shrink-0">{row.sector_name}</DialogTitle>
<DialogDescription className="whitespace-nowrap">
{row.sector_type === "concept" ? "概念" : "行业"} · {row.sector_code}{" "}
· {row.trade_date}
</DialogDescription>
</div>
{actions ? (
<div className="flex shrink-0 items-center gap-2 sm:ml-auto">
{actions}
</div>
) : null}
</div>
)
}
function RadarDetailBody({
data,
initialMetric,
row,
}: {
data: RadarDetailResponse
initialMetric: RadarMetricKind
row: RadarRankingRow
}) {
const [metric, setMetric] = useState<RadarMetricKind>(initialMetric)
const [memberMetric, setMemberMetric] =
@@ -130,39 +166,50 @@ function RadarDetailBody({
}
return (
<div className="min-w-0 space-y-5">
<div className="flex flex-wrap justify-end gap-2">
<Button
variant="outline"
size="sm"
disabled={!data.members.length}
onClick={() => void copyMembers()}
>
复制成员
</Button>
<Button
variant="outline"
size="sm"
disabled={!data.members.length}
onClick={exportMembers}
>
导出成员
</Button>
</div>
{exportStatus ? <p role="status">{exportStatus}</p> : null}
<div className="flex flex-wrap gap-x-6 gap-y-2 rounded-md bg-muted/50 p-3">
<p>实际交易日:{data.trade_date ?? "—"}</p>
<p>
当日涨跌幅:
<span className={radarValueTone(data.pct_change)}>
{formatRadarValue(data.pct_change, "percent")}
</span>
</p>
<p>
领涨股:
{data.leading_stock
? `${data.leading_stock.name ?? ""} ${data.leading_stock.ts_code}`
: "暂无数据"}
</p>
<div className="space-y-3">
<RadarDetailHeader
row={row}
actions={
<>
{exportStatus ? (
<p role="status" className="text-xs text-muted-foreground">
{exportStatus}
</p>
) : null}
<Button
variant="outline"
size="sm"
disabled={!data.members.length}
onClick={() => void copyMembers()}
>
复制成员
</Button>
<Button
variant="outline"
size="sm"
disabled={!data.members.length}
onClick={exportMembers}
>
导出成员
</Button>
</>
}
/>
<div className="flex flex-wrap gap-x-6 gap-y-1 rounded-md bg-muted/50 px-3 py-2">
<p>实际交易日:{data.trade_date ?? "—"}</p>
<p>
当日涨跌幅:
<span className={radarValueTone(data.pct_change)}>
{formatRadarValue(data.pct_change, "percent")}
</span>
</p>
<p>
领涨股:
{data.leading_stock
? `${data.leading_stock.name ?? ""} ${data.leading_stock.ts_code}`
: "暂无数据"}
</p>
</div>
</div>
<section aria-label="三指标排名" className="space-y-3">
<div className="grid grid-cols-3 gap-2">
@@ -142,6 +142,21 @@ describe("radar detail interactions", () => {
expect(detailQuery).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole("button", { name: "机器人" }))
const dialog = await screen.findByRole("dialog")
const compactHeader = within(dialog).getByRole("group", {
name: "板块详情标题栏",
})
expect(
within(compactHeader).getByRole("heading", { name: "机器人" }),
).toBeInTheDocument()
expect(
within(compactHeader).getByText(/概念 · BK01 · 2026-08-28/),
).toBeInTheDocument()
expect(
within(compactHeader).getByRole("button", { name: "复制成员" }),
).toBeInTheDocument()
expect(
within(compactHeader).getByRole("button", { name: "导出成员" }),
).toBeInTheDocument()
expect(detailQuery).toHaveBeenCalledWith({
sectorType: "concept",
sectorCode: "BK01",