42 lines
5.3 KiB
Markdown
42 lines
5.3 KiB
Markdown
|
|
# 选股页面迭代:上搜索 + 左列表/右详情布局 & 板块筛选
|
|||
|
|
|
|||
|
|
## 现状与关键结论
|
|||
|
|
|
|||
|
|
- 布局:`selection-results-workbench.tsx` 目前是左右两栏 grid(左 320px 筛选+列表 / 右详情),筛选栏嵌在左栏顶部。
|
|||
|
|
- 板块数据:选股结果表(`selection_run_item`)没有板块字段;板块归属在 `sector_radar_membership` 表,现有 membership 接口只支持单股查询。**板块聚合必须由后端新增**(前端逐股请求既慢又只覆盖已加载分页,计数不准)。
|
|||
|
|
- 已确认口径(用户未答,按推荐执行):概念板块(`sector_type='concept'`,与详情面板"板块"标签一致),单选;接口保留 `sector_type` 参数,后续扩展行业零成本。排序 = 按 `stock_count` 倒序(选项旁展示数量)。
|
|||
|
|
|
|||
|
|
## 一、后端(zhixing-server)
|
|||
|
|
|
|||
|
|
遵循 bounded-context-first(ADR 0001):selection 不直接 join sector_radar 表,通过应用层端口调用 sector_radar 的读服务。
|
|||
|
|
|
|||
|
|
1. **sector_radar 模块**(`application/read.py` + `infrastructure/postgres.py`):
|
|||
|
|
- 仓储新增两个只读方法(复用已有的 `get_last_good_publication` 解析快照日):
|
|||
|
|
- `load_sector_counts(stock_codes, snapshot_date, sector_type)`:`SELECT sector_code, sector_name, COUNT(*) FROM sector_radar_membership WHERE stock_code = ANY(%s) AND trade_date = %s AND sector_type = %s AND membership_status='available' GROUP BY 1,2`
|
|||
|
|
- `load_sector_member_codes(snapshot_date, sector_code, sector_type)`:返回该板块成员股票代码列表
|
|||
|
|
- 读服务 `ReadSectorRadar` 新增:`sector_counts(stock_codes, trade_date, sector_type)`(内部解析 last-good publication,返回按 stock_count 降序、名称升序)和 `sector_member_codes(trade_date, sector_code, sector_type)`。
|
|||
|
|
2. **selection 模块**:
|
|||
|
|
- `domain/runs.py`:`SelectionResultQuery` 增加 `sector: str | None`;新增 `SelectionSectorCount(sector_code, sector_name, stock_count)` 值对象;定义端口协议 `SectorMembershipReader`(`sector_counts` / `sector_member_codes` 两个方法)。
|
|||
|
|
- `application/run.py`:新增 `list_sector_counts(strategy, target_trade_date, sector_type)` —— 取 latest run,收集 `status='selected' AND signal_count>0` 的 ts_code,调端口聚合;`get_latest`/`get` 结果查询在 `query.sector` 有值时先调端口取成员代码,空则直接返回空页,否则把代码数组传入仓储。
|
|||
|
|
- `infrastructure/postgres_runs.py`:`_stock_filter` 增加子句 `item.ts_code = ANY(%s)`(参数由应用层传入)。
|
|||
|
|
- `presentation/http.py`:
|
|||
|
|
- 新端点 `GET /api/v1/selection/sectors?strategy=&target_trade_date=§or_type=` → `{ sector_type, snapshot_trade_date, sectors: [{sector_code, sector_name, stock_count}] }`
|
|||
|
|
- `GET /results` 与 `GET /runs/{id}` 增加 `sector` query 参数校验(去空格、限长)。
|
|||
|
|
- 组合根(router/依赖装配处)把 sector_radar 的读服务适配为 selection 的端口注入。
|
|||
|
|
3. **测试**:`tests/unit/sector_radar/`(新仓储方法)、`tests/unit/selection/test_postgres_runs.py`(sector 过滤、ANY 数组、空成员空页)、`tests/test_selection_http.py`(新端点契约 + results 带 sector)。跑 `./dev.sh check`(ruff/pyright/pytest)。
|
|||
|
|
|
|||
|
|
## 二、前端(zhixing-web)
|
|||
|
|
|
|||
|
|
1. **布局重构** `selection-results-workbench.tsx`:
|
|||
|
|
- 外层改为 `flex flex-col`:顶部一个全宽 section 放搜索栏(关键词 / 信号分类 / **板块(新增)** / 排序 / 筛选结果计数),带 `rounded-md border bg-card` 与现有一致;
|
|||
|
|
- 下方 `md:grid md:grid-cols-[320px_minmax(0,1fr)]` 左列表右详情;移动端纵向堆叠为 搜索 → 列表 → 详情。
|
|||
|
|
2. **类型与 API**(`selection.types.ts` / `selection.api.ts`):新增 `SelectionSectorAggregate`;`SelectionResultsQuery` 加 `sector?`;`getSelectionResultSectors()`;`buildSelectionQueryParams` 带 sector。
|
|||
|
|
3. **查询层**(`selection.query.ts`):`useSelectionResultSectors`(key:strategy+date,结果就绪后启用);`selectionResultsQueryKey`/`selectionRunQueryKey` 加入 sector 使筛选变化触发重新请求。
|
|||
|
|
4. **路由**(`routes/route-tree.tsx`):selectionRoute `validateSearch` 增加 `sector`(string,限长,默认 undefined)。
|
|||
|
|
5. **页面接线**(`selection-results-page.tsx`):把 URL 中的 sector 传入 `useSelectionResults`;页面层调用 `useSelectionResultSectors` 并把聚合结果传给 workbench。
|
|||
|
|
6. **workbench 板块下拉**:选项 = "全部板块" + 聚合数据,item 渲染 `名称 + 数量徽标`(数量 tabular-nums,倒序由后端保证);选择写 URL;聚合加载后若当前 sector 不在列表中(如切换策略/日期)自动重置为全部;板块无数据时下拉仅剩"全部板块"并禁用。
|
|||
|
|
|
|||
|
|
## 三、执行方式
|
|||
|
|
|
|||
|
|
- 按仓库 Trellis 工作流建任务目录并加载 `.trellis/spec/backend`(selection.md 契约:查询不触发重算、错误矩阵等)与前端规范后再动手;先后端(接口+测试)再前端接线,最后 `./dev.sh check` + 前端 lint/tsc 全量质量门禁。
|
|||
|
|
- 语义说明:板块数量 = 当次 run 全部选中股票中归属该板块的数量(不随关键词/信号分类变化);列表"筛选结果 N 只" = 包含板块在内的全部过滤叠加后的 `stocks_total`。
|