feat(selection): rework results layout and add sector filter

- move the filter bar out of the list column into a full-width top section;
  list and detail stay side by side below on md+, stacking search > list >
  detail on small screens
- add sector select fed by the run's concept-board aggregates; options show
  per-sector stock counts ordered by count desc, stale selections reset when
  the aggregate list changes
- persist sector in the route search, results/runs query keys, and refresh
  sector aggregates when a selection run finishes
This commit is contained in:
yuxuanhui
2026-09-05 19:48:41 +08:00
parent 7e0f13d678
commit 12642f3c2d
8 changed files with 233 additions and 26 deletions
@@ -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<SelectionSectors>(
`/api/v1/selection/sectors?${params.toString()}`,
{ signal },
)
}
export function triggerSelectionRun(
request: SelectionRunRequest,
signal?: AbortSignal,
@@ -69,6 +69,7 @@ describe("selection query hooks", () => {
"平安",
"pullback",
"score_desc",
"",
],
)
expect(
@@ -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<SelectionResults> | undefined,
): SelectionResults | undefined {
@@ -120,6 +130,18 @@ export function useSelectionChart(
})
}
export function useSelectionResultSectors(
strategy: SelectionStrategy,
targetTradeDate?: string,
sectorType: "concept" | "industry" = "concept",
) {
return useQuery<SelectionSectors, Error>({
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(
@@ -25,6 +25,27 @@ export interface SelectionResultsQuery {
search?: string
category?: Exclude<SelectionCategoryFilter, "all">
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 =