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:
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user