+ import("./selection-chart").then((module) => ({
+ default: module.SelectionChart,
+ })),
+)
interface SignalDetailPanelProps {
stock: SelectionStockResult | undefined
}
export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
+ const chartQuery = useSelectionChart(stock?.ts_code, stock?.target_trade_date)
+
if (!stock) {
return (
+
+
+ 技术图表
+
+ {chartQuery.isPending ? (
+ 正在加载行情图表…
+ ) : chartQuery.isError ? (
+ 行情图表暂时不可用。
+ ) : chartQuery.data?.points.length ? (
+ 正在加载图表组件…
+ }
+ >
+
+
+ ) : (
+ 暂无可用行情数据。
+ )}
+
+
+
+
vi.fn())
const useSelectionResults = vi.fn()
+const useSelectionChart = vi.fn()
const useSelectionRun = vi.fn()
const useTriggerSelectionRun = vi.fn()
const mutate = vi.fn()
@@ -23,6 +24,7 @@ const reset = vi.fn()
vi.mock("@/features/selection/api/selection.query", () => ({
useSelectionResults: (...args: unknown[]) => useSelectionResults(...args),
+ useSelectionChart: (...args: unknown[]) => useSelectionChart(...args),
useSelectionRun: (...args: unknown[]) => useSelectionRun(...args),
useTriggerSelectionRun: () => useTriggerSelectionRun(),
}))
@@ -110,6 +112,11 @@ const selectedResult: SelectionResults = {
describe("SelectionResultsPage", () => {
beforeEach(() => {
+ useSelectionChart.mockReturnValue({
+ data: undefined,
+ isError: false,
+ isPending: false,
+ })
useSelectionResults.mockReturnValue({
data: selectedResult,
isError: false,
@@ -167,12 +174,23 @@ describe("SelectionResultsPage", () => {
})
expect(within(detailPanel).getByText("关键指标")).toBeInTheDocument()
expect(within(detailPanel).getByText("图形相似度评分")).toBeInTheDocument()
+ expect(
+ within(detailPanel).getByText("暂无可用行情数据。"),
+ ).toBeInTheDocument()
expect(
within(detailPanel).getByText("最佳案例:华纳药厂"),
).toBeInTheDocument()
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
expect(within(detailPanel).getAllByText("J 值")).toHaveLength(2)
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
+ const caseImages = within(detailPanel).getAllByRole("img", {
+ name: "华纳药厂 B1 参考案例图",
+ })
+ expect(caseImages).toHaveLength(1)
+ expect(caseImages[0]).toHaveAttribute(
+ "src",
+ "/patterns/b1-cases/case_001_688799_华纳药厂_2025-05-12.jpg",
+ )
})
it.each<{
@@ -184,14 +202,23 @@ describe("SelectionResultsPage", () => {
score: null,
},
{
- expected: "未匹配到 60.00 分以上案例。",
+ expected: "低于匹配阈值",
score: {
- breakdown: null,
- case: null,
- reason: "未匹配到评分阈值以上案例",
+ breakdown: {
+ kdj_state: 42,
+ price_shape: 44,
+ trend_structure: 40,
+ volume_pattern: 43,
+ },
+ case: {
+ breakout_date: "2025-07-23",
+ id: "case_004",
+ name: "方正科技",
+ },
+ reason: null,
status: "below_threshold",
threshold: 60,
- value: null,
+ value: 42.5,
version: "zhixing_b1_pattern_fastdtw_v1",
},
},
@@ -225,9 +252,51 @@ describe("SelectionResultsPage", () => {
const table = screen.getByRole("table")
expect(within(table).getByText("原始 B1")).toBeInTheDocument()
expect(within(table).getByText("12")).toBeInTheDocument()
+ if (score?.status === "below_threshold") {
+ const detailPanel = screen.getByRole("complementary", {
+ name: "当前股票详情",
+ })
+ const images = within(detailPanel).getAllByRole("img", {
+ name: "方正科技 B1 参考案例图",
+ })
+ expect(images).toHaveLength(1)
+ expect(images[0]).toHaveAttribute(
+ "src",
+ "/patterns/b1-cases/case_004_600601_方正科技_2025-07-23.jpg",
+ )
+ expect(within(detailPanel).getAllByText("42.50 分")).toHaveLength(2)
+ } else {
+ const detailPanel = screen.getByRole("complementary", {
+ name: "当前股票详情",
+ })
+ expect(within(detailPanel).queryByRole("img")).not.toBeInTheDocument()
+ }
},
)
+ it("shows chart loading and error states for the selected stock", () => {
+ useSelectionChart.mockReturnValue({
+ data: undefined,
+ isError: false,
+ isPending: true,
+ })
+ const view = render()
+
+ expect(screen.getByText("正在加载行情图表…")).toBeInTheDocument()
+ expect(useSelectionChart).toHaveBeenLastCalledWith(
+ "000001.SZ",
+ "2026-08-08",
+ )
+
+ useSelectionChart.mockReturnValue({
+ data: undefined,
+ isError: true,
+ isPending: false,
+ })
+ view.rerender()
+ expect(screen.getByText("行情图表暂时不可用。")).toBeInTheDocument()
+ })
+
it("opens execution status details in a drawer and restores trigger focus", async () => {
useSelectionResults.mockReturnValue({
data: {