20 KiB
对象单击/勾选 驱动 数据列表·异常(含异常体)·属性 面板(接真实 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(不回退本地样本):
- 单击对象(GS 或 TM) → ① 左下数据列表显示其下所有 DS(GS=该工区全部 DS;TM=该测线 DS);② 右上「对象属性」显示该对象动态表单详情。当前单击行高亮(单选)。
- 勾选 / 反勾选 GS/TM → 右上「对象异常」面板显示所有勾选对象下的「异常 + 异常体」树(见 §2 决策、§7.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)
// 动态表单(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},纯函数,可单测)
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)
// 既有 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 getGsObjectDetail,2=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
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拉数据+文件首页(emitdatasetsLoaded/filesLoaded),再loadObjectDetail→ emitobjectDetailLoaded。setCheckedTms:对集合每个 TM——命中tmExceptionCache_复用、否则loadExceptionsByTm入缓存;对每个 TM 调groupExceptionsByConsortium装配一个ObjectExceptionGroup(objectName 由lastStructNodes_解析);emitexceptionTreeLoaded(groups, tmCount)。空集 → emit 空(面板回占位)。selectDataset:loadDatasetForm→ emitdatasetDetailLoaded。- 切项目/工作空间:清
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<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)。
- 回退:若单 TM 异常不带异常体归属,则改用项目级
- 聚合性能:同步多请求 + 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 / ObjectExceptionGroupsrc/data/dto/NavDto.{hpp,cpp}—+ parseDynamicForm / parseExceptions / groupExceptionsByConsortiumsrc/data/repo/IProjectRepository.hpp—loadTmRows→loadRows泛化;+ loadObjectDetail / loadDatasetForm / loadExceptionsByTmsrc/data/api/ApiProjectRepository.{hpp,cpp}— 实现新方法src/controller/WorkbenchNavController.{hpp,cpp}—+ selectObject / setCheckedTms / selectDataset / 三新信号 / per-TM 缓存 / lastStructNodes_;selectTm退役、loadMore*改泛化 parentsrc/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 接口确认)
- GS 级
data/page的structParentConfType取值与是否递归返回全部 DS(§9 回退)。 DynamicFormVO.properties的索引键(fieldCode假设)。- 异常的「异常体归属」字段(
consortiumId/Name/Type来源):单 TM 异常 payload 是否自带,否则走项目级异常体树映射(§9 回退)。 exceptionConsortium各字段命名与「异常体类型」取值来源(exceptionTypeIdvs 类型名)。
13. 后续轮(本轮明确不做)
- 异常体拖拽合并/编辑(
exceptionConsortium新增/关联/取消关联/删除写操作)。 - 异常项眼睛(显隐) 联动中央/详情视图——随 dd/ert 真实剖面渲染同轮接入。
- 中央 2D/3D 与「数据详情」真实数据渲染、项目 CRS 替换、异步仓储、文件下载。