@@ -86,7 +91,16 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
>
关键指标
-
+
+ {stock.signals.map((signal) => (
+
+
+ {getCategoryLabel(signal.category)}
+
+
+
+ ))}
+
)
diff --git a/zhixing-web/src/features/selection/components/signal-record-list.tsx b/zhixing-web/src/features/selection/components/signal-record-list.tsx
index 3a2627a..351c38b 100644
--- a/zhixing-web/src/features/selection/components/signal-record-list.tsx
+++ b/zhixing-web/src/features/selection/components/signal-record-list.tsx
@@ -3,29 +3,29 @@ import { ChevronDown, ChevronUp } from "lucide-react"
import { Badge } from "@/shared/ui/badge"
import { Button } from "@/shared/ui/button"
-import type { SelectionSignal } from "../api/selection.types"
+import type { SelectionStockResult } from "../api/selection.types"
import {
categoryToneClass,
getCategoryLabel,
getSignalJValue,
- getSignalKey,
+ getStockKey,
} from "./selection-presentation"
import { SignalDetails } from "./signal-details"
interface SignalRecordListProps {
- signals: SelectionSignal[]
+ stocks: SelectionStockResult[]
expandedKeys: ReadonlySet
- onSelect: (signal: SelectionSignal) => void
- onToggleExpanded: (signal: SelectionSignal) => void
+ onSelect: (stock: SelectionStockResult) => void
+ onToggleExpanded: (stock: SelectionStockResult) => void
}
export function SignalRecordList({
- signals,
+ stocks,
expandedKeys,
onSelect,
onToggleExpanded,
}: SignalRecordListProps) {
- if (signals.length === 0) {
+ if (stocks.length === 0) {
return (
没有符合当前筛选条件的信号。
@@ -35,46 +35,53 @@ export function SignalRecordList({
return (
- {signals.map((signal) => {
- const key = getSignalKey(signal)
+ {stocks.map((stock) => {
+ const key = getStockKey(stock)
const expanded = expandedKeys.has(key)
return (
-
- {getCategoryLabel(signal.category)}
-
+
+ {stock.signals.map((signal) => (
+
+ {getCategoryLabel(signal.category)}
+
+ ))}
+
J
- {getSignalJValue(signal.details)}
+ {stock.signals
+ .map((signal) => getSignalJValue(signal.details))
+ .join(" / ")}
{expanded ? (
-
+
+ {stock.signals.map((signal) => (
+
+
+ {getCategoryLabel(signal.category)}
+
+
+
+ ))}
+
) : null}
)
diff --git a/zhixing-web/src/features/selection/components/signal-table.tsx b/zhixing-web/src/features/selection/components/signal-table.tsx
index 856c004..267dab7 100644
--- a/zhixing-web/src/features/selection/components/signal-table.tsx
+++ b/zhixing-web/src/features/selection/components/signal-table.tsx
@@ -2,32 +2,32 @@ import type { KeyboardEvent } from "react"
import { Badge } from "@/shared/ui/badge"
-import type { SelectionSignal } from "../api/selection.types"
+import type { SelectionStockResult } from "../api/selection.types"
import {
categoryToneClass,
getCategoryLabel,
getSignalJValue,
- getSignalKey,
+ getStockKey,
} from "./selection-presentation"
interface SignalTableProps {
- signals: SelectionSignal[]
+ stocks: SelectionStockResult[]
selectedKey: string | null
- onSelect: (signal: SelectionSignal) => void
+ onSelect: (stock: SelectionStockResult) => void
}
export function SignalTable({
- signals,
+ stocks,
selectedKey,
onSelect,
}: SignalTableProps) {
function handleRowKeyDown(
event: KeyboardEvent
,
- signal: SelectionSignal,
+ stock: SelectionStockResult,
) {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
- onSelect(signal)
+ onSelect(stock)
}
return (
@@ -43,38 +43,47 @@ export function SignalTable({
- {signals.length > 0 ? (
- signals.map((signal) => {
- const selected = selectedKey === getSignalKey(signal)
+ {stocks.length > 0 ? (
+ stocks.map((stock) => {
+ const selected = selectedKey === getStockKey(stock)
return (
onSelect(signal)}
- onKeyDown={(event) => handleRowKeyDown(event, signal)}
+ onClick={() => onSelect(stock)}
+ onKeyDown={(event) => handleRowKeyDown(event, stock)}
tabIndex={0}
>
|
- {signal.name}
+ {stock.name}
- {signal.ts_code}
+ {stock.ts_code}
|
-
- {getCategoryLabel(signal.category)}
-
+
+ {stock.signals.map((signal) => (
+
+ {getCategoryLabel(signal.category)}
+
+ ))}
+
|
- {getSignalJValue(signal.details)}
+ {stock.signals.map((signal) => (
+
+ {getSignalJValue(signal.details)}
+
+ ))}
|
- ¥ {signal.close.toFixed(2)}
+ ¥ {stock.close.toFixed(2)}
|
)
diff --git a/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx b/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx
index 5116d65..f12442d 100644
--- a/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx
+++ b/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx
@@ -45,22 +45,31 @@ const selectedResult: SelectionResults = {
run_id: "run-1",
selected_stock_count: 1,
signal_count: 2,
- signals_total: 2,
- signals: [
+ stocks_total: 1,
+ stocks: [
{
- category: "zhixing_b1_original_b1",
close: 10.5,
- details: { j: 12, rsi: 20 },
- name: "平安银行",
- strategy: "zhixing_b1",
- target_trade_date: "2026-08-08",
- ts_code: "000001.SZ",
- },
- {
- category: "zhixing_b1_pullback_white",
- close: 10.5,
- details: { j: 12 },
name: "平安银行",
+ signals: [
+ {
+ category: "zhixing_b1_original_b1",
+ close: 10.5,
+ details: { j: 12, rsi: 20 },
+ name: "平安银行",
+ strategy: "zhixing_b1",
+ target_trade_date: "2026-08-08",
+ ts_code: "000001.SZ",
+ },
+ {
+ category: "zhixing_b1_pullback_white",
+ close: 10.5,
+ details: { j: 13 },
+ name: "平安银行",
+ strategy: "zhixing_b1",
+ target_trade_date: "2026-08-08",
+ ts_code: "000001.SZ",
+ },
+ ],
strategy: "zhixing_b1",
target_trade_date: "2026-08-08",
ts_code: "000001.SZ",
@@ -113,14 +122,15 @@ describe("SelectionResultsPage", () => {
).not.toBeInTheDocument()
expect(within(table).getByText("原始 B1")).toBeInTheDocument()
expect(within(table).getByText("回踩白线 B")).toBeInTheDocument()
- expect(within(table).getAllByText("12")).toHaveLength(2)
- expect(within(table).getAllByText("平安银行")).toHaveLength(2)
+ expect(within(table).getByText("12")).toBeInTheDocument()
+ expect(within(table).getByText("13")).toBeInTheDocument()
+ expect(within(table).getByText("平安银行")).toBeInTheDocument()
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
expect(within(detailPanel).getByText("关键指标")).toBeInTheDocument()
- expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
+ expect(within(detailPanel).getAllByText("J 值")).toHaveLength(2)
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
})
@@ -220,8 +230,8 @@ describe("SelectionResultsPage", () => {
data: {
...selectedResult,
signal_count: 0,
- signals: [],
- signals_total: 0,
+ stocks: [],
+ stocks_total: 0,
},
isError: false,
isPending: false,
@@ -260,7 +270,7 @@ describe("SelectionResultsPage", () => {
},
],
signal_count: 0,
- signals: [],
+ stocks: [],
status: "failed",
},
isError: false,
@@ -293,6 +303,9 @@ describe("SelectionResultsPage", () => {
fireEvent.click(categoryTrigger)
const oversoldOption = await screen.findByRole("option", { name: "超卖类" })
+ const content = oversoldOption.closest('[data-slot="select-content"]')
+ expect(content).toHaveAttribute("data-align-trigger", "false")
+ expect(content?.parentElement).toHaveAttribute("data-align", "start")
fireEvent.pointerDown(oversoldOption, { pointerType: "mouse" })
fireEvent.click(oversoldOption)
@@ -316,18 +329,26 @@ describe("SelectionResultsPage", () => {
})
it("filters signals, updates the detail panel, and expands mobile details", () => {
- const extraSignal = {
- ...selectedResult.signals[0],
- category: "zhixing_b1_oversold_volume",
- details: { macd: 3.2 },
+ const extraStock = {
+ ...selectedResult.stocks[0],
name: "招商银行",
+ signals: [
+ {
+ ...selectedResult.stocks[0].signals[0],
+ category: "zhixing_b1_oversold_volume",
+ details: { macd: 3.2 },
+ name: "招商银行",
+ ts_code: "600036.SH",
+ },
+ ],
ts_code: "600036.SH",
}
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
signal_count: 3,
- signals: [...selectedResult.signals, extraSignal],
+ stocks: [...selectedResult.stocks, extraStock],
+ stocks_total: 2,
},
isError: false,
isPending: false,
@@ -335,7 +356,7 @@ describe("SelectionResultsPage", () => {
render()
- fireEvent.change(screen.getByLabelText("搜索命中信号"), {
+ fireEvent.change(screen.getByLabelText("搜索命中股票"), {
target: { value: "招商" },
})
const table = screen.getByRole("table")
@@ -347,7 +368,7 @@ describe("SelectionResultsPage", () => {
).getByText("招商银行"),
).toBeInTheDocument()
- const expandButton = screen.getAllByRole("button", { name: "查看详情" })[2]
+ const expandButton = screen.getAllByRole("button", { name: "查看详情" })[1]
fireEvent.click(expandButton)
expect(expandButton).toHaveAttribute("aria-expanded", "true")
expect(screen.getAllByText("MACD").length).toBeGreaterThan(1)
@@ -355,17 +376,25 @@ describe("SelectionResultsPage", () => {
})
it("navigates to another page when the result list is paginated", () => {
- const signals = Array.from({ length: 6 }, (_, index) => ({
- ...selectedResult.signals[0],
+ const stocks = Array.from({ length: 6 }, (_, index) => ({
+ ...selectedResult.stocks[0],
name: `股票${index + 1}`,
+ signals: [
+ {
+ ...selectedResult.stocks[0].signals[0],
+ name: `股票${index + 1}`,
+ ts_code: `00000${index + 1}.SZ`,
+ },
+ ],
ts_code: `00000${index + 1}.SZ`,
}))
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
- signal_count: signals.length,
- signals,
- signals_total: signals.length,
+ selected_stock_count: stocks.length,
+ signal_count: stocks.length,
+ stocks,
+ stocks_total: stocks.length,
},
isError: false,
isPending: false,
@@ -422,7 +451,7 @@ describe("SelectionResultsPage", () => {
],
selected_stock_count: 0,
signal_count: 0,
- signals: [],
+ stocks: [],
status: "failed",
},
isError: false,
@@ -442,7 +471,7 @@ describe("SelectionResultsPage", () => {
data: {
...selectedResult,
run_id: null,
- signals: [],
+ stocks: [],
status: "no_data",
target_trade_date: "2026-08-08",
},
@@ -490,12 +519,12 @@ describe("SelectionResultsPage", () => {
it("continues polling a persisted running run after page reload", () => {
useSelectionResults.mockReturnValue({
- data: { ...selectedResult, signals: [], status: "running" },
+ data: { ...selectedResult, stocks: [], status: "running" },
isError: false,
isPending: false,
})
useSelectionRun.mockReturnValue({
- data: { ...selectedResult, signals: [], status: "running" },
+ data: { ...selectedResult, stocks: [], status: "running" },
isError: false,
isPending: false,
})