feat(selection): show stock industry and concept boards in detail panel

Add point-in-time stock membership lookup to the sector radar module
(GET /sector-radar/stocks/{ts_code}/membership) reading the existing
dc_index/dc_member snapshots, and surface industries plus concept chips
in the selection signal detail panel with graceful no-data hiding.
This commit is contained in:
yuxuanhui
2026-09-05 16:23:50 +08:00
parent 50cac3575a
commit 41a9b4eb9a
14 changed files with 825 additions and 5 deletions
@@ -4,7 +4,11 @@ const requestJson = vi.hoisted(() => vi.fn())
vi.mock("@/shared/api/request-json", () => ({ requestJson }))
import { getSectorRadarDates, getSectorRadarRankings } from "./sector-radar.api"
import {
getSectorRadarDates,
getSectorRadarRankings,
getStockSectorMembership,
} from "./sector-radar.api"
const publication = {
publication_id: "publication-1",
@@ -180,3 +184,59 @@ describe("sector radar API adapters", () => {
).rejects.toThrow("rankings.rows[0].rank_percentile must be greater than 0")
})
})
describe("stock sector membership adapter", () => {
const membershipPayload = {
status: "success",
ts_code: "000001.SZ",
requested_trade_date: "2026-08-28",
trade_date: "2026-08-28",
industries: [{ code: "BK0901.DC", name: "银行" }],
concepts: [
{ code: "BK0001.DC", name: "机器人" },
{ code: "BK0002.DC", name: "人工智能" },
],
concept_total: 2,
concept_limit: 20,
}
it("requests the snake-case endpoint and validates the payload", async () => {
requestJson.mockResolvedValue(membershipPayload)
const result = await getStockSectorMembership("000001.SZ", "2026-08-28")
expect(requestJson).toHaveBeenCalledWith(
"/api/v1/sector-radar/stocks/000001.SZ/membership?trade_date=2026-08-28",
{ signal: undefined },
)
expect(result).toEqual(membershipPayload)
})
it("keeps the no_data status with a null snapshot date", async () => {
requestJson.mockResolvedValue({
...membershipPayload,
status: "no_data",
trade_date: null,
industries: [],
concepts: [],
concept_total: 0,
})
const result = await getStockSectorMembership("000001.SZ", "2026-08-28")
expect(result.status).toBe("no_data")
expect(result.trade_date).toBeNull()
expect(result.industries).toEqual([])
})
it("rejects a membership response with invalid sector references", async () => {
requestJson.mockResolvedValue({
...membershipPayload,
industries: [{ code: "BK0901.DC", name: " " }],
})
await expect(
getStockSectorMembership("000001.SZ", "2026-08-28"),
).rejects.toThrow("membership.industries[0].name must be a non-empty string")
})
})