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

259 lines
17 KiB
Markdown
Raw 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 真实剖面渲染。
---
## 1. 背景与目标
`real-api-navigation` 轮已把**顶层导航壳**(工作空间 / 项目 / 对象树)接到真实后端,并把渲染与导航解耦:
- 对象树 `ObjectTreePanel` 已显示真实结构(项目根 → GS → TM叶子=TM 可勾选)。
- 单击 TM`tmClicked`)→ `WorkbenchNavController::selectTm` → 左下「数据集」列出该 TM 的 DS数据/文件两页签,分页)。
- `tmCheckToggled` 是**前瞻钩子,本轮前无消费者**`loadDataset`、右上「异常」`AnomalyListPanel`(本地 `core::Anomaly`)、右上「对象属性」与右下「数据集属性」面板均为**占位**。
本轮补齐三件被推迟的交互,全部接**真实业务 API**(不回退本地样本):
1. **单击对象GS 或 TM** → ① 左下数据列表显示其下所有 DSGS=该工区全部 DSTM=该测线 DS② 右上「对象属性」显示该对象动态表单详情。当前单击行高亮(单选)。
2. **勾选 / 反勾选 GS/TM** → 右上「异常」列表显示**所有被勾选 TM 叶子**下异常的聚合并集。
3. **单击数据列表某 DS** → 右下「数据集属性」显示该 DS 动态表单详情。
**非目标(仍占位,留后续轮)**:中央 2D/3D 与「数据详情」的 dd/ert 真实剖面/反演渲染;异步仓储;文件下载动作。`render/*` 与 `LocalSampleRepository` 代码保留不删。
## 2. 关键决策(需求方已拍板)
| # | 决策点 | 结论 |
|---|---|---|
| D1 | 三面板数据来源 | **接真实业务 API**(新增仓储方法 + DTO 映射),不复用本地样本 |
| D2 | 勾选 GS 行为 | **联动**:勾 GS 自动勾其下所有 TM子 TM 可单独取消GS 转半选态);取消联动取消其异常 |
| D3 | 「对象属性」跟随谁 | **跟随当前单击高亮的行**(与勾选集相互独立) |
| D4 | 属性展示深度 | **完整动态表单**(分组键值);字段显示名**取自 API**`FormItemVO.fieldName`),无需推断 |
**D2 的连锁简化**:因勾选源只数 **TM 叶子**GS 仅作批量开关,且无 GS 级异常接口),
- 「GS→后代 TM」的展开由 Qt 原生三态复选框承担(`ItemIsAutoTristate` 级联),控制器只读叶子集;
- 聚合 = 被勾选 TM 叶子异常的并集(功能本身固有);
- **不存在去重问题**(叶子 id 唯一)。
## 3. 接口映射
网关与会话沿用现有 `ApiClient``http://tenant.geomative.cn/pop-api`token 已注入)。成功判定 `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]`。**无 GS/项目级异常接口** → GS 勾选靠级联到 TM 叶子解决 |
**`DynamicFormVO` 形状**`{ name, confCode, description, formList:[FormVO], properties:object, ... }`。
- `FormVO{ groupName, groupSort, values:[FormItemVO] }``FormItemVO{ fieldName(显示名), fieldCode, displaySort, ... }`。
- `properties``{ fieldCode: value }` 键值对(值容器;**待联调确认确实按 fieldCode 索引**)。
- 渲染规则:按 `groupSort` 排组、组内按 `displaySort` 排字段,逐项显示 `fieldName : properties[fieldCode]`
**`ExceptionVO` 关键字段**`id, exceptionName, exceptionTypeName, exceptionMarkTypeName, createTime, remark, location, latitudeLongitude, geographicalCoordinates, parentId, parentConfType`。
## 4. 架构分层
沿用既有四层依赖单向向下UI 不直接碰 `ApiClient`
```
UI(app) ObjectTreePanel(三态勾选) DynamicFormView(新) AnomalyListPanel(+异常行渲染)
│ 信号(单击/勾选) ▲ 槽(模型)
controller WorkbenchNavController ← selectObject / setCheckedTms / selectDataset
│ IProjectRepository(同步契约)
data ApiProjectRepository + NavDto(parseDynamicForm/parseExceptions)
│ ApiClient
net ApiClient / AuthService复用不改
模型(RepoTypes.hpp)+ DynamicForm / DynamicFormGroup / DynamicFormField / ExceptionRow
```
## 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 排好序
};
// 异常列表行(右上「异常」面板用;列表展示,本轮不联动 VTK
struct ExceptionRow {
std::string id, name, typeName, markTypeName, createTime, remark;
};
```
### 5.2 DTO`src/data/dto/NavDto.{hpp,cpp}`,纯函数,可单测)
```cpp
// DynamicFormVO 对象 → DynamicForm合并 formList(字段定义) + properties(值)。
// - 组按 groupSort、字段按 displaySort 排序;值取 properties[fieldCode](缺失→空串)。
// - properties 中存在但 formList 未定义的键:本轮忽略(以表单定义为准;待联调复核)。
DynamicForm parseDynamicForm(const QJsonObject& data);
// ExceptionVO 数组 → [ExceptionRow]字段直映射name=exceptionName、typeName=exceptionTypeName、
// markTypeName=exceptionMarkTypeName
std::vector<ExceptionRow> parseExceptions(const QJsonArray& arr);
```
> 不需要 `collectDescendantTmIds`GS→TM 展开由 UI 三态复选框级联承担(见 §7.1)。
### 5.3 数据访问层 `IProjectRepository` 扩展(`src/data/repo/IProjectRepository.hpp`
```cpp
// 既有 loadTmRows 泛化:把硬编码 structParentConfType=2 改为入参 parentConfType支持 GS=1
// classifyType 3=数据 1=文件pageNo 从 1 起pageSize 固定 5。
// (保留旧名或重命名为 loadRows调用点同步更新。
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 前 `QUrl::toPercentEncoding`;错误归一为 `RepoResult{ok=false, error=msg}`
## 6. 逻辑层 `WorkbenchNavController`
新增状态与契约(不碰 widget
```cpp
public slots:
// 泛化原 selectTm单击对象GS/TM→ 加载其 data/file 首页(loadRows 按 confType)
// + 加载对象详情 → emit objectDetailLoaded。confType: 1=GS 2=TM。
void selectObject(const QString& objectId, int confType);
// 勾选集变化(已是 TM 叶子集合,由面板算好并合并发射):逐 TM 查异常→并集→emit exceptionsLoaded。
void setCheckedTms(const QStringList& tmObjectIds);
// 单击数据集 → 加载 dynamicForm → emit datasetDetailLoaded。
void selectDataset(const QString& dsObjectId);
void loadMoreData(); void loadMoreFiles(); // 适配泛化 parent见下
signals:
void objectDetailLoaded(const QString& title, const geopro::data::DynamicForm& form);
void exceptionsLoaded(const std::vector<geopro::data::ExceptionRow>& rows, int tmCount);
void datasetDetailLoaded(const geopro::data::DynamicForm& form);
private:
std::string currentParentId_; int currentParentConfType_ = 0; // 加载更多用(替代 currentTmId_
std::map<std::string, std::vector<data::ExceptionRow>> tmExceptionCache_; // per-TM 异常缓存
```
- `selectObject`:置 `currentParentId_/ConfType_`、`dataPageNo_=filePageNo_=1``loadRows` 拉数据+文件首页emit `datasetsLoaded/filesLoaded`),再 `loadObjectDetail` emit `objectDetailLoaded`
- `setCheckedTms`:对集合中每个 TM命中 `tmExceptionCache_` 则复用、否则 `loadExceptionsByTm` 并入缓存;合并为并集(顺序:按勾选/树序稳定emit `exceptionsLoaded(rows, tmCount=集合大小)`。空集合 → emit 空列表(面板回占位)。
- `selectDataset``loadDatasetForm` → emit `datasetDetailLoaded`
- `loadMoreData/Files`:用 `currentParentId_/ConfType_` 续页(原依赖 `currentTmId_`,改为泛化 parent
- 切项目/工作空间:清 `tmExceptionCache_`、`currentParentId_`,并(经既有 `structureLoaded` 接线)清空三面板。
- 沿用 `BusyGuard` 重入保护与 `busyChanged`(同步阻塞 + WaitCursor
## 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`(满足 D2
- **信号改造**
- `objectClicked(QString objectId, int confType)`(取代 `tmClicked`——单击行GS 或 TM
- `checkedTmsChanged(QStringList tmObjectIds)`(取代 `tmCheckToggled`)——发射**当前全部被勾选的 TM 叶子 id**。
- **合并发射**`itemChanged` 在 GS 级联时会对每个子项各触发一次。用 `QTimer::singleShot(0, ...)` 合并:标脏 → 事件循环回合一次性遍历树收集所有勾选叶子 → 发一次 `checkedTmsChanged`(避免一次点击触发 N 次重算/N 组请求)。
- 单击高亮当前行(`QTreeWidget` 默认单选即可)。
### 7.2 `DynamicFormView`(新增,共享键值渲染器)
- `src/app/panels/DynamicFormView.{hpp,cpp}``QWidget`,方法 `void setForm(const DynamicForm&)` / `void showMessage(const QString&)`(空/错占位)。
- 布局:`QScrollArea` 内纵向堆叠——每组一个组标题 + `QFormLayout`(左 `字段名`、右 `值`,值支持换行)。无数据 → 居中淡色占位。
- 主题化:颜色取全局令牌(`text/primary`、`text/secondary` 等),随 `ThemeManager::changed` 重绘。
- **「对象属性」与「数据集属性」两面板各持一个 `DynamicFormView` 实例**(取代现有两个占位 `QLabel`)。
### 7.3 `AnomalyListPanel`(新增异常行渲染)
- 新增 `void populateExceptionList(QListWidget*, const std::vector<ExceptionRow>&)`:复用现有卡片视觉
(左色条可用中性/警示色、标题=`name`、第二行=`typeName · markTypeName · createTime`
**本轮去掉「眼睛/显隐」**(无 VTK 详情可联动)。
- 既有 `populateAnomalyList(vector<core::Anomaly>)` + `AnomalyCardDelegate`(带眼睛、联动 VTK**保留不动**,留给未来「数据详情真实渲染」轮;本轮不调用。
- 右上「异常」Tab 数量徽标:填 `rows.size()`,空则隐藏。
### 7.4 `main.cpp` 接线(增量)
- `objectTree.objectClicked → nav.selectObject(id, confType)`
- `nav.objectDetailLoaded → 对象属性 DynamicFormView.setForm(form)`(并可用 title 更新 Tab/表头)。
- `objectTree.checkedTmsChanged → nav.setCheckedTms(tmIds)`
- `nav.exceptionsLoaded → AnomalyListPanel.populateExceptionList + 徽标`
- `datasetList.itemClicked`(非「加载更多」行)→ `nav.selectDataset(dsId)`;移除当前写入 `propLabel` 的占位文案。
- `nav.datasetDetailLoaded → 数据集属性 DynamicFormView.setForm(form)`
- `structureLoaded`(切项目/空间):清空对象属性、数据集属性两 `DynamicFormView` 与异常列表/徽标(回占位)。
- 既有 `selectTm` 接线点替换为 `selectObject``loadMore*` 接线不变(控制器内部改用泛化 parent
## 8. 交互时序
```
单击对象(GS/TM): ObjectTreePanel.objectClicked(id, confType)
→ nav.selectObject:
loadRows(pid,id,confType,3,1)+loadRows(...,1,1) → datasetsLoaded/filesLoaded → 左下数据/文件页签
loadObjectDetail(id,confType) → objectDetailLoaded → 右上「对象属性」DynamicFormView
勾选/取消(GS 三态级联到 TM 叶子): ObjectTreePanel 合并 → checkedTmsChanged([tm...])
→ nav.setCheckedTms: 逐 TM(缓存)查异常 → 并集 → exceptionsLoaded(rows, n) → 右上「异常」列表+徽标
(取消某 TM/GS → 叶子集变小 → 重算并集 → 该 TM 异常自动消失)
单击数据集: datasetList.itemClicked(dsId)
→ nav.selectDataset: loadDatasetForm(dsId) → datasetDetailLoaded → 右下「数据集属性」DynamicFormView
切项目/空间: structureLoaded → 清空三面板 + 勾选集 + 异常缓存
```
## 9. 边界与错误处理
- **GS confType=1 待联调**:若 `data/page``structParentConfType=1` 下**不返回**GS 下全部 DS不递归
回退方案:对 GS 的直接子 TM 逐个 `data/page` 合并展示(分页退化为"加载全部"或按 TM 分段)。实现前用 live 接口验证一次再定。
- **properties 索引键待联调**`parseDynamicForm` 按 `fieldCode` 取值;若实测 `properties` 用其它键(如 `confFieldId`)索引,调整映射键即可(隔离在 DTO 一处)。
- **异常聚合性能**:同步多请求 + WaitCursor`tmExceptionCache_` 避免增量勾选时重复请求;切项目清缓存。
- **空 / 错状态**:任一面板无数据 → 居中「暂无…」占位;请求失败 → `loadFailed(stage,msg)` 状态栏提示 + 面板错误占位;**不回退本地样本**。
- **输入边界**id 为空短路不发请求URL 中 id 百分号编码。
- **重入**:沿用 `busy_` 保护(快速连点不污染状态)。
## 10. 测试策略
聚焦纯逻辑单测GoogleTest + CTest沿用 `tests/data/test_nav_dto.cpp`
- `parseDynamicForm`:分组+值合并、`groupSort/displaySort` 排序、`properties` 缺失值→空串、空 `formList`、`name` 透传。
- `parseExceptions`:字段映射(`exceptionName→name` 等)、空数组、缺字段容错。
- UI 三态级联 / 控制器聚合:依赖 Qt/live靠手动联调验证无桩
## 11. 文件清单
**改造**
- `src/data/repo/RepoTypes.hpp``+ DynamicForm/DynamicFormGroup/DynamicFormField/ExceptionRow`
- `src/data/dto/NavDto.{hpp,cpp}``+ parseDynamicForm / parseExceptions`
- `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 缓存``selectTm` 退役、`loadMore*` 改泛化 parent
- `src/app/panels/ObjectTreePanel.{hpp,cpp}` — GS 三态可勾选+可单击;信号改 `objectClicked / checkedTmsChanged`(合并发射)
- `src/app/panels/AnomalyListPanel.{hpp,cpp}``+ populateExceptionList`(旧 `core::Anomaly` 路径保留)
- `src/app/main.cpp` — 三面板接线(见 §7.4);移除 DS 单击占位文案
- `tests/data/test_nav_dto.cpp``+ parseDynamicForm / parseExceptions` 用例
**新增**
- `src/app/panels/DynamicFormView.{hpp,cpp}` — 共享键值渲染器(对象属性 + 数据集属性)
**保留不删**`LocalSampleRepository`、`render/*`、`AnomalyCardDelegate`/`populateAnomalyList`、中央/详情渲染代码(留未来 dd/ert 渲染轮)。
## 12. 未决 / 验证点(实现前用 live 接口确认)
1. GS 级 `data/page``structParentConfType` 取值与是否递归返回全部 DS§9 回退)。
2. `DynamicFormVO.properties` 的索引键(`fieldCode` 假设)。
3. `exception/queryExceptionByTmObjectId` 实际字段名与 `ExceptionVO` 是否一致(标记类型字段命名)。