diff --git a/zhixing-web/src/features/selection/api/selection.api.ts b/zhixing-web/src/features/selection/api/selection.api.ts index cae3579..beb57c0 100644 --- a/zhixing-web/src/features/selection/api/selection.api.ts +++ b/zhixing-web/src/features/selection/api/selection.api.ts @@ -7,6 +7,7 @@ import { type SelectionResultsQuery, type SelectionRunAccepted, type SelectionRunRequest, + type SelectionSectors, type SelectionStrategy, } from "./selection.types" @@ -63,9 +64,27 @@ function buildSelectionQueryParams(query: SelectionResultsQuery) { if (query.search) params.set("search", query.search) if (query.category) params.set("category", query.category) if (query.sort) params.set("sort", query.sort) + if (query.sector) params.set("sector", query.sector) return params } +export function getSelectionResultSectors( + strategy: SelectionStrategy, + targetTradeDate?: string, + sectorType: "concept" | "industry" = "concept", + signal?: AbortSignal, +) { + const params = new URLSearchParams({ + strategy, + sector_type: sectorType, + }) + if (targetTradeDate) params.set("target_trade_date", targetTradeDate) + return requestJson( + `/api/v1/selection/sectors?${params.toString()}`, + { signal }, + ) +} + export function triggerSelectionRun( request: SelectionRunRequest, signal?: AbortSignal, diff --git a/zhixing-web/src/features/selection/api/selection.query.test.ts b/zhixing-web/src/features/selection/api/selection.query.test.ts index 8fbce75..159fc53 100644 --- a/zhixing-web/src/features/selection/api/selection.query.test.ts +++ b/zhixing-web/src/features/selection/api/selection.query.test.ts @@ -69,6 +69,7 @@ describe("selection query hooks", () => { "平安", "pullback", "score_desc", + "", ], ) expect( diff --git a/zhixing-web/src/features/selection/api/selection.query.ts b/zhixing-web/src/features/selection/api/selection.query.ts index 3dfdb91..1e6ef63 100644 --- a/zhixing-web/src/features/selection/api/selection.query.ts +++ b/zhixing-web/src/features/selection/api/selection.query.ts @@ -10,6 +10,7 @@ import { useEffect } from "react" import { getSelectionChart, + getSelectionResultSectors, getSelectionResults, getSelectionRun, triggerSelectionRun, @@ -19,6 +20,7 @@ import { type SelectionResults, type SelectionResultsQuery, type SelectionRunRequest, + type SelectionSectors, type SelectionStrategy, } from "./selection.types" @@ -55,6 +57,7 @@ export const selectionResultsQueryKey = ( query.search ?? "", query.category ?? "all", query.sort ?? "code", + query.sector ?? "", ] as const export const selectionRunQueryKey = ( @@ -69,8 +72,15 @@ export const selectionRunQueryKey = ( query.search ?? "", query.category ?? "all", query.sort ?? "code", + query.sector ?? "", ] as const +export const selectionSectorsQueryKey = ( + strategy: SelectionStrategy, + targetTradeDate?: string, + sectorType: "concept" | "industry" = "concept", +) => ["selection", "sectors", strategy, targetTradeDate, sectorType] as const + export function flattenSelectionResults( data: InfiniteData | undefined, ): SelectionResults | undefined { @@ -120,6 +130,18 @@ export function useSelectionChart( }) } +export function useSelectionResultSectors( + strategy: SelectionStrategy, + targetTradeDate?: string, + sectorType: "concept" | "industry" = "concept", +) { + return useQuery({ + queryFn: ({ signal }) => + getSelectionResultSectors(strategy, targetTradeDate, sectorType, signal), + queryKey: selectionSectorsQueryKey(strategy, targetTradeDate, sectorType), + }) +} + export function useSelectionRun( runId: string | null, resultQuery: SelectionResultsListQuery = defaultListQuery, @@ -173,9 +195,14 @@ export function invalidateSelectionResults( queryClient: QueryClient, strategy: SelectionStrategy, ) { - return queryClient.invalidateQueries({ - queryKey: ["selection", "results", strategy], - }) + return Promise.all([ + queryClient.invalidateQueries({ + queryKey: ["selection", "results", strategy], + }), + queryClient.invalidateQueries({ + queryKey: ["selection", "sectors", strategy], + }), + ]) } function nextSelectionPageParam( diff --git a/zhixing-web/src/features/selection/api/selection.types.ts b/zhixing-web/src/features/selection/api/selection.types.ts index abac089..6e49c77 100644 --- a/zhixing-web/src/features/selection/api/selection.types.ts +++ b/zhixing-web/src/features/selection/api/selection.types.ts @@ -25,6 +25,27 @@ export interface SelectionResultsQuery { search?: string category?: Exclude sort?: SelectionSort + sector?: string +} + +export type SelectionSectorType = "concept" | "industry" + +export const selectionSectorTypes = ["concept", "industry"] as const + +export interface SelectionSectorAggregate { + sector_code: string + sector_name: string + stock_count: number +} + +export interface SelectionSectors { + strategy: SelectionStrategy + target_trade_date: string | null + run_id: string | null + status: SelectionRunStatus + snapshot_trade_date: string | null + sector_type: SelectionSectorType + sectors: SelectionSectorAggregate[] } export type SelectionRunStatus = diff --git a/zhixing-web/src/features/selection/components/selection-results-workbench.tsx b/zhixing-web/src/features/selection/components/selection-results-workbench.tsx index 128a5dc..18b638a 100644 --- a/zhixing-web/src/features/selection/components/selection-results-workbench.tsx +++ b/zhixing-web/src/features/selection/components/selection-results-workbench.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { useNavigate, useSearch } from "@tanstack/react-router" import { Input } from "@/shared/ui/input" @@ -11,7 +11,11 @@ import { SelectValue, } from "@/shared/ui/select" -import type { SelectionResults, SelectionSort } from "../api/selection.types" +import type { + SelectionResults, + SelectionSectorAggregate, + SelectionSort, +} from "../api/selection.types" import { SignalDetailPanel } from "./signal-detail-panel" import { SignalRecordList } from "./signal-record-list" import { @@ -30,12 +34,15 @@ const SCORE_SORT_OPTIONS: ReadonlyArray<{ { label: "评分从低到高", value: "score_asc" }, ] +const ALL_SECTORS_VALUE = "all" + interface SelectionResultsWorkbenchProps { hasNextPage: boolean isFetchNextPageError: boolean isFetchingNextPage: boolean onLoadMore: () => void | Promise result: SelectionResults + sectorOptions: ReadonlyArray } export function SelectionResultsWorkbench({ @@ -44,12 +51,14 @@ export function SelectionResultsWorkbench({ isFetchingNextPage, onLoadMore, result, + sectorOptions, }: SelectionResultsWorkbenchProps) { const search = useSearch({ from: "/_workspace/selection" }) const navigate = useNavigate({ from: "/selection" }) const query = search.search ?? "" const category = search.category ?? "all" const sort = search.sort ?? "code" + const sector = search.sector const [selectedKey, setSelectedKey] = useState(null) const loadMoreRequestPending = useRef(false) @@ -68,11 +77,32 @@ export function SelectionResultsWorkbench({ const selectedStock = visibleStocks.find((stock) => getStockKey(stock) === selectedKey) ?? visibleStocks[0] + const sectorSelectItems = useMemo( + () => [ + { label: "全部板块", value: ALL_SECTORS_VALUE }, + ...sectorOptions.map((option) => ({ + label: option.sector_name, + value: option.sector_code, + })), + ], + [sectorOptions], + ) + + // The aggregate list is the source of truth for valid sectors; drop a stale + // value left over from another strategy or trade date. + useEffect(() => { + if (!sector) return + if (sectorOptions.some((option) => option.sector_code === sector)) return + void navigate({ + search: (previous) => ({ ...previous, sector: undefined }), + }) + }, [navigate, sector, sectorOptions]) function updateSearch(next: { search?: string category?: SignalCategoryFilter sort?: SelectionSort + sector?: string }) { void navigate({ search: (previous) => ({ ...previous, ...next }) }) } @@ -89,6 +119,10 @@ export function SelectionResultsWorkbench({ updateSearch({ sort: value }) } + function handleSectorChange(value: string) { + updateSearch({ sector: value === ALL_SECTORS_VALUE ? undefined : value }) + } + function requestLoadMore() { if ( loadMoreRequestPending.current || @@ -105,9 +139,9 @@ export function SelectionResultsWorkbench({ } return ( -
-
-
+
+
+
+