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 { Button } from "@/shared/ui/button"
import { import {
Dialog, Dialog,
@@ -47,55 +47,91 @@ export function RadarDetailDialog({ row }: { row: RadarRankingRow }) {
{row.sector_name} {row.sector_name}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-[90dvh] grid-cols-1 overflow-y-auto sm:max-w-4xl"> <DialogContent className="max-h-[90dvh] grid-cols-1 overflow-y-auto sm:max-w-4xl">
<DialogTitle>{row.sector_name}</DialogTitle> {open ? <RadarDetail row={row} /> : null}
<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}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )
} }
function RadarDetail({ function RadarDetail({ row }: { row: RadarRankingRow }) {
query, const query: RadarSectorQuery = {
initialMetric, sectorType: row.sector_type,
}: { sectorCode: row.sector_code,
query: RadarSectorQuery tradeDate: row.trade_date,
initialMetric: RadarMetricKind }
}) {
const detail = useSectorRadarDetail(query) 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) if (detail.isError)
return ( return (
<div role="alert"> <>
板块详情加载失败。 <RadarDetailHeader row={row} />
<Button onClick={() => void detail.refetch()} variant="outline"> <div role="alert">
重试详情 板块详情加载失败。
</Button> <Button onClick={() => void detail.refetch()} variant="outline">
</div> 重试详情
</Button>
</div>
</>
) )
if (!detail.data || detail.data.status === "no_data") if (!detail.data || detail.data.status === "no_data")
return <p>暂无板块详情数据。</p> return (
return <RadarDetailBody data={detail.data} initialMetric={initialMetric} /> <>
<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({ function RadarDetailBody({
data, data,
initialMetric, initialMetric,
row,
}: { }: {
data: RadarDetailResponse data: RadarDetailResponse
initialMetric: RadarMetricKind initialMetric: RadarMetricKind
row: RadarRankingRow
}) { }) {
const [metric, setMetric] = useState<RadarMetricKind>(initialMetric) const [metric, setMetric] = useState<RadarMetricKind>(initialMetric)
const [memberMetric, setMemberMetric] = const [memberMetric, setMemberMetric] =
@@ -130,39 +166,50 @@ function RadarDetailBody({
} }
return ( return (
<div className="min-w-0 space-y-5"> <div className="min-w-0 space-y-5">
<div className="flex flex-wrap justify-end gap-2"> <div className="space-y-3">
<Button <RadarDetailHeader
variant="outline" row={row}
size="sm" actions={
disabled={!data.members.length} <>
onClick={() => void copyMembers()} {exportStatus ? (
> <p role="status" className="text-xs text-muted-foreground">
复制成员 {exportStatus}
</Button> </p>
<Button ) : null}
variant="outline" <Button
size="sm" variant="outline"
disabled={!data.members.length} size="sm"
onClick={exportMembers} disabled={!data.members.length}
> onClick={() => void copyMembers()}
导出成员 >
</Button> 复制成员
</div> </Button>
{exportStatus ? <p role="status">{exportStatus}</p> : null} <Button
<div className="flex flex-wrap gap-x-6 gap-y-2 rounded-md bg-muted/50 p-3"> variant="outline"
<p>实际交易日:{data.trade_date ?? "—"}</p> size="sm"
<p> disabled={!data.members.length}
当日涨跌幅: onClick={exportMembers}
<span className={radarValueTone(data.pct_change)}> >
{formatRadarValue(data.pct_change, "percent")} 导出成员
</span> </Button>
</p> </>
<p> }
领涨股: />
{data.leading_stock <div className="flex flex-wrap gap-x-6 gap-y-1 rounded-md bg-muted/50 px-3 py-2">
? `${data.leading_stock.name ?? ""} ${data.leading_stock.ts_code}` <p>实际交易日:{data.trade_date ?? "—"}</p>
: "暂无数据"} <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> </div>
<section aria-label="三指标排名" className="space-y-3"> <section aria-label="三指标排名" className="space-y-3">
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
@@ -142,6 +142,21 @@ describe("radar detail interactions", () => {
expect(detailQuery).not.toHaveBeenCalled() expect(detailQuery).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole("button", { name: "机器人" })) fireEvent.click(screen.getByRole("button", { name: "机器人" }))
const dialog = await screen.findByRole("dialog") 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({ expect(detailQuery).toHaveBeenCalledWith({
sectorType: "concept", sectorType: "concept",
sectorCode: "BK01", sectorCode: "BK01",