feat(selection): 支持选股结果服务端分页

This commit is contained in:
yuxuanhui
2026-08-10 11:09:23 +08:00
parent ed7bdda515
commit 3af97bf65d
16 changed files with 431 additions and 63 deletions
@@ -0,0 +1,47 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
const requestJson = vi.hoisted(() => vi.fn())
vi.mock("@/shared/api/request-json", () => ({ requestJson }))
import { getSelectionResults, getSelectionRun } from "./selection.api"
describe("selection API adapters", () => {
beforeEach(() => {
requestJson.mockReset()
})
it("sends paged result filters using the HTTP field names", async () => {
await getSelectionResults("zhixing_b1", "2026-08-08", {
category: "pullback",
page: 2,
pageSize: 15,
search: "平安银行",
})
const [input, init] = requestJson.mock.calls[0] as [
string,
{ signal?: AbortSignal },
]
const params = new URL(input, "http://localhost").searchParams
expect(params.get("strategy")).toBe("zhixing_b1")
expect(params.get("target_trade_date")).toBe("2026-08-08")
expect(params.get("page")).toBe("2")
expect(params.get("page_size")).toBe("15")
expect(params.get("search")).toBe("平安银行")
expect(params.get("category")).toBe("pullback")
expect(init).toEqual({ signal: undefined })
})
it("uses the same query contract while polling a run", async () => {
await getSelectionRun("run-1", { page: 3, pageSize: 5 })
const [input] = requestJson.mock.calls[0] as [string]
const params = new URL(input, "http://localhost").searchParams
expect(input).toContain("/api/v1/selection/runs/run-1?")
expect(params.get("page")).toBe("3")
expect(params.get("page_size")).toBe("5")
expect(params.has("search")).toBe(false)
expect(params.has("category")).toBe(false)
})
})
@@ -2,6 +2,7 @@ import { requestJson } from "@/shared/api/request-json"
import type {
SelectionResults,
SelectionResultsQuery,
SelectionRunAccepted,
SelectionRunRequest,
SelectionStrategy,
@@ -10,9 +11,11 @@ import type {
export function getSelectionResults(
strategy: SelectionStrategy,
targetTradeDate?: string,
query: SelectionResultsQuery = { page: 1, pageSize: 10 },
signal?: AbortSignal,
) {
const params = new URLSearchParams({ strategy })
const params = buildSelectionQueryParams(query)
params.set("strategy", strategy)
if (targetTradeDate) params.set("target_trade_date", targetTradeDate)
return requestJson<SelectionResults>(
`/api/v1/selection/results?${params.toString()}`,
@@ -20,10 +23,28 @@ export function getSelectionResults(
)
}
export function getSelectionRun(runId: string, signal?: AbortSignal) {
return requestJson<SelectionResults>(`/api/v1/selection/runs/${runId}`, {
signal,
export function getSelectionRun(
runId: string,
query: SelectionResultsQuery = { page: 1, pageSize: 10 },
signal?: AbortSignal,
) {
const params = buildSelectionQueryParams(query)
return requestJson<SelectionResults>(
`/api/v1/selection/runs/${runId}?${params.toString()}`,
{
signal,
},
)
}
function buildSelectionQueryParams(query: SelectionResultsQuery) {
const params = new URLSearchParams({
page: String(query.page),
page_size: String(query.pageSize),
})
if (query.search) params.set("search", query.search)
if (query.category) params.set("category", query.category)
return params
}
export function triggerSelectionRun(
@@ -11,33 +11,63 @@ import {
getSelectionRun,
triggerSelectionRun,
} from "./selection.api"
import type { SelectionRunRequest, SelectionStrategy } from "./selection.types"
import type {
SelectionResultsQuery,
SelectionRunRequest,
SelectionStrategy,
} from "./selection.types"
export const selectionResultsQueryKey = (
strategy: SelectionStrategy,
targetTradeDate?: string,
) => ["selection", "results", strategy, targetTradeDate] as const
query: SelectionResultsQuery = { page: 1, pageSize: 10 },
) =>
[
"selection",
"results",
strategy,
targetTradeDate,
query.page,
query.pageSize,
query.search ?? "",
query.category ?? "all",
] as const
export const selectionRunQueryKey = (runId: string) =>
["selection", "runs", runId] as const
export const selectionRunQueryKey = (
runId: string,
query: SelectionResultsQuery = { page: 1, pageSize: 10 },
) =>
[
"selection",
"runs",
runId,
query.page,
query.pageSize,
query.search ?? "",
query.category ?? "all",
] as const
export function useSelectionResults(
strategy: SelectionStrategy,
targetTradeDate?: string,
query: SelectionResultsQuery = { page: 1, pageSize: 10 },
) {
return useQuery({
queryFn: ({ signal }) =>
getSelectionResults(strategy, targetTradeDate, signal),
queryKey: selectionResultsQueryKey(strategy, targetTradeDate),
getSelectionResults(strategy, targetTradeDate, query, signal),
queryKey: selectionResultsQueryKey(strategy, targetTradeDate, query),
})
}
export function useSelectionRun(runId: string | null) {
export function useSelectionRun(
runId: string | null,
resultQuery: SelectionResultsQuery = { page: 1, pageSize: 10 },
) {
const queryClient = useQueryClient()
const query = useQuery({
enabled: Boolean(runId),
queryFn: ({ signal }) => getSelectionRun(runId ?? "", signal),
queryKey: selectionRunQueryKey(runId ?? "none"),
queryFn: ({ signal }) => getSelectionRun(runId ?? "", resultQuery, signal),
queryKey: selectionRunQueryKey(runId ?? "none", resultQuery),
refetchInterval: (query) =>
query.state.data?.status === "running" ? 1500 : false,
})
@@ -1,5 +1,22 @@
export type SelectionStrategy = "zhixing_b1"
export type SelectionCategoryFilter =
"all" | "pullback" | "oversold" | "original"
export const selectionCategoryFilters = [
"all",
"pullback",
"oversold",
"original",
] as const
export interface SelectionResultsQuery {
page: number
pageSize: number
search?: string
category?: Exclude<SelectionCategoryFilter, "all">
}
export type SelectionRunStatus =
"no_data" | "running" | "success" | "partial_success" | "failed"
@@ -50,6 +67,9 @@ export interface SelectionResults {
error_message: string | null
created_at: string | null
finished_at: string | null
page: number
page_size: number
signals_total: number
failures: SelectionFailure[]
signals: SelectionSignal[]
}