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

17 KiB
Raw Blame History

对象单击/勾选 驱动 数据列表·异常·属性 三面板(接真实 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 可勾选)。
  • 单击 TMtmClicked)→ 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 属性展示深度 完整动态表单(分组键值);字段显示名取自 APIFormItemVO.fieldName),无需推断

D2 的连锁简化:因勾选源只数 TM 叶子GS 仅作批量开关,且无 GS 级异常接口),

  • 「GS→后代 TM」的展开由 Qt 原生三态复选框承担(ItemIsAutoTristate 级联),控制器只读叶子集;
  • 聚合 = 被勾选 TM 叶子异常的并集(功能本身固有);
  • 不存在去重问题(叶子 id 唯一)。

3. 接口映射

网关与会话沿用现有 ApiClienthttp://tenant.geomative.cn/pop-apitoken 已注入)。成功判定 code==200

能力 方法 路径 请求 / 返回要点
数据列表GS/TM 级) POST /business/dsObject/data/page body {projectId, structParentId, structParentConfType, classifyTypeList:[3], pageNo, pageSize:5}structParentConfTypeTM=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

// 动态表单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 排好序
};

// 异常列表行(右上「异常」面板用;列表展示,本轮不联动 VTKstruct ExceptionRow {
    std::string id, name, typeName, markTypeName, createTime, remark;
};

5.2 DTOsrc/data/dto/NavDto.{hpp,cpp},纯函数,可单测)

// DynamicFormVO 对象 → DynamicForm合并 formList(字段定义) + properties(值)。
//   - 组按 groupSort、字段按 displaySort 排序;值取 properties[fieldCode](缺失→空串)。
//   - properties 中存在但 formList 未定义的键:本轮忽略(以表单定义为准;待联调复核)。
DynamicForm parseDynamicForm(const QJsonObject& data);

// ExceptionVO 数组 → [ExceptionRow]字段直映射name=exceptionName、typeName=exceptionTypeName、
//   markTypeName=exceptionMarkTypeNamestd::vector<ExceptionRow> parseExceptions(const QJsonArray& arr);

不需要 collectDescendantTmIdsGS→TM 展开由 UI 三态复选框级联承担(见 §7.1)。

5.3 数据访问层 IProjectRepository 扩展(src/data/repo/IProjectRepository.hpp

// 既有 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/getDetailvirtual 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

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_=1loadRows 拉数据+文件首页emit datasetsLoaded/filesLoaded),再 loadObjectDetail emit objectDetailLoaded
  • setCheckedTms:对集合中每个 TM命中 tmExceptionCache_ 则复用、否则 loadExceptionsByTm 并入缓存;合并为并集(顺序:按勾选/树序稳定emit exceptionsLoaded(rows, tmCount=集合大小)。空集合 → emit 空列表(面板回占位)。
  • selectDatasetloadDatasetForm → 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/primarytext/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 接线点替换为 selectObjectloadMore* 接线不变(控制器内部改用泛化 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/pagestructParentConfType=1不返回GS 下全部 DS不递归 回退方案:对 GS 的直接子 TM 逐个 data/page 合并展示(分页退化为"加载全部"或按 TM 分段)。实现前用 live 接口验证一次再定。
  • properties 索引键待联调parseDynamicFormfieldCode 取值;若实测 properties 用其它键(如 confFieldId)索引,调整映射键即可(隔离在 DTO 一处)。
  • 异常聚合性能:同步多请求 + WaitCursortmExceptionCache_ 避免增量勾选时重复请求;切项目清缓存。
  • 空 / 错状态:任一面板无数据 → 居中「暂无…」占位;请求失败 → loadFailed(stage,msg) 状态栏提示 + 面板错误占位;不回退本地样本
  • 输入边界id 为空短路不发请求URL 中 id 百分号编码。
  • 重入:沿用 busy_ 保护(快速连点不污染状态)。

10. 测试策略

聚焦纯逻辑单测GoogleTest + CTest沿用 tests/data/test_nav_dto.cpp

  • parseDynamicForm:分组+值合并、groupSort/displaySort 排序、properties 缺失值→空串、空 formListname 透传。
  • 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.hpploadTmRows 泛化为 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} — 共享键值渲染器(对象属性 + 数据集属性)

保留不删LocalSampleRepositoryrender/*AnomalyCardDelegate/populateAnomalyList、中央/详情渲染代码(留未来 dd/ert 渲染轮)。

12. 未决 / 验证点(实现前用 live 接口确认)

  1. GS 级 data/pagestructParentConfType 取值与是否递归返回全部 DS§9 回退)。
  2. DynamicFormVO.properties 的索引键(fieldCode 假设)。
  3. exception/queryExceptionByTmObjectId 实际字段名与 ExceptionVO 是否一致(标记类型字段命名)。