diff --git a/zhixing-web/src/features/selection/components/selection-presentation.ts b/zhixing-web/src/features/selection/components/selection-presentation.ts index 9fb3bd2..713fab9 100644 --- a/zhixing-web/src/features/selection/components/selection-presentation.ts +++ b/zhixing-web/src/features/selection/components/selection-presentation.ts @@ -28,6 +28,20 @@ export const categoryLabels: Record = { zhixing_b1_pullback_yellow: "回踩黄线 B", } +const detailLabels: Record = { + daily_amplitude: "日振幅", + daily_change: "日涨跌幅", + j: "J 值", + macd: "MACD", + rsi: "RSI", + sub_signal: "命中信号", + trend_white: "知行白线", + trend_yellow: "知行黄线", + volume: "成交量", +} + +const percentageDetailKeys = new Set(["daily_amplitude", "daily_change"]) + export type SignalCategoryFilter = SelectionCategoryFilter export const signalCategoryOptions: ReadonlyArray<{ @@ -59,10 +73,32 @@ export function getSignalKey(signal: SelectionSignal) { return `${signal.ts_code}-${signal.category}` } -export function formatDetails(details: SelectionSignal["details"]) { - return Object.entries(details) - .map(([key, value]) => `${key}: ${value ?? "—"}`) - .join(" · ") +export function getSignalDetailItems(details: SelectionSignal["details"]) { + return Object.entries(details).map(([key, value]) => ({ + key, + label: detailLabels[key] ?? key, + value: formatSignalDetailValue(key, value), + })) +} + +export function getSignalJValue(details: SelectionSignal["details"]) { + return formatSignalDetailValue("j", details.j) +} + +function formatSignalDetailValue( + key: string, + value: number | string | null | undefined, +) { + if (value === null || value === undefined || value === "") return "—" + if (typeof value !== "number") return value + if (key === "volume") { + return value.toLocaleString("zh-CN", { maximumFractionDigits: 0 }) + } + + const formatted = value.toLocaleString("zh-CN", { + maximumFractionDigits: 2, + }) + return percentageDetailKeys.has(key) ? `${formatted}%` : formatted } export function matchesCategory( diff --git a/zhixing-web/src/features/selection/components/signal-detail-panel.tsx b/zhixing-web/src/features/selection/components/signal-detail-panel.tsx index 1cda755..607070c 100644 --- a/zhixing-web/src/features/selection/components/signal-detail-panel.tsx +++ b/zhixing-web/src/features/selection/components/signal-detail-panel.tsx @@ -4,11 +4,8 @@ import { Badge } from "@/shared/ui/badge" import { Card } from "@/shared/ui/card" import type { SelectionSignal } from "../api/selection.types" -import { - categoryToneClass, - formatDetails, - getCategoryLabel, -} from "./selection-presentation" +import { categoryToneClass, getCategoryLabel } from "./selection-presentation" +import { SignalDetails } from "./signal-details" interface SignalDetailPanelProps { signal: SelectionSignal | undefined @@ -69,7 +66,7 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) { {getCategoryLabel(signal.category)} -
+
目标交易日
@@ -80,13 +77,17 @@ export function SignalDetailPanel({ signal }: SignalDetailPanelProps) {
策略
知行 B1
-
-
关键依据
-
- {formatDetails(signal.details)} -
-
+ +
+

+ 关键指标 +

+ +
) } diff --git a/zhixing-web/src/features/selection/components/signal-details.tsx b/zhixing-web/src/features/selection/components/signal-details.tsx new file mode 100644 index 0000000..89223bf --- /dev/null +++ b/zhixing-web/src/features/selection/components/signal-details.tsx @@ -0,0 +1,38 @@ +import { cn } from "@/shared/ui/utils" + +import type { SelectionSignal } from "../api/selection.types" +import { getSignalDetailItems } from "./selection-presentation" + +interface SignalDetailsProps { + className?: string + details: SelectionSignal["details"] +} + +export function SignalDetails({ className, details }: SignalDetailsProps) { + const items = getSignalDetailItems(details) + + if (items.length === 0) { + return

暂无指标详情

+ } + + return ( +
+ {items.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+ ) +} 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 8e78040..3a2627a 100644 --- a/zhixing-web/src/features/selection/components/signal-record-list.tsx +++ b/zhixing-web/src/features/selection/components/signal-record-list.tsx @@ -6,10 +6,11 @@ import { Button } from "@/shared/ui/button" import type { SelectionSignal } from "../api/selection.types" import { categoryToneClass, - formatDetails, getCategoryLabel, + getSignalJValue, getSignalKey, } from "./selection-presentation" +import { SignalDetails } from "./signal-details" interface SignalRecordListProps { signals: SelectionSignal[] @@ -63,24 +64,33 @@ export function SignalRecordList({ > {getCategoryLabel(signal.category)} - +
+ + J  + + {getSignalJValue(signal.details)} + + + +
{expanded ? ( -

- {formatDetails(signal.details)} -

+ ) : null} ) diff --git a/zhixing-web/src/features/selection/components/signal-table.tsx b/zhixing-web/src/features/selection/components/signal-table.tsx index a15e0c0..856c004 100644 --- a/zhixing-web/src/features/selection/components/signal-table.tsx +++ b/zhixing-web/src/features/selection/components/signal-table.tsx @@ -5,8 +5,8 @@ import { Badge } from "@/shared/ui/badge" import type { SelectionSignal } from "../api/selection.types" import { categoryToneClass, - formatDetails, getCategoryLabel, + getSignalJValue, getSignalKey, } from "./selection-presentation" @@ -37,9 +37,9 @@ export function SignalTable({ 股票 - 子信号 - 收盘价 - 关键详情 + 信号类型 + J 值 + 收盘价 @@ -70,13 +70,11 @@ export function SignalTable({ {getCategoryLabel(signal.category)} - - ¥ {signal.close.toFixed(2)} + + {getSignalJValue(signal.details)} - - - {formatDetails(signal.details)} - + + ¥ {signal.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 cc2b9d7..5116d65 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 @@ -102,9 +102,26 @@ describe("SelectionResultsPage", () => { expect(screen.queryByText("目标股票数")).not.toBeInTheDocument() const table = screen.getByRole("table") expect(table).toBeInTheDocument() + expect( + within(table).getByRole("columnheader", { name: "信号类型" }), + ).toBeInTheDocument() + expect( + within(table).getByRole("columnheader", { name: "J 值" }), + ).toBeInTheDocument() + expect( + within(table).queryByRole("columnheader", { name: "关键详情" }), + ).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) + + const detailPanel = screen.getByRole("complementary", { + name: "当前股票详情", + }) + expect(within(detailPanel).getByText("关键指标")).toBeInTheDocument() + expect(within(detailPanel).getByText("J 值")).toBeInTheDocument() + expect(within(detailPanel).getByText("RSI")).toBeInTheDocument() }) it("opens execution status details in a drawer and restores trigger focus", async () => { @@ -330,10 +347,11 @@ describe("SelectionResultsPage", () => { ).getByText("招商银行"), ).toBeInTheDocument() - const expandButton = screen.getAllByRole("button", { name: "查看详情" })[0] + const expandButton = screen.getAllByRole("button", { name: "查看详情" })[2] fireEvent.click(expandButton) expect(expandButton).toHaveAttribute("aria-expanded", "true") - expect(screen.getAllByText("macd: 3.2").length).toBeGreaterThan(1) + expect(screen.getAllByText("MACD").length).toBeGreaterThan(1) + expect(screen.getAllByText("3.2").length).toBeGreaterThan(1) }) it("navigates to another page when the result list is paginated", () => {