style(sector_radar): compact detail dialog header
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<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,7 +166,16 @@ function RadarDetailBody({
|
||||
}
|
||||
return (
|
||||
<div className="min-w-0 space-y-5">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<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"
|
||||
@@ -147,9 +192,10 @@ function RadarDetailBody({
|
||||
>
|
||||
导出成员
|
||||
</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">
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
当日涨跌幅:
|
||||
@@ -164,6 +210,7 @@ function RadarDetailBody({
|
||||
: "暂无数据"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<section aria-label="三指标排名" className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{(["swing", "ratio", "amount"] as const).map((key) => (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user