17 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 真实剖面渲染。
1. 背景与目标
real-api-navigation 轮已把顶层导航壳(工作空间 / 项目 / 对象树)接到真实后端,并把渲染与导航解耦:
- 对象树
ObjectTreePanel已显示真实结构(项目根 → GS → TM,叶子=TM 可勾选)。 - 单击 TM(
tmClicked)→WorkbenchNavController::selectTm→ 左下「数据集」列出该 TM 的 DS(数据/文件两页签,分页)。 tmCheckToggled是前瞻钩子,本轮前无消费者;loadDataset、右上「异常」AnomalyListPanel(本地core::Anomaly)、右上「对象属性」与右下「数据集属性」面板均为占位。
本轮补齐三件被推迟的交互,全部接真实业务 API(不回退本地样本):
- 单击对象(GS 或 TM) → ① 左下数据列表显示其下所有 DS(GS=该工区全部 DS;TM=该测线 DS);② 右上「对象属性」显示该对象动态表单详情。当前单击行高亮(单选)。
- 勾选 / 反勾选 GS/TM → 右上「异常」列表显示所有被勾选 TM 叶子下异常的聚合并集。
- 单击数据列表某 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)
// 动态表单(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},纯函数,可单测)
// 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)
// 既有 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→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 前 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_=1,loadRows拉数据+文件首页(emitdatasetsLoaded/filesLoaded),再loadObjectDetailemitobjectDetailLoaded。setCheckedTms:对集合中每个 TM,命中tmExceptionCache_则复用、否则loadExceptionsByTm并入缓存;合并为并集(顺序:按勾选/树序稳定),emitexceptionsLoaded(rows, tmCount=集合大小)。空集合 → emit 空列表(面板回占位)。selectDataset:loadDatasetForm→ emitdatasetDetailLoaded。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/ExceptionRowsrc/data/dto/NavDto.{hpp,cpp}—+ parseDynamicForm / parseExceptionssrc/data/repo/IProjectRepository.hpp—loadTmRows泛化为loadRows;+ loadObjectDetail / loadDatasetForm / loadExceptionsByTmsrc/data/api/ApiProjectRepository.{hpp,cpp}— 实现上述新方法src/controller/WorkbenchNavController.{hpp,cpp}—+ selectObject / setCheckedTms / selectDataset / 三新信号 / per-TM 缓存;selectTm退役、loadMore*改泛化 parentsrc/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 接口确认)
- GS 级
data/page的structParentConfType取值与是否递归返回全部 DS(§9 回退)。 DynamicFormVO.properties的索引键(fieldCode假设)。exception/queryExceptionByTmObjectId实际字段名与ExceptionVO是否一致(标记类型字段命名)。