# 对象单击/勾选 驱动 数据列表·异常(含异常体)·属性 面板(接真实 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)** → ① 左下数据列表显示其下所有 DS(GS=该工区全部 DS;TM=该测线 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 fields; }; struct DynamicForm { std::string name; std::vector 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 exceptions; }; // 对象分组(树根层,对应一个被勾选 TM)。 struct ObjectExceptionGroup { std::string objectId, objectName; std::vector consortia; // 该对象下的异常体(含其异常) std::vector looseExceptions; // 未合并进异常体的独立异常 }; ``` ### 5.2 DTO(`src/data/dto/NavDto.{hpp,cpp}`,纯函数,可单测) ```cpp DynamicForm parseDynamicForm(const QJsonObject& data); // formList(定义)+properties(值) 合并 std::vector parseExceptions(const QJsonArray& arr); // ExceptionVO→ExceptionRow(含 consortium* + detailSummary) // 把一个对象(TM)的异常行,按 consortiumId 分组成「异常体列表 + 独立异常列表」。纯函数、可单测。 // - 同 consortiumId 归一组(组名/类型取首个非空 consortiumName/Type); // - consortiumId 为空 → looseExceptions。 struct GroupedExceptions { std::vector consortia; std::vector loose; }; GroupedExceptions groupExceptionsByConsortium(const std::vector& 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 loadRows(const std::string& projectId, const std::string& parentId, int parentConfType, int classifyType, int pageNo) = 0; // 对象详情:按 confType 选端点(1=GS getGsObjectDetail,2=TM tmObject/getDetail)。 virtual RepoResult loadObjectDetail(const std::string& objectId, int confType) = 0; // 数据集详情:dsObject/dynamicForm/{dsObjectId}。 virtual RepoResult loadDatasetForm(const std::string& dsObjectId) = 0; // 单 TM 异常(含异常体归属字段):exception/queryExceptionByTmObjectId/{tmObjectId}。 virtual RepoResult> 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& groups, int tmCount); void datasetDetailLoaded(const geopro::data::DynamicForm& form); private: std::string currentParentId_; int currentParentConfType_ = 0; // 加载更多用(替代 currentTmId_) std::vector lastStructNodes_; // tmId→name 解析 std::map> 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`: ``` 对象A(TM 名) ├─ ▸ 异常体X(异常体类型) ← 中间层:ConsortiumGroup │ ├─ 异常1(异常类型) ← 叶子:ExceptionRow │ │ └─ ▸ 详情:标记类型/坐标/高程/创建时间/备注 ← D6 详情展开(内联,懒构建) │ └─ 异常2(异常类型) ├─ 独立异常(未合并) ← looseExceptions 分组节点 │ └─ 异常3(异常类型) 对象B(TM 名)… ``` - `void setGroups(const std::vector&)`:重建树;`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 替换、异步仓储、文件下载。