geopro/docs/superpowers/specs/2026-06-10-object-selection...

263 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 对象单击/勾选 驱动 数据列表·异常(含异常体)·属性 面板(接真实 API— 设计文档
- 日期2026-06-10
- 分支main建议拉子分支 `feat/object-selection-panels`
- 状态:已与需求方确认范围与关键决策,待 spec 评审
- 上游:本轮是 `docs/superpowers/specs/2026-06-09-real-api-navigation-design.md` §12.1「下一轮」的**聚焦子集**——只覆盖左下「数据集」列表与右侧「异常 / 对象属性 / 数据集属性」面板,**不含**中央 2D/3D 与「数据详情」的 VTK 真实剖面渲染。
- 参考:`D:\Projects\GEOPRO\Geopro3.0 菜单.xlsx`「视图定义」表;`docs/apis/business_OpenAPI.json`。
---
## 1. 背景与目标
`real-api-navigation` 轮已把顶层导航壳(工作空间/项目/对象树接到真实后端并把渲染与导航解耦对象树已显示真实结构项目根→GS→TM叶子=TM 可勾选),单击 TM 已能列出其 DS数据/文件分页)。但 `tmCheckToggled`、右上「异常」(本地 `core::Anomaly` 占位)、「对象属性」「数据集属性」面板均为**占位**。
本轮补齐三件交互,全部接**真实业务 API**(不回退本地样本):
1. **单击对象GS 或 TM** → ① 左下数据列表显示其下所有 DSGS=该工区全部 DSTM=该测线 DS② 右上「对象属性」显示该对象动态表单详情。当前单击行高亮(单选)。
2. **勾选 / 反勾选 GS/TM** → 右上「对象异常」面板显示**所有勾选对象**下的「**异常 + 异常体**」树(见 §2 决策、§7.3)。
3. **单击数据列表某 DS** → 右下「数据集属性」显示该 DS 动态表单详情。
**非目标(仍占位/推迟)**:中央 2D/3D 与「数据详情」dd/ert 真实剖面渲染;异常项「眼睛(显隐)」(联动 VTK与 VTK 同轮接);异常体的「拖拽合并/编辑」(写操作);异步仓储;文件下载动作。`render/*`、`LocalSampleRepository`、旧 `core::Anomaly` 渲染路径**保留不删**。
## 2. 关键决策(需求方已拍板)
| # | 决策点 | 结论 |
|---|---|---|
| D1 | 三面板数据来源 | **接真实业务 API**(新增仓储方法 + DTO 映射),不复用本地样本 |
| D2 | 勾选 GS 行为 | **联动**:勾 GS 自动勾其下所有 TM子 TM 可单独取消GS 转半选态);取消联动取消其异常 |
| D3 | 「对象属性」跟随谁 | **跟随当前单击高亮的行**(与勾选集相互独立) |
| D4 | 属性展示深度 | **完整动态表单**(分组键值);字段显示名取自 API`FormItemVO.fieldName` |
| D5 | 异常面板含异常体 | **只读树**:「对象 → 异常体(ID/类型) → 异常(ID/类型)」+ 未合并的「独立异常」单列;**拖拽合并/编辑推迟** |
| D6 | 详情展开 / 眼睛 | **详情展开做**(异常叶子可展开看其详情,内联已加载字段);**眼睛(显隐)推迟**(随 VTK 同轮) |
**D2 的连锁简化**:勾选源只数 **TM 叶子**GS 仅作批量开关,且无 GS 级异常接口)→ GS→TM 展开由 Qt 原生三态复选框承担、控制器只读叶子集;聚合 = 被勾选 TM 异常/异常体的并集;**无去重问题**(叶子唯一)。
## 3. 接口映射
网关与会话沿用现有 `ApiClient`,成功判定 `code==200`
| 能力 | 方法 | 路径 | 要点 |
|---|---|---|---|
| 数据列表GS/TM 级) | POST | `/business/dsObject/data/page` | body `{projectId, structParentId, structParentConfType, classifyTypeList:[3], pageNo, pageSize:5}``structParentConfType`**TM=2已固化GS=1待联调验证**。返回沿用 `DsPage` |
| 文件列表GS/TM 级) | POST | `/business/dsObject/file/page` | body 同上 `classifyTypeList:[1]` |
| 对象详情(GS) | GET | `/business/gsObject/getGsObjectDetail/{gsId}` | `data: DynamicFormVO` |
| 对象详情(TM) | GET | `/business/tmObject/getDetail/{tmObjectId}` | `data: DynamicFormVO` |
| 数据集详情 | GET | `/business/dsObject/dynamicForm/{dsObjectId}` | `data: DynamicFormVO`(与对象详情**同结构**,复用渲染) |
| 异常(按 TM | GET | `/business/exception/queryExceptionByTmObjectId/{tmObjectId}` | `data: [ExceptionVO]`。**异常的「异常体归属」字段需 live 验证**(候选:`parentId`+`parentConfType`,或专有 consortium 字段) |
| 异常体树(项目级,备用) | GET | `/business/exceptionConsortium/getExceptionConsortiumTree/{projectId}` | `data: [{id, name, type, exceptionTypeId, parentId, status, ...}]`(项目级,不按对象过滤;作 §9 回退用) |
**`DynamicFormVO`**`{ name, confCode, description, formList:[FormVO], properties:object }``FormVO{ groupName, groupSort, values:[FormItemVO] }``FormItemVO{ fieldName, fieldCode, displaySort }``properties` 为 `{fieldCode: value}`。渲染:组按 `groupSort`、字段按 `displaySort`,逐项 `fieldName : properties[fieldCode]`
**`ExceptionVO` 关键字段**`id, exceptionName, exceptionTypeName, exceptionMarkType, exceptionMarkTypeName, createTime, remark, location, latitudeLongitude, geographicalCoordinates, elevationList, zlist, parentId, parentConfType, type`。
## 4. 架构分层
```
UI(app) ObjectTreePanel(三态勾选) DynamicFormView(新) ObjectExceptionPanel(新树面板)
│ 信号(单击/勾选) ▲ 槽(模型)
controller WorkbenchNavController ← selectObject / setCheckedTms / selectDataset
│ IProjectRepository(同步契约)
data ApiProjectRepository + NavDto(parseDynamicForm/parseExceptions/groupExceptionsByConsortium)
│ ApiClient
net ApiClient / AuthService复用不改
模型(RepoTypes.hpp)+ DynamicForm 系列 / ExceptionRow / ConsortiumGroup / ObjectExceptionGroup
```
## 5. 数据 / 模型层
### 5.1 模型(`src/data/repo/RepoTypes.hpp`,纯结构,无 Qt/VTK
```cpp
// 动态表单GS/TM/DS 详情统一模型)。
struct DynamicFormField { std::string name, value; };
struct DynamicFormGroup { std::string name; std::vector<DynamicFormField> fields; };
struct DynamicForm { std::string name; std::vector<DynamicFormGroup> groups; }; // 已按 sort 排好
// 异常树叶本轮只读。detailSummary = 详情展开内联显示的派生摘要(坐标/高程/标记类型/备注)。
struct ExceptionRow {
std::string id, name, typeName, createTime;
std::string consortiumId, consortiumName, consortiumType; // 异常体归属(空=独立异常)
std::string detailSummary; // 详情展开用(已加载字段拼接)
};
// 异常体分组(树中间层)。
struct ConsortiumGroup { std::string id, name, typeName; std::vector<ExceptionRow> exceptions; };
// 对象分组(树根层,对应一个被勾选 TM
struct ObjectExceptionGroup {
std::string objectId, objectName;
std::vector<ConsortiumGroup> consortia; // 该对象下的异常体(含其异常)
std::vector<ExceptionRow> looseExceptions; // 未合并进异常体的独立异常
};
```
### 5.2 DTO`src/data/dto/NavDto.{hpp,cpp}`,纯函数,可单测)
```cpp
DynamicForm parseDynamicForm(const QJsonObject& data); // formList(定义)+properties(值) 合并
std::vector<ExceptionRow> parseExceptions(const QJsonArray& arr); // ExceptionVO→ExceptionRow含 consortium* + detailSummary
// 把一个对象TM的异常行按 consortiumId 分组成「异常体列表 + 独立异常列表」。纯函数、可单测。
// - 同 consortiumId 归一组(组名/类型取首个非空 consortiumName/Type
// - consortiumId 为空 → looseExceptions。
struct GroupedExceptions { std::vector<ConsortiumGroup> consortia; std::vector<ExceptionRow> loose; };
GroupedExceptions groupExceptionsByConsortium(const std::vector<ExceptionRow>& rows);
```
> `detailSummary` 在 `parseExceptions` 内由 `exceptionMarkTypeName / createTime / 坐标(latitudeLongitude 或 location 质心) / 高程(elevationList/zlist 极值) / remark` 拼成可读多行串(无额外请求)。
### 5.3 数据访问层 `IProjectRepository` 扩展(`src/data/repo/IProjectRepository.hpp`
```cpp
// 既有 loadTmRows 泛化structParentConfType 由硬编码 2 改入参(支持 GS=1
virtual RepoResult<DsPage> loadRows(const std::string& projectId, const std::string& parentId,
int parentConfType, int classifyType, int pageNo) = 0;
// 对象详情:按 confType 选端点1=GS getGsObjectDetail2=TM tmObject/getDetail
virtual RepoResult<DynamicForm> loadObjectDetail(const std::string& objectId, int confType) = 0;
// 数据集详情dsObject/dynamicForm/{dsObjectId}。
virtual RepoResult<DynamicForm> loadDatasetForm(const std::string& dsObjectId) = 0;
// 单 TM 异常含异常体归属字段exception/queryExceptionByTmObjectId/{tmObjectId}。
virtual RepoResult<std::vector<ExceptionRow>> loadExceptionsByTm(const std::string& tmObjectId) = 0;
```
`ApiProjectRepository` 按 §3 实现id 进 URL 前百分号编码;错误归一为 `RepoResult{ok=false,error=msg}`
## 6. 逻辑层 `WorkbenchNavController`
```cpp
public slots:
void selectObject(const QString& objectId, int confType); // 单击对象→DS列表(loadRows)+对象详情
void setCheckedTms(const QStringList& tmObjectIds); // 勾选叶子集→逐TM异常→分组→异常树
void selectDataset(const QString& dsObjectId); // 单击DS→dynamicForm
void loadMoreData(); void loadMoreFiles(); // 用泛化 parent 续页
signals:
void objectDetailLoaded(const QString& title, const geopro::data::DynamicForm& form);
void exceptionTreeLoaded(const std::vector<geopro::data::ObjectExceptionGroup>& groups, int tmCount);
void datasetDetailLoaded(const geopro::data::DynamicForm& form);
private:
std::string currentParentId_; int currentParentConfType_ = 0; // 加载更多用(替代 currentTmId_
std::vector<data::StructNode> lastStructNodes_; // tmId→name 解析
std::map<std::string, std::vector<data::ExceptionRow>> tmExceptionCache_; // per-TM 异常缓存
```
- `selectObject`:置 `currentParentId_/ConfType_`、页码归 1`loadRows` 拉数据+文件首页emit `datasetsLoaded/filesLoaded`),再 `loadObjectDetail` → emit `objectDetailLoaded`
- `setCheckedTms`:对集合每个 TM——命中 `tmExceptionCache_` 复用、否则 `loadExceptionsByTm` 入缓存;对每个 TM 调 `groupExceptionsByConsortium` 装配一个 `ObjectExceptionGroup`objectName 由 `lastStructNodes_` 解析emit `exceptionTreeLoaded(groups, tmCount)`。空集 → emit 空(面板回占位)。
- `selectDataset``loadDatasetForm` → emit `datasetDetailLoaded`
- 切项目/工作空间:清 `tmExceptionCache_`、`currentParentId_`;经既有 `structureLoaded` 接线清空各面板。
- 沿用 `BusyGuard` 重入保护 + `busyChanged`(同步阻塞 + WaitCursor。`structureLoaded` 时控制器留存 `lastStructNodes_`
## 7. UI 层 `app`
### 7.1 `ObjectTreePanel`(三态勾选 + GS 可单击)
- **GS**`Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate`,存 `confType=1` 角色,可单击。
- **TM 叶子**`Qt::ItemIsUserCheckable`,存 `confType=2` + `tmObjectId`(沿用现有角色)。
- **级联**Qt 原生三态——勾 GS 自动勾全部子 TM子 TM 取消 → GS 转 `PartiallyChecked`
- **信号**`objectClicked(QString objectId, int confType)`(取代 `tmClicked``checkedTmsChanged(QStringList tmObjectIds)`(取代 `tmCheckToggled`,发射**当前全部被勾选的 TM 叶子 id**)。
- **合并发射**GS 级联会对每个子项各触发 `itemChanged`。用 `QTimer::singleShot(0,…)` 合并:标脏 → 回合一次性遍历收集勾选叶子 → 发一次 `checkedTmsChanged`(避免一次点击触发 N 次重算/N 组请求)。
### 7.2 `DynamicFormView`(新增,共享键值渲染器)
- `src/app/panels/DynamicFormView.{hpp,cpp}``QWidget``setForm(const DynamicForm&)` / `showMessage(QString)`
- 布局:`QScrollArea` 内每组「组标题 + `QFormLayout`(左字段名/右值,值可换行)」;无数据→居中占位。主题化随 `ThemeManager::changed` 重绘。
- 「对象属性」「数据集属性」两面板各持一个实例(取代现有两占位 `QLabel`)。
### 7.3 `ObjectExceptionPanel`(新增,异常+异常体 只读树)
替代右上「异常」Tab 内原扁平列表(原 `QListWidget`),改为 `QTreeWidget`
```
对象ATM 名)
├─ ▸ 异常体X异常体类型 ← 中间层ConsortiumGroup
│ ├─ 异常1异常类型 ← 叶子ExceptionRow
│ │ └─ ▸ 详情:标记类型/坐标/高程/创建时间/备注 ← D6 详情展开(内联,懒构建)
│ └─ 异常2异常类型
├─ 独立异常(未合并) ← looseExceptions 分组节点
│ └─ 异常3异常类型
对象BTM 名)…
```
- `void setGroups(const std::vector<ObjectExceptionGroup>&)`:重建树;`showMessage(QString)` 空/错占位。
- **详情展开D6**:异常叶子下挂一个「详情」子节点,显示 `ExceptionRow.detailSummary`(已加载字段,无额外请求);默认折叠。
- **眼睛/显隐(推迟)**:不画眼睛列(无 VTK 可联动)。
- 顶部数量徽标:填异常总数(跨所有对象组),空则隐藏。
- 主题化随 `ThemeManager::changed`
-`AnomalyListPanel``populateAnomalyList(core::Anomaly)` + `AnomalyCardDelegate` + 眼睛 + VTK 联动)**整体保留不删**,留未来「数据详情真实渲染 + 眼睛联动」轮;本轮不接线。
### 7.4 `main.cpp` 接线(增量)
- `objectTree.objectClicked → nav.selectObject(id, confType)``nav.objectDetailLoaded → 对象属性 DynamicFormView.setForm`。
- `objectTree.checkedTmsChanged → nav.setCheckedTms(tmIds)``nav.exceptionTreeLoaded → ObjectExceptionPanel.setGroups + 徽标`。
- `datasetList.itemClicked`(非「加载更多」行)→ `nav.selectDataset(dsId)`;移除现写入 `propLabel` 的占位文案。
- `nav.datasetDetailLoaded → 数据集属性 DynamicFormView.setForm`
- `structureLoaded`(切项目/空间):清空对象属性、数据集属性两 `DynamicFormView` 与异常树/徽标(回占位)。
- 既有 `selectTm` 接线点替换为 `selectObject``loadMore*` 接线不变(控制器内部改用泛化 parent
- 右上 Tab 面板:把原 `anomalyList(QListWidget)` 槽位替换为 `ObjectExceptionPanel`「对象属性」Tab 内容替换为 `DynamicFormView`
## 8. 交互时序
```
单击对象(GS/TM): objectClicked(id, confType)
→ selectObject: loadRows×2(数据+文件首页)→datasetsLoaded/filesLoaded→左下
loadObjectDetail(id,confType)→objectDetailLoaded→右上「对象属性」
勾选/取消(GS三态级联到TM叶子): 合并→checkedTmsChanged([tm...])
→ setCheckedTms: 逐TM(缓存)查异常→groupExceptionsByConsortium→[ObjectExceptionGroup]
→ exceptionTreeLoaded → 右上「对象异常」树(对象→异常体→异常 + 独立异常)+ 徽标
取消某TM/GS → 叶子集变小 → 重算 → 其异常/异常体自动消失)
单击数据集: itemClicked(dsId)
→ selectDataset: loadDatasetForm(dsId)→datasetDetailLoaded→右下「数据集属性」
切项目/空间: structureLoaded → 清空各面板 + 勾选集 + 异常缓存
```
## 9. 边界与错误处理
- **GS confType=1 待联调**:若 `data/page``structParentConfType=1` 下不递归返回 GS 全部 DS回退对 GS 直接子 TM 逐个 `data/page` 合并(分页退化为按 TM 分段或一次拉全)。实现前 live 验证。
- **异常体归属字段待联调(关键)**`groupExceptionsByConsortium` 依赖 `ExceptionRow.consortiumId`,其来源字段需 live 验证(候选 `ExceptionVO.parentId`+`parentConfType`,或专有字段)。
- 回退:若单 TM 异常不带异常体归属,则改用项目级 `getExceptionConsortiumTree/{projectId}` 建「异常id→异常体」映射再对勾选 TM 的异常套用该映射分组;仍无 → 全部作独立异常列出(保证面板可用,异常体层为空)。
- `consortiumName/Type` 来源同样 live 验证(异常 payload 的 `parentName`,或异常体树/`getDetail`)。
- **聚合性能**:同步多请求 + WaitCursor`tmExceptionCache_` 避免增量勾选重复请求;切项目清缓存。
- **空/错状态**:各面板无数据→「暂无…」占位;失败→`loadFailed(stage,msg)` 状态栏提示 + 面板错误占位;**不回退本地样本**。
- **输入边界**id 为空短路URL 中 id 百分号编码。**重入**:沿用 `busy_` 保护。
## 10. 测试策略
纯逻辑单测GoogleTest + CTest沿用 `tests/data/test_nav_dto.cpp`
- `parseDynamicForm`:分组+值合并、`groupSort/displaySort` 排序、缺失值→空、空 `formList`、`name` 透传。
- `parseExceptions`:字段映射、`detailSummary` 拼接、`consortium*` 提取、空数组、缺字段容错。
- `groupExceptionsByConsortium`:同 consortiumId 归组、空 consortiumId→loose、组名取首个非空、空输入、全独立、全归属。
- UI 三态级联 / 控制器装配:依赖 Qt/live手动联调验证无桩
## 11. 文件清单
**改造**
- `src/data/repo/RepoTypes.hpp``+ DynamicForm 系列 / ExceptionRow / ConsortiumGroup / ObjectExceptionGroup`
- `src/data/dto/NavDto.{hpp,cpp}``+ parseDynamicForm / parseExceptions / groupExceptionsByConsortium`
- `src/data/repo/IProjectRepository.hpp``loadTmRows`→`loadRows` 泛化;`+ loadObjectDetail / loadDatasetForm / loadExceptionsByTm`
- `src/data/api/ApiProjectRepository.{hpp,cpp}` — 实现新方法
- `src/controller/WorkbenchNavController.{hpp,cpp}``+ selectObject / setCheckedTms / selectDataset / 三新信号 / per-TM 缓存 / lastStructNodes_``selectTm` 退役、`loadMore*` 改泛化 parent
- `src/app/panels/ObjectTreePanel.{hpp,cpp}` — GS 三态可勾选+可单击;信号改 `objectClicked / checkedTmsChanged`(合并发射)
- `src/app/main.cpp` — 各面板接线§7.4);移除 DS 单击占位文案;右上 Tab 槽位替换为新面板
- `tests/data/test_nav_dto.cpp``+ parseDynamicForm / parseExceptions / groupExceptionsByConsortium` 用例
**新增**
- `src/app/panels/DynamicFormView.{hpp,cpp}` — 共享键值渲染器(对象属性 + 数据集属性)
- `src/app/panels/ObjectExceptionPanel.{hpp,cpp}` — 异常+异常体 只读树面板
**保留不删**`LocalSampleRepository`、`render/*`、`AnomalyListPanel``populateAnomalyList`/`AnomalyCardDelegate`/眼睛/VTK 联动)、中央/详情渲染代码(留未来 dd/ert 渲染 + 眼睛联动轮)。
## 12. 未决 / 验证点(实现前用 live 接口确认)
1. GS 级 `data/page``structParentConfType` 取值与是否递归返回全部 DS§9 回退)。
2. `DynamicFormVO.properties` 的索引键(`fieldCode` 假设)。
3. **异常的「异常体归属」字段**`consortiumId/Name/Type` 来源):单 TM 异常 payload 是否自带否则走项目级异常体树映射§9 回退)。
4. `exceptionConsortium` 各字段命名与「异常体类型」取值来源(`exceptionTypeId` vs 类型名)。
## 13. 后续轮(本轮明确不做)
- 异常体**拖拽合并/编辑**`exceptionConsortium` 新增/关联/取消关联/删除写操作)。
- 异常项**眼睛(显隐)** 联动中央/详情视图——随 dd/ert 真实剖面渲染同轮接入。
- 中央 2D/3D 与「数据详情」真实数据渲染、项目 CRS 替换、异步仓储、文件下载。