feat(sector-radar): 完成前端排名工作台

This commit is contained in:
yuxuanhui
2026-08-29 20:14:42 +08:00
parent efc4c3d530
commit 8e96e64151
12 changed files with 1913 additions and 11 deletions
@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
const requestJson = vi.hoisted(() => vi.fn())
vi.mock("@/shared/api/request-json", () => ({ requestJson }))
import { getSectorRadarDates, getSectorRadarRankings } from "./sector-radar.api"
const publication = {
publication_id: "publication-1",
target_trade_date: "2026-08-28",
status: "success",
source_version: "tushare-pro-v1",
universe_version: "eastmoney-dc-v1",
metric_versions: ["zhixing_amount_net_bn_v1"],
input_hash: "a".repeat(64),
coverage: "0.99",
started_at: "2026-08-28T08:00:00Z",
finished_at: "2026-08-28T08:05:00Z",
error_summary: null,
}
const rankingPayload = {
status: "success",
requested_trade_date: "2026-08-28",
sector_type: "concept",
view: "rank_change",
rank_change_metric: "amount",
rank_change_days: 5,
side: "top",
search: "机器人",
publication,
definition: {
metric_kind: "amount",
metric_version: "zhixing_amount_net_bn_v1",
label: "主力净流入(知行独立实现)",
unit: "CNY_100M",
implementation_kind: "independent",
disclaimer: "知行独立实现,非 OneChartLab 原站公式。",
},
page: 2,
page_size: 10,
total: 1,
rows: [
{
trade_date: "2026-08-28",
sector_type: "concept",
sector_code: "BK0001.DC",
sector_name: "机器人",
metric_kind: "amount",
metric_version: "zhixing_amount_net_bn_v1",
implementation_kind: "independent",
unit: "CNY_100M",
metric_value: "12.5",
quality: "available",
member_count: 20,
valid_sample_count: 19,
membership_coverage: "1",
moneyflow_coverage: "0.95",
rank_position: 1,
rank_percentile: "100",
rank_change_days: 5,
rank_change: 3,
},
],
}
describe("sector radar API adapters", () => {
beforeEach(() => {
requestJson.mockReset()
})
it("forwards AbortSignal and normalizes decimal publication fields", async () => {
const signal = new AbortController().signal
requestJson.mockResolvedValue({
status: "success",
available_dates: ["2026-08-28"],
current_attempt: publication,
last_good: publication,
})
const result = await getSectorRadarDates(signal)
expect(requestJson).toHaveBeenCalledWith("/api/v1/sector-radar/dates", {
signal,
})
expect(result.last_good?.coverage).toBe(0.99)
})
it("maps URL-backed filters to the ranking HTTP contract", async () => {
const signal = new AbortController().signal
requestJson.mockResolvedValue(rankingPayload)
const result = await getSectorRadarRankings(
{
tradeDate: "2026-08-28",
sectorType: "concept",
view: "rank_change",
rankChangeMetric: "amount",
rankChangeDays: 5,
side: "top",
search: " 机器人 ",
page: 2,
pageSize: 10,
},
signal,
)
const [input, init] = requestJson.mock.calls[0] as [
string,
{ signal?: AbortSignal },
]
const params = new URL(input, "http://localhost").searchParams
expect(input).toContain("/api/v1/sector-radar/rankings?")
expect(params.get("trade_date")).toBe("2026-08-28")
expect(params.get("sector_type")).toBe("concept")
expect(params.get("view")).toBe("rank_change")
expect(params.get("rank_change_metric")).toBe("amount")
expect(params.get("rank_change_days")).toBe("5")
expect(params.get("side")).toBe("top")
expect(params.get("search")).toBe("机器人")
expect(params.get("page")).toBe("2")
expect(params.get("page_size")).toBe("10")
expect(init).toEqual({ signal })
expect(result.rows[0]?.metric_value).toBe(12.5)
expect(result.rows[0]?.rank_percentile).toBe(100)
})
it("rejects unknown stable enum values at the feature boundary", async () => {
requestJson.mockResolvedValue({ ...rankingPayload, view: "private_score" })
await expect(
getSectorRadarRankings({
sectorType: "concept",
view: "amount",
rankChangeMetric: "amount",
rankChangeDays: 1,
side: "all",
page: 1,
pageSize: 20,
}),
).rejects.toThrow("Invalid sector radar response: rankings.view")
})
it("rejects non-finite decimal strings instead of rendering them", async () => {
requestJson.mockResolvedValue({
...rankingPayload,
rows: [{ ...rankingPayload.rows[0], metric_value: "NaN" }],
})
await expect(
getSectorRadarRankings({
sectorType: "concept",
view: "amount",
rankChangeMetric: "amount",
rankChangeDays: 1,
side: "all",
page: 1,
pageSize: 20,
}),
).rejects.toThrow("rankings.rows[0].metric_value must be finite")
})
})