feat(selection): show stock industry and concept boards in detail panel

Add point-in-time stock membership lookup to the sector radar module
(GET /sector-radar/stocks/{ts_code}/membership) reading the existing
dc_index/dc_member snapshots, and surface industries plus concept chips
in the selection signal detail panel with graceful no-data hiding.
This commit is contained in:
yuxuanhui
2026-09-05 16:23:50 +08:00
parent 50cac3575a
commit 41a9b4eb9a
14 changed files with 825 additions and 5 deletions
@@ -1,5 +1,6 @@
import { lazy, Suspense } from "react"
import { Search } from "lucide-react"
import { useStockSectorMembership } from "@/features/sector-radar/api/sector-radar.query"
import { useSelectionChart } from "../api/selection.query"
import { Card } from "@/shared/ui/card"
@@ -18,6 +19,8 @@ const SelectionChart = lazy(() =>
})),
)
const displayedConceptCount = 8
interface SignalDetailPanelProps {
stock: SelectionStockResult | undefined
}
@@ -28,6 +31,10 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
stock?.target_trade_date,
stock?.strategy,
)
const membershipQuery = useStockSectorMembership(
stock?.ts_code,
stock?.target_trade_date,
)
if (!stock) {
return (
@@ -63,6 +70,19 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
},
]
const membership =
membershipQuery.data?.status === "success" ? membershipQuery.data : undefined
const industries = membership?.industries ?? []
const concepts = membership?.concepts ?? []
const extraConceptCount = Math.max(
0,
concepts.length - displayedConceptCount,
)
const sectorSnapshotTitle =
membership?.trade_date != null
? `板块成分截至 ${membership.trade_date}`
: undefined
return (
<aside
aria-label="当前股票详情"
@@ -117,6 +137,33 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
¥ {stock.close.toFixed(2)}
</p>
{industries.length > 0 && (
<p className="mt-1 text-xs text-muted-foreground">
行业:{industries.map((industry) => industry.name).join("、")}
</p>
)}
{concepts.length > 0 && (
<div
className="mt-2 flex flex-wrap items-center gap-1"
title={sectorSnapshotTitle}
>
<span className="text-xs text-muted-foreground">板块</span>
{concepts.slice(0, displayedConceptCount).map((concept) => (
<span
className="rounded bg-muted px-1.5 py-0.5 text-xs"
key={concept.code}
>
{concept.name}
</span>
))}
{extraConceptCount > 0 && (
<span className="text-xs text-muted-foreground">
等 {concepts.length} 个
</span>
)}
</div>
)}
<dl className="mt-3 divide-y divide-border/60 border-y border-border/60 text-xs">
{metricRows.map((row) => (
<div