2026-09-01 13:37:19 +08:00
|
|
|
import { lazy, Suspense } from "react"
|
2026-09-02 18:26:57 +08:00
|
|
|
import { Search } from "lucide-react"
|
2026-09-01 13:37:19 +08:00
|
|
|
import { useSelectionChart } from "../api/selection.query"
|
2026-08-10 10:07:55 +08:00
|
|
|
|
|
|
|
|
import { Card } from "@/shared/ui/card"
|
|
|
|
|
|
2026-08-28 11:39:05 +08:00
|
|
|
import type { SelectionStockResult } from "../api/selection.types"
|
2026-08-31 16:14:16 +08:00
|
|
|
import { PatternScoreDetails } from "./pattern-score"
|
2026-09-01 13:37:19 +08:00
|
|
|
import { PatternCaseImage } from "./pattern-case-image"
|
2026-09-05 10:22:08 +08:00
|
|
|
import {
|
|
|
|
|
getSelectionStrategyLabel,
|
|
|
|
|
getStockBasicMetrics,
|
|
|
|
|
} from "./selection-presentation"
|
2026-09-01 13:37:19 +08:00
|
|
|
|
|
|
|
|
const SelectionChart = lazy(() =>
|
|
|
|
|
import("./selection-chart").then((module) => ({
|
|
|
|
|
default: module.SelectionChart,
|
|
|
|
|
})),
|
|
|
|
|
)
|
2026-08-10 10:07:55 +08:00
|
|
|
|
|
|
|
|
interface SignalDetailPanelProps {
|
2026-08-28 11:39:05 +08:00
|
|
|
stock: SelectionStockResult | undefined
|
2026-08-10 10:07:55 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 11:39:05 +08:00
|
|
|
export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
|
2026-09-05 13:41:25 +08:00
|
|
|
const chartQuery = useSelectionChart(
|
|
|
|
|
stock?.ts_code,
|
|
|
|
|
stock?.target_trade_date,
|
|
|
|
|
stock?.strategy,
|
|
|
|
|
)
|
2026-09-01 13:37:19 +08:00
|
|
|
|
2026-08-28 11:39:05 +08:00
|
|
|
if (!stock) {
|
2026-08-10 10:07:55 +08:00
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
aria-label="当前股票详情"
|
2026-08-10 16:58:16 +08:00
|
|
|
className="hidden min-h-0 overflow-auto rounded-md border-border/80 bg-card p-3 md:flex md:flex-col md:justify-center"
|
2026-08-10 10:07:55 +08:00
|
|
|
>
|
|
|
|
|
<div className="space-y-2 text-center">
|
|
|
|
|
<Search
|
|
|
|
|
className="mx-auto size-5 text-muted-foreground"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
2026-08-28 11:39:05 +08:00
|
|
|
<p className="font-medium">暂无选中股票</p>
|
2026-08-10 10:07:55 +08:00
|
|
|
<p className="text-xs leading-5 text-muted-foreground">
|
2026-08-28 11:39:05 +08:00
|
|
|
从左侧列表选择一只股票查看详情。
|
2026-08-10 10:07:55 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 18:26:57 +08:00
|
|
|
const metricRows = [
|
|
|
|
|
...getStockBasicMetrics(stock),
|
|
|
|
|
{
|
|
|
|
|
key: "target_trade_date",
|
|
|
|
|
label: "目标交易日",
|
|
|
|
|
value: stock.target_trade_date,
|
|
|
|
|
},
|
2026-09-05 10:22:08 +08:00
|
|
|
{
|
|
|
|
|
key: "strategy",
|
|
|
|
|
label: "策略",
|
|
|
|
|
value: getSelectionStrategyLabel(stock.strategy),
|
|
|
|
|
},
|
2026-09-02 18:26:57 +08:00
|
|
|
]
|
|
|
|
|
|
2026-08-10 10:07:55 +08:00
|
|
|
return (
|
|
|
|
|
<aside
|
|
|
|
|
aria-label="当前股票详情"
|
2026-09-02 18:26:57 +08:00
|
|
|
className="hidden min-h-0 overflow-hidden rounded-md border border-border/80 bg-card md:flex"
|
2026-08-10 10:07:55 +08:00
|
|
|
>
|
2026-09-02 18:26:57 +08:00
|
|
|
<section
|
|
|
|
|
aria-labelledby="technical-chart"
|
|
|
|
|
className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden p-3"
|
|
|
|
|
>
|
2026-09-01 13:37:19 +08:00
|
|
|
<h3
|
2026-09-02 18:26:57 +08:00
|
|
|
className="mb-2 shrink-0 text-xs font-medium text-muted-foreground"
|
2026-09-01 13:37:19 +08:00
|
|
|
id="technical-chart"
|
|
|
|
|
>
|
|
|
|
|
技术图表
|
|
|
|
|
</h3>
|
2026-09-02 18:26:57 +08:00
|
|
|
<div className="min-h-0 flex-1">
|
|
|
|
|
{chartQuery.isPending ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground">正在加载行情图表…</p>
|
|
|
|
|
) : chartQuery.isError ? (
|
|
|
|
|
<p className="text-xs text-destructive">行情图表暂时不可用。</p>
|
|
|
|
|
) : chartQuery.data?.points.length ? (
|
|
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
正在加载图表组件…
|
|
|
|
|
</p>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectionChart chart={chartQuery.data} />
|
|
|
|
|
</Suspense>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-muted-foreground">暂无可用行情数据。</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-09-01 13:37:19 +08:00
|
|
|
</section>
|
|
|
|
|
|
2026-09-02 18:26:57 +08:00
|
|
|
<section
|
|
|
|
|
aria-labelledby="selected-stock-heading"
|
2026-09-02 18:50:13 +08:00
|
|
|
className="flex w-65 shrink-0 flex-col overflow-hidden border-l border-border/60 p-3"
|
2026-09-02 18:26:57 +08:00
|
|
|
>
|
2026-09-02 18:50:13 +08:00
|
|
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
|
|
|
<h2
|
|
|
|
|
className="truncate text-base font-semibold tracking-tight"
|
|
|
|
|
id="selected-stock-heading"
|
|
|
|
|
>
|
|
|
|
|
{stock.name}
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="font-mono text-xs text-muted-foreground">
|
|
|
|
|
{stock.ts_code}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-3 text-2xl font-semibold tabular-nums tracking-tight">
|
|
|
|
|
¥ {stock.close.toFixed(2)}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<dl className="mt-3 divide-y divide-border/60 border-y border-border/60 text-xs">
|
|
|
|
|
{metricRows.map((row) => (
|
|
|
|
|
<div
|
|
|
|
|
key={row.key}
|
|
|
|
|
className="flex items-center justify-between gap-2 py-2"
|
|
|
|
|
>
|
|
|
|
|
<dt className="shrink-0 text-muted-foreground">{row.label}</dt>
|
|
|
|
|
<dd className="min-w-0 truncate text-right font-medium tabular-nums">
|
|
|
|
|
{row.value}
|
|
|
|
|
</dd>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</dl>
|
2026-09-02 18:26:57 +08:00
|
|
|
|
2026-09-02 18:50:13 +08:00
|
|
|
<section className="mt-3" aria-labelledby="pattern-score-details">
|
|
|
|
|
<h3
|
|
|
|
|
className="mb-2 text-xs font-medium text-muted-foreground"
|
|
|
|
|
id="pattern-score-details"
|
2026-09-02 18:26:57 +08:00
|
|
|
>
|
2026-09-02 18:50:13 +08:00
|
|
|
图形相似度评分
|
|
|
|
|
</h3>
|
|
|
|
|
<PatternScoreDetails score={stock.score} />
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
2026-09-02 18:26:57 +08:00
|
|
|
|
2026-09-02 18:50:13 +08:00
|
|
|
<PatternCaseImage key={stock.ts_code} score={stock.score} />
|
2026-08-27 13:17:51 +08:00
|
|
|
</section>
|
2026-08-10 10:07:55 +08:00
|
|
|
</aside>
|
|
|
|
|
)
|
|
|
|
|
}
|