diff --git a/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md b/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md index 2a5e701..8d5001f 100644 --- a/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md +++ b/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md @@ -1,40 +1,43 @@ -# 选股页面迭代:布局重构与板块筛选 +# 选股页面迭代:布局重构与细分行业筛选 ## 需求 1. 选股页面从左右布局改为「上搜索栏 + 左列表 / 右详情」三段式布局。 -2. 搜索模块增加板块筛选:下拉选项为当次策略结果的板块聚合,选项旁展示数量,按数量倒序。 +2. 搜索模块增加细分行业筛选:下拉选项为当次策略结果的细分行业聚合,选项旁展示数量,按数量倒序。 +3. 选股模块不展示「板块」(概念板块):列表筛选与详情页均去掉;详情页「行业」口径改称「细分行业」。(2026-09-05 修订,原口径为概念板块) ## 关键决策 -- 板块口径:概念板块(sector_type=concept),与详情面板「板块」标签一致;接口保留 sector_type 参数可扩展行业。 +- 板块口径:细分行业(sector_type=industry,即东财行业快照),selection 上下文固定为 industry——HTTP `/sectors` 不再接受 sector_type 参数,端口/适配器默认值均为 industry;「细分行业」仍是 sector 词汇的一种,内部参数名保持 `sector` 不变。 - 单选下拉;排序按 stock_count 倒序(后端保证),名称升序 tie-break。 -- 板块数据不在 selection 表中,按 ADR 0001 通过端口委托 sector_radar 读服务(不跨上下文 join SQL)。 +- 行业数据不在 selection 表中,按 ADR 0001 通过端口委托 sector_radar 读服务(不跨上下文 join SQL)。 ## 实现 后端(zhixing-server): - sector_radar:`domain/persistence.py` 新增 `SectorCountEntry` + 2 个协议方法;`infrastructure/postgres.py` / `infrastructure/memory.py` 实现 `load_sector_counts` / `load_sector_member_codes`;`application/read.py` 新增 `sector_counts` / `sector_member_codes`(先解析 last-good publication)。 -- selection:`domain/runs.py` 新增 `SelectionSectorReader` 端口、`SelectionSectorCount` / `SelectionSectorMembership` / `SelectionRunIdentity`,`SelectionResultQuery.sector`;`application/run.py` 新增 `list_sector_counts`,`get_run`/`get_latest` 经 identity → 成员代码 → `sector_stock_codes` 过滤;`infrastructure/postgres_runs.py` `_stock_filter` 支持 `ts_code = ANY(...)` / FALSE;`infrastructure/sector_membership.py` 桥接适配器;`presentation/http.py` 新增 `GET /sectors`,results/runs 增加 `sector` 参数。 +- selection:`domain/runs.py` 新增 `SelectionSectorReader` 端口、`SelectionSectorCount` / `SelectionSectorMembership` / `SelectionRunIdentity`,`SelectionResultQuery.sector`;`application/run.py` 新增 `list_sector_counts`(固定 industry),`get_run`/`get_latest` 经 identity → 成员代码 → `sector_stock_codes` 过滤;`infrastructure/postgres_runs.py` `_stock_filter` 支持 `ts_code = ANY(...)` / FALSE;`infrastructure/sector_membership.py` 桥接适配器;`presentation/http.py` 新增 `GET /sectors`,results/runs 增加 `sector` 参数。 前端(zhixing-web): -- `selection.types.ts` / `selection.api.ts` / `selection.query.ts`:`SelectionSectors`、`getSelectionResultSectors`、`useSelectionResultSectors`,query key 加 sector,结果失效同时失效 sectors。 +- `selection.types.ts` / `selection.api.ts` / `selection.query.ts`:`SelectionSectors`(sector_type 收窄为 "industry")、`getSelectionResultSectors`、`useSelectionResultSectors`(不传 sector_type),query key 加 sector,结果失效同时失效 sectors。 - `route-tree.tsx`:selection 路由 search 增加 `sector`。 - `selection-results-page.tsx`:结果查询带 sector,页面调用 sectors hook 并下传 workbench;切策略重置 sector。 -- `selection-results-workbench.tsx`:布局重构为上搜索栏 + 下方 320px 列表/详情两栏;板块下拉(全部板块 + 聚合选项带数量徽标);聚合加载后自动清掉失效 sector。 +- `selection-results-workbench.tsx`:布局重构为上搜索栏 + 下方 320px 列表/详情两栏;细分行业下拉(全部细分行业 + 聚合选项带数量徽标);聚合加载后自动清掉失效 sector。 +- `signal-detail-panel.tsx`:展示「细分行业:…」(title 提示成分快照日期),不再展示概念板块 chips。 ## 测试 -- 后端:`test_read.py` +8、`test_sector_filter.py` 新建 9 个、`test_selection_http.py` +5。 -- 前端:页面测试新增「filters by sector and shows per-sector counts」。 +- 后端:`test_read.py` +8、`test_sector_filter.py` 新建 9 个(口径 industry)、`test_selection_http.py` +3(不再有 sector_type 转发/422 用例)。 +- 前端:页面测试新增「filters by sub-industry and shows per-industry counts」;详情面板测试改为细分行业断言并删除概念 chips 用例。 ## 验收 - [x] 页面布局为上搜索栏 + 左列表右详情(移动端纵向堆叠 搜索→列表→详情) -- [x] 板块下拉选项为当次策略结果的概念板块聚合,选项旁展示数量,按数量倒序 -- [x] 选择板块后结果列表服务端过滤,`筛选结果 N 只` 反映叠加计数 +- [x] 细分行业下拉选项为当次策略结果的细分行业聚合,选项旁展示数量,按数量倒序 +- [x] 选择细分行业后结果列表服务端过滤,`筛选结果 N 只` 反映叠加计数 +- [x] 详情面板只展示细分行业,不展示概念板块 - [x] 后端 ruff/pyright/pytest 与前端 lint/typecheck/test/build 门禁通过(遗留项均为基线原有) ## 遗留(均为基线原有,非本次引入) -- 后端 pyright 16 个错误(chart.py/gold_brick.py pandas 相关、test_run.py 旧 fake 类型);前端 4 个执行状态抽屉测试失败;前端 format:check 有基线未格式化文件(.playwright-cli、sector-radar 部分文件、signal-detail-panel.tsx)。 +- 后端 pyright 16 个错误(chart.py/gold_brick.py pandas 相关、test_run.py 旧 fake 类型);前端 4 个执行状态抽屉测试失败;前端 format:check 有基线未格式化文件(.playwright-cli、sector-radar 部分文件;signal-detail-panel.tsx 已随本次格式化移除)。 diff --git a/zhixing-server/src/zhixing_server/modules/selection/application/run.py b/zhixing-server/src/zhixing_server/modules/selection/application/run.py index 76c50a0..457c521 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/application/run.py +++ b/zhixing-server/src/zhixing_server/modules/selection/application/run.py @@ -642,20 +642,22 @@ class RunZhixingB1: target_trade_date: date, sector_code: str, ) -> tuple[str, ...] | None: - """Resolve one sector's members, or None when the port is absent.""" + """Resolve one sub-industry's members, or None when the port is absent.""" if self.sector_reader is None: return None - return self.sector_reader.sector_member_codes(target_trade_date, sector_code) + return self.sector_reader.sector_member_codes( + target_trade_date, + sector_code, + sector_type="industry", + ) def list_sector_counts( self, strategy: StrategyName, target_trade_date: date | None = None, - *, - sector_type: str = "concept", ) -> SelectionSectorAggregates | None: - """Aggregate the current run's selected stocks by point-in-time sector.""" + """Aggregate the current run's selected stocks by sub-industry.""" run = self.store.get_latest_run(strategy, target_trade_date) if run is None: @@ -665,11 +667,11 @@ class RunZhixingB1: for item in run.items if item.status == "selected" and item.signal_count > 0 ] - membership = self._sector_membership(selected_codes, run.target_trade_date, sector_type) + membership = self._sector_membership(selected_codes, run.target_trade_date) return SelectionSectorAggregates( run=run, snapshot_trade_date=membership.snapshot_trade_date, - sector_type=sector_type, + sector_type="industry", sectors=membership.sector_counts, ) @@ -677,16 +679,15 @@ class RunZhixingB1: self, stock_codes: Sequence[str], target_trade_date: date, - sector_type: str, ) -> SelectionSectorMembership: - """Read sector counts for a stock set, tolerating a missing port.""" + """Read sub-industry counts for a stock set, tolerating a missing port.""" if self.sector_reader is None or not stock_codes: return SelectionSectorMembership(snapshot_trade_date=None, sector_counts=()) return self.sector_reader.sector_counts( stock_codes, target_trade_date, - sector_type=sector_type, + sector_type="industry", ) diff --git a/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py b/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py index d9878d2..2219a0d 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py +++ b/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py @@ -67,7 +67,7 @@ class SelectionSectorReader(Protocol): stock_codes: Sequence[str], target_trade_date: date, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> SelectionSectorMembership: ... def sector_member_codes( @@ -75,7 +75,7 @@ class SelectionSectorReader(Protocol): target_trade_date: date, sector_code: str, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> tuple[str, ...]: ... diff --git a/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py b/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py index 86e31da..83772d0 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py +++ b/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py @@ -43,7 +43,7 @@ class SectorRadarSelectionReader: stock_codes: Sequence[str], target_trade_date: date, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> SelectionSectorMembership: """Return per-sector stock counts for one run's selected stocks.""" @@ -69,7 +69,7 @@ class SectorRadarSelectionReader: target_trade_date: date, sector_code: str, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> tuple[str, ...]: """Return one sector's member stock codes on the aligned snapshot.""" diff --git a/zhixing-server/src/zhixing_server/modules/selection/presentation/http.py b/zhixing-server/src/zhixing_server/modules/selection/presentation/http.py index 9409bb1..4cbfcd6 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/presentation/http.py +++ b/zhixing-server/src/zhixing_server/modules/selection/presentation/http.py @@ -231,14 +231,14 @@ def _empty_sectors() -> list[SelectionSectorCountResponse]: class SelectionSectorsResponse(BaseModel): - """The current run's selected stocks aggregated by point-in-time sector.""" + """The current run's selected stocks aggregated by point-in-time sub-industry.""" strategy: StrategyValue target_trade_date: date | None run_id: str | None status: SelectionStatusValue snapshot_trade_date: date | None - sector_type: Literal["concept", "industry"] + sector_type: Literal["industry"] sectors: list[SelectionSectorCountResponse] = Field(default_factory=_empty_sectors) @@ -391,16 +391,11 @@ def get_selection_sectors( service: Annotated[RunZhixingB1, Depends(get_selection_service)], strategy: StrategyValue = "zhixing_b1", target_trade_date: date | None = None, - sector_type: Literal["concept", "industry"] = "concept", ) -> SelectionSectorsResponse: - """Aggregate the current run's selected stocks by point-in-time sector.""" + """Aggregate the current run's selected stocks by point-in-time sub-industry.""" try: - aggregates = service.list_sector_counts( - strategy, - target_trade_date, - sector_type=sector_type, - ) + aggregates = service.list_sector_counts(strategy, target_trade_date) except SelectionRunStoreError as exc: raise _http_error(503, "selection_storage_unavailable", str(exc)) from exc if aggregates is None or aggregates.run is None: @@ -410,7 +405,7 @@ def get_selection_sectors( run_id=None, status="no_data", snapshot_trade_date=None, - sector_type=sector_type, + sector_type="industry", ) run = aggregates.run return SelectionSectorsResponse( @@ -419,7 +414,7 @@ def get_selection_sectors( run_id=run.id, status=run.status, snapshot_trade_date=aggregates.snapshot_trade_date, - sector_type=sector_type, + sector_type="industry", sectors=[ SelectionSectorCountResponse( sector_code=count.sector_code, diff --git a/zhixing-server/tests/test_selection_http.py b/zhixing-server/tests/test_selection_http.py index 2294d29..4e5df5a 100644 --- a/zhixing-server/tests/test_selection_http.py +++ b/zhixing-server/tests/test_selection_http.py @@ -59,7 +59,6 @@ class FakeSelectionService: self.executed = False self.mode = "ok" self.last_query: SelectionResultQuery | None = None - self.sector_type_requested: str | None = None def prepare( self, @@ -117,16 +116,13 @@ class FakeSelectionService: self, strategy: str, target_trade_date: date | None = None, - *, - sector_type: str = "concept", ) -> SelectionSectorAggregates | None: - self.sector_type_requested = sector_type if self.run is None: return None return SelectionSectorAggregates( run=self.run, snapshot_trade_date=self.run.target_trade_date, - sector_type=sector_type, + sector_type="industry", sectors=self.sectors, ) @@ -493,7 +489,7 @@ def test_sectors_returns_aggregated_counts_desc() -> None: "run_id": "run-http", "status": "success", "snapshot_trade_date": "2026-08-08", - "sector_type": "concept", + "sector_type": "industry", "sectors": [ {"sector_code": "BK0001.DC", "sector_name": "机器人", "stock_count": 3}, {"sector_code": "BK0003.DC", "sector_name": "数字经济", "stock_count": 2}, @@ -501,19 +497,6 @@ def test_sectors_returns_aggregated_counts_desc() -> None: } -def test_sectors_forwards_sector_type() -> None: - service = FakeSelectionService(_run("run-http", "success")) - - response = _client(service).get( - "/api/v1/selection/sectors", - params={"sector_type": "industry"}, - ) - - assert response.status_code == 200 - assert service.sector_type_requested == "industry" - assert response.json()["sector_type"] == "industry" - - def test_sectors_without_run_returns_no_data() -> None: response = _client(FakeSelectionService()).get("/api/v1/selection/sectors") @@ -524,20 +507,11 @@ def test_sectors_without_run_returns_no_data() -> None: "run_id": None, "status": "no_data", "snapshot_trade_date": None, - "sector_type": "concept", + "sector_type": "industry", "sectors": [], } -def test_sectors_rejects_unknown_sector_type() -> None: - response = _client(FakeSelectionService()).get( - "/api/v1/selection/sectors", - params={"sector_type": "macro"}, - ) - - assert response.status_code == 422 - - def test_chart_returns_bounded_qfq_contract() -> None: chart_service = FakeChartService() diff --git a/zhixing-server/tests/unit/selection/test_sector_filter.py b/zhixing-server/tests/unit/selection/test_sector_filter.py index 7dccd33..3a8fd03 100644 --- a/zhixing-server/tests/unit/selection/test_sector_filter.py +++ b/zhixing-server/tests/unit/selection/test_sector_filter.py @@ -49,7 +49,7 @@ class FakeSectorReader: stock_codes: Sequence[str], target_trade_date: date, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> SelectionSectorMembership: self.count_calls.append((tuple(stock_codes), target_trade_date, sector_type)) return SelectionSectorMembership( @@ -62,7 +62,7 @@ class FakeSectorReader: target_trade_date: date, sector_code: str, *, - sector_type: str = "concept", + sector_type: str = "industry", ) -> tuple[str, ...]: self.member_calls.append((target_trade_date, sector_code, sector_type)) return self.member_codes @@ -161,9 +161,9 @@ def test_list_sector_counts_aggregates_only_selected_stocks() -> None: aggregates = service.list_sector_counts("zhixing_b1") assert isinstance(aggregates, SelectionSectorAggregates) - assert sector_reader.count_calls == [(("000001.SZ", "000002.SZ"), TARGET, "concept")] + assert sector_reader.count_calls == [(("000001.SZ", "000002.SZ"), TARGET, "industry")] assert aggregates.snapshot_trade_date == TARGET - assert aggregates.sector_type == "concept" + assert aggregates.sector_type == "industry" assert aggregates.sectors == sector_reader.counts assert aggregates.run is store.run @@ -198,7 +198,7 @@ def test_get_latest_resolves_sector_filter_against_run_snapshot() -> None: ) assert run is store.run - assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "concept")] + assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "industry")] assert store.sector_codes_seen == ("000001.SZ", "000002.SZ") assert store.queries[-1].sector is None assert store.queries[-1].page == 2 @@ -239,7 +239,7 @@ def test_get_run_resolves_sector_filter_against_run_snapshot() -> None: run = service.get_run("run-1", query=SelectionResultQuery(sector="BK0001.DC")) assert run is store.run - assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "concept")] + assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "industry")] assert store.sector_codes_seen == ("000001.SZ",) @@ -251,14 +251,3 @@ def test_get_latest_without_run_and_sector_filter_returns_none() -> None: ) assert service.get_latest("zhixing_b1", query=SelectionResultQuery(sector="BK0001.DC")) is None - - -def test_list_sector_counts_accepts_industry_type() -> None: - sector_reader = FakeSectorReader() - service = RunZhixingB1(FakeReader(), FakeStore(_run()), sector_reader=sector_reader) - - aggregates = service.list_sector_counts("zhixing_b1", sector_type="industry") - - assert aggregates is not None - assert aggregates.sector_type == "industry" - assert sector_reader.count_calls == [(("000001.SZ", "000002.SZ"), TARGET, "industry")] diff --git a/zhixing-web/src/features/selection/api/selection.api.ts b/zhixing-web/src/features/selection/api/selection.api.ts index beb57c0..fda0a25 100644 --- a/zhixing-web/src/features/selection/api/selection.api.ts +++ b/zhixing-web/src/features/selection/api/selection.api.ts @@ -71,12 +71,10 @@ function buildSelectionQueryParams(query: SelectionResultsQuery) { 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( diff --git a/zhixing-web/src/features/selection/api/selection.query.ts b/zhixing-web/src/features/selection/api/selection.query.ts index 1e6ef63..42bb1c9 100644 --- a/zhixing-web/src/features/selection/api/selection.query.ts +++ b/zhixing-web/src/features/selection/api/selection.query.ts @@ -78,8 +78,7 @@ export const selectionRunQueryKey = ( export const selectionSectorsQueryKey = ( strategy: SelectionStrategy, targetTradeDate?: string, - sectorType: "concept" | "industry" = "concept", -) => ["selection", "sectors", strategy, targetTradeDate, sectorType] as const +) => ["selection", "sectors", strategy, targetTradeDate] as const export function flattenSelectionResults( data: InfiniteData | undefined, @@ -133,12 +132,11 @@ export function useSelectionChart( export function useSelectionResultSectors( strategy: SelectionStrategy, targetTradeDate?: string, - sectorType: "concept" | "industry" = "concept", ) { return useQuery({ queryFn: ({ signal }) => - getSelectionResultSectors(strategy, targetTradeDate, sectorType, signal), - queryKey: selectionSectorsQueryKey(strategy, targetTradeDate, sectorType), + getSelectionResultSectors(strategy, targetTradeDate, signal), + queryKey: selectionSectorsQueryKey(strategy, targetTradeDate), }) } diff --git a/zhixing-web/src/features/selection/api/selection.types.ts b/zhixing-web/src/features/selection/api/selection.types.ts index 6e49c77..d2d5b8f 100644 --- a/zhixing-web/src/features/selection/api/selection.types.ts +++ b/zhixing-web/src/features/selection/api/selection.types.ts @@ -28,9 +28,7 @@ export interface SelectionResultsQuery { sector?: string } -export type SelectionSectorType = "concept" | "industry" - -export const selectionSectorTypes = ["concept", "industry"] as const +export type SelectionSectorType = "industry" export interface SelectionSectorAggregate { sector_code: string diff --git a/zhixing-web/src/features/selection/components/selection-results-workbench.tsx b/zhixing-web/src/features/selection/components/selection-results-workbench.tsx index 18b638a..b1899e0 100644 --- a/zhixing-web/src/features/selection/components/selection-results-workbench.tsx +++ b/zhixing-web/src/features/selection/components/selection-results-workbench.tsx @@ -79,7 +79,7 @@ export function SelectionResultsWorkbench({ visibleStocks[0] const sectorSelectItems = useMemo( () => [ - { label: "全部板块", value: ALL_SECTORS_VALUE }, + { label: "全部细分行业", value: ALL_SECTORS_VALUE }, ...sectorOptions.map((option) => ({ label: option.sector_name, value: option.sector_code, @@ -158,14 +158,14 @@ export function SelectionResultsWorkbench({ value={sector ?? ALL_SECTORS_VALUE} > - + - 全部板块 + 全部细分行业 {sectorOptions.map((option) => ( { }) }) - it("shows the stock industry and concept boards from the membership query", () => { + it("shows the stock sub-industries from the membership query", () => { useStockSectorMembership.mockReturnValue({ data: { status: "success", @@ -69,42 +69,12 @@ describe("SignalDetailPanel", () => { render() - expect(screen.getByText("行业:银行")).toBeInTheDocument() - expect(screen.getByText("板块")).toBeInTheDocument() - expect(screen.getByText("机器人")).toBeInTheDocument() - expect(screen.getByText("人工智能")).toBeInTheDocument() - expect(screen.queryByText(/等 \d+ 个/)).not.toBeInTheDocument() + expect(screen.getByText(/细分行业:银行/)).toBeInTheDocument() + expect(screen.queryByText("机器人")).not.toBeInTheDocument() + expect(screen.queryByText("人工智能")).not.toBeInTheDocument() }) - it("caps visible concepts and reports the total", () => { - const concepts = Array.from({ length: 10 }, (_, index) => ({ - code: `BK${String(index + 1).padStart(4, "0")}.DC`, - name: `板块${index + 1}`, - })) - useStockSectorMembership.mockReturnValue({ - data: { - status: "success", - ts_code: "000001.SZ", - requested_trade_date: "2026-08-28", - trade_date: "2026-08-28", - industries: [], - concepts, - concept_total: 10, - concept_limit: 20, - }, - isError: false, - isPending: false, - }) - - render() - - expect(screen.getByText("板块1")).toBeInTheDocument() - expect(screen.getByText("板块8")).toBeInTheDocument() - expect(screen.queryByText("板块9")).not.toBeInTheDocument() - expect(screen.getByText("等 10 个")).toBeInTheDocument() - }) - - it("hides the sector rows when the membership query returns no data", () => { + it("hides the sub-industry row when the membership query returns no data", () => { useStockSectorMembership.mockReturnValue({ data: { status: "no_data", @@ -122,7 +92,6 @@ describe("SignalDetailPanel", () => { render() - expect(screen.queryByText(/行业:/)).not.toBeInTheDocument() - expect(screen.queryByText("板块")).not.toBeInTheDocument() + expect(screen.queryByText(/细分行业:/)).not.toBeInTheDocument() }) }) diff --git a/zhixing-web/src/features/selection/components/signal-detail-panel.tsx b/zhixing-web/src/features/selection/components/signal-detail-panel.tsx index ea4b1a0..f340a24 100644 --- a/zhixing-web/src/features/selection/components/signal-detail-panel.tsx +++ b/zhixing-web/src/features/selection/components/signal-detail-panel.tsx @@ -19,8 +19,6 @@ const SelectionChart = lazy(() => })), ) -const displayedConceptCount = 8 - interface SignalDetailPanelProps { stock: SelectionStockResult | undefined } @@ -71,16 +69,13 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) { ] const membership = - membershipQuery.data?.status === "success" ? membershipQuery.data : undefined + membershipQuery.data?.status === "success" + ? membershipQuery.data + : undefined const industries = membership?.industries ?? [] - const concepts = membership?.concepts ?? [] - const extraConceptCount = Math.max( - 0, - concepts.length - displayedConceptCount, - ) - const sectorSnapshotTitle = + const industrySnapshotTitle = membership?.trade_date != null - ? `板块成分截至 ${membership.trade_date}` + ? `细分行业成分截至 ${membership.trade_date}` : undefined return ( @@ -138,30 +133,13 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {

{industries.length > 0 && ( -

- 行业:{industries.map((industry) => industry.name).join("、")} -

- )} - {concepts.length > 0 && ( -
- 板块 - {concepts.slice(0, displayedConceptCount).map((concept) => ( - - {concept.name} - - ))} - {extraConceptCount > 0 && ( - - 等 {concepts.length} 个 - - )} -
+ 细分行业: + {industries.map((industry) => industry.name).join("、")} +

)}
diff --git a/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx b/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx index b6464f0..b949f03 100644 --- a/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx +++ b/zhixing-web/src/features/selection/pages/selection-results-page.test.tsx @@ -543,7 +543,7 @@ describe("SelectionResultsPage", () => { }) }) - it("filters by sector and shows per-sector counts", async () => { + it("filters by sub-industry and shows per-industry counts", async () => { useSelectionResultSectors.mockReturnValue({ data: { strategy: "zhixing_b1", @@ -551,10 +551,10 @@ describe("SelectionResultsPage", () => { run_id: "run-1", status: "success", snapshot_trade_date: "2026-08-08", - sector_type: "concept", + sector_type: "industry", sectors: [ - { sector_code: "BK0001.DC", sector_name: "机器人", stock_count: 3 }, - { sector_code: "BK0003.DC", sector_name: "数字经济", stock_count: 2 }, + { sector_code: "BK0475.DC", sector_name: "银行", stock_count: 3 }, + { sector_code: "BK0477.DC", sector_name: "保险", stock_count: 2 }, ], }, isError: false, @@ -562,16 +562,16 @@ describe("SelectionResultsPage", () => { }) render() - const sectorTrigger = screen.getByRole("combobox", { name: "筛选板块" }) - expect(sectorTrigger).toHaveTextContent("全部板块") + const sectorTrigger = screen.getByRole("combobox", { name: "筛选细分行业" }) + expect(sectorTrigger).toHaveTextContent("全部细分行业") fireEvent.click(sectorTrigger) - const robotOption = await screen.findByRole("option", { - name: /机器人/, + const bankOption = await screen.findByRole("option", { + name: /银行/, }) - expect(robotOption).toHaveTextContent("3") - fireEvent.pointerDown(robotOption, { pointerType: "mouse" }) - fireEvent.click(robotOption) + expect(bankOption).toHaveTextContent("3") + fireEvent.pointerDown(bankOption, { pointerType: "mouse" }) + fireEvent.click(bankOption) expect(routerNavigate).toHaveBeenCalled() const lastCall = @@ -579,7 +579,7 @@ describe("SelectionResultsPage", () => { const searchUpdate = lastCall[0].search as (previous: { sector?: string }) => Record - expect(searchUpdate({})).toEqual({ sector: "BK0001.DC" }) + expect(searchUpdate({})).toEqual({ sector: "BK0475.DC" }) }) it("requests database-backed score sorting", async () => {