393 lines
28 KiB
Markdown
393 lines
28 KiB
Markdown
# 接入真实导航(工作空间 / 项目 / 对象树)— 设计文档
|
||
|
||
- 日期:2026-06-09
|
||
- 分支:feat/m1-finishing(建议拉子分支 feat/real-api-navigation)
|
||
- 状态:已与需求方确认范围,待 spec 评审
|
||
|
||
---
|
||
|
||
## 1. 背景与目标
|
||
|
||
当前应用已搭好骨架:登录链路(`net::AuthService` + `net::ApiClient`,共享会话、token 注入)可用;
|
||
工作台(`src/app/main.cpp::buildWorkbench`)用本地静态样本仓储 `data::LocalSampleRepository`
|
||
渲染三维/二维示例;顶部 `app::TopBar` 的"工作空间切换 / 项目"为**静态视觉壳**(硬编码下拉项)。
|
||
|
||
本轮目标:把**顶层导航壳**接到真实后端接口,逐步替换静态数据:
|
||
|
||
1. 工作空间(=企业租户/空间)列表与切换;
|
||
2. 项目列表与切换;
|
||
3. 对象显示栏的树形结构(项目 → GS → TM)+ 选中 TM 后其 DS 列表。
|
||
|
||
中央三维/二维渲染与"数据详情"的**真实剖面/反演数据**走另一批 `dd/ert` 接口,**本轮不接**。
|
||
|
||
## 2. 范围(已确认决策)
|
||
|
||
**做(In Scope)**
|
||
- 工作空间列表 / 切换(真实接口)。
|
||
- 项目列表 / 切换(真实接口):下拉显示首页项目(首页 10);项目数超过首页 → 下拉底部「全部项目…」打开
|
||
**项目列表弹窗**(名称/类型过滤 + 分页 + 8 列表格:序号/名称/编号/状态/类型/业主/负责人/创建时间;点项目名切换并关弹窗)。
|
||
- 对象树:**按真实结构显示 GS 层**(项目根 → GS → TM);TM 在左下"数据真实显示栏"列出其 DS。
|
||
- DS **数据/文件两个页签**接真实分页接口(每页 5);每行显示"名称 / 创建时间 · 类型(数据)或大小(文件)";
|
||
超过首页 → 列表末尾「加载更多」追加下一页。
|
||
- 真实接口失败(断网 / token 过期 / 无数据)→ **显示错误 / 空状态**,**不回退本地样本**。
|
||
- 项目 `referenceCRSCode` 存入导航状态,供下一轮替换硬编码 `EPSG:4547`(本轮不改渲染)。
|
||
|
||
**不做(Out of Scope,留下一轮)**
|
||
- 中央 2D/3D 视图、数据详情的真实数据渲染(`dd/ert/gpr` 接口)。
|
||
→ 点击真实 DS 时中央/详情显示**占位"待接入"**;`render/*` 与 `LocalSampleRepository` 代码**保留不删**。
|
||
- 异步仓储(QFuture/回调)—— 本轮同步阻塞 + WaitCursor(与登录一致),异步留 M1.5。
|
||
- 用户头像 / 姓名接真实 `auth/getUserInfo`(本轮先留静态)。
|
||
- 文件下载(文件页签已展示文件名/大小、下载 `url` 已存入列表项备用,实际下载动作留后续)。
|
||
|
||
## 3. 接口确认结论
|
||
|
||
网关:`http://tenant.geomative.cn/pop-api`(现有 `ApiClient` 基址);business 与 admin 两份 OpenAPI 同一上游。
|
||
token 已由登录注入(`geomativeauthorization` 头),下列接口直接复用现有会话。
|
||
|
||
| 能力 | 方法 | 路径 | 关键返回 |
|
||
|---|---|---|---|
|
||
| 工作空间列表 | GET | `/business/system/tenant/enterprise/joined/list` | `[{id, name, ownerType(1个人/2企业), isCurTenant(0/1), logoPath}]` |
|
||
| 切换工作空间 | POST | `/business/system/tenant/enterprise/switch/{tenantId}` | 信封 code/msg |
|
||
| 项目分页 | POST | `/business/my/profile/project/page` | body `{projectName(名称模糊,可空), projectTypeId(类型过滤,可空), pageNo, pageSize}`;`data{list:[{id, projectName, projectCode, status, projectTypeId, projectTypeName, ownerCompanyName, responsiblePersonName, createTime, referenceCRSCode}], total}`(实测 `queryByUser`/`queryProject` 返回空或不带分页,弃用) |
|
||
| 项目类型列表 | GET | `/business/project/type/list` | `data.value:[{id, name}]`(弹窗"项目类型"过滤下拉) |
|
||
| 项目结构 | GET | `/business/projectStruct/queryProjectStruct/{projectId}` | `data.value:[{id, name, parentId, type(1项目/2TM), typeId, typeName, confCode}]`(仅项目根+TM,不含 DS) |
|
||
| TM 下数据(页签) | POST | `/business/dsObject/data/page` | body `{projectId, structParentId:<tmObjectId>, structParentConfType:2, classifyTypeList:[3], pageNo, pageSize}`;`data{list:[{id, dsName, name(类型名), ddCode, createTime}], total}` |
|
||
| TM 下文件(页签) | POST | `/business/dsObject/file/page` | body 同上但 `classifyTypeList:[1]`;项另含 `createTime` 与 `file{name, size, url}` |
|
||
|
||
**层级确认(修正需求方假设)**:真实结构**不是** `项目→tm→ds`,而是 **`项目 → GS(工区) → TM(测线) → DS`**。
|
||
- `queryProjectStruct` 返回一个**扁平 parent-child 列表**(仅含 GS + TM 两类节点,**不含 DS**),客户端按 `parentId` 自建树。
|
||
- DS 不在结构列表里:按 TM 拉数据/文件两类,分别用 `dsObject/data/page`(classify=3)、`dsObject/file/page`(classify=1),传 `structParentId=<tmObjectId>`、`structParentConfType=2`。(实测:`queryByUser` 返回空,项目列表改用 `my/profile/queryProject`;`queryDsByTmObjectId` 字段不全已弃用。)
|
||
- **项目不能直接挂 DS**;DS 永远挂在 TM 下。但由于是 `parentId` 扁平结构,**TM 可直接挂在项目下(无中间 GS)**——这是"项目直接挂"印象的来源,但叶子仍是 TM→DS。
|
||
|
||
**节点判定**:结构列表只含 GS+TM,故 **TM = 该节点在结构列表中无子节点(叶子)**;非叶子 = GS。
|
||
`type`(integer) / `confCode` 一并保留为辅助信号,待见到 live 数据后可固化判定规则。
|
||
|
||
## 4. 架构分层
|
||
|
||
遵循仓库既有四层(见各层 README);本轮新增组件按层就位,**依赖方向单向向下**,UI 不直接碰 `ApiClient`。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ UI 层 (src/app, 目标 src/view) — 被动视图,只渲染模型 + 发用户意图信号 │
|
||
│ TopBar(数据驱动) ObjectTreePanel DatasetListPanel(已有) │
|
||
└───────────────▲──────────────────────────┬──────────────────┘
|
||
信号(用户意图) 槽(模型数据)
|
||
┌───────────────┴──────────────────────────▼──────────────────┐
|
||
│ 逻辑层 (src/controller) — 编排状态机,无 widget │
|
||
│ WorkbenchNavController : QObject │
|
||
│ state: 当前 workspaceId / projectId / project.crsCode │
|
||
│ slots: switchWorkspace / switchProject / selectTm │
|
||
│ signals: workspacesLoaded / projectsLoaded / │
|
||
│ structureLoaded / datasetsLoaded / loadFailed │
|
||
└───────────────────────────┬──────────────────────────────────┘
|
||
IProjectRepository(同步契约)
|
||
┌───────────────────────────▼──────────────────────────────────┐
|
||
│ 数据访问层 (src/data) │
|
||
│ repo/IProjectRepository.hpp ← 导航仓储抽象(Result<T>) │
|
||
│ api/ApiProjectRepository.{h,cpp} ← 用 ApiClient 实现 │
|
||
│ dto/NavDto.{h,cpp} ← 后端 JSON → 模型 纯映射 + 扁平→树 │
|
||
└───────────────────────────┬──────────────────────────────────┘
|
||
net::ApiClient(原始 HTTP)
|
||
┌───────────────────────────▼──────────────────────────────────┐
|
||
│ 接口层 (src/net) — 复用,无改动 │
|
||
│ ApiClient(共享会话/token) ; AuthService(登录) │
|
||
└───────────────────────────────────────────────────────────────┘
|
||
|
||
模型层 (src/data/repo/RepoTypes.hpp & 新增 NavTypes) — 纯结构,被各层共享
|
||
Workspace / ProjectSummary / StructNode / Project,GsNode,TmNode,DsNode(已有)
|
||
```
|
||
|
||
依赖规则:`net` 不依赖任何上层;`data` 依赖 `net` + 模型;`controller` 依赖 `data` + 模型(不依赖 UI);
|
||
`app/view` 依赖 `controller` + 模型(不依赖 `data/net` 具体类型,只经 controller 信号拿模型)。
|
||
|
||
## 5. 各层组件详细设计
|
||
|
||
### 5.1 接口层 `net`(复用,不改)
|
||
`ApiClient::get(path)` / `postJson(path, body)` 返回 `ApiResponse{httpStatus, code, data, msg, rawError}`。
|
||
本轮所有业务接口经它发出,token 已注入,会话已共享。
|
||
|
||
### 5.2 模型层(纯结构,无 Qt / 无 VTK)
|
||
`src/data/repo/RepoTypes.hpp` 已有 `Project/GsNode/TmNode/DsNode`。新增导航模型(同文件或 `NavTypes.hpp`):
|
||
|
||
```cpp
|
||
struct Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; };
|
||
struct ProjectSummary {
|
||
std::string id, name, typeName, crsCode, crsName;
|
||
std::string code, projectTypeId, ownerCompany, responsiblePerson, createTime; // 弹窗 8 列用
|
||
int status = 0; // 1=未开始 2=进行中(其余显示数字)
|
||
};
|
||
struct ProjectType { std::string id, name; }; // 类型过滤下拉
|
||
struct ProjectListPage { std::vector<ProjectSummary> rows; int total = 0; }; // 项目分页结果
|
||
// 项目结构扁平节点(GS / TM);客户端按 parentId 建树。
|
||
struct StructNode {
|
||
std::string id, name, parentId, typeName, confCode;
|
||
int type = 0;
|
||
};
|
||
```
|
||
新增 `DsRow{id, dsName, typeName, ddCode, createTime, fileName, fileUrl, fileSize}`(数据/文件页签行通用;文件行含 file*)
|
||
+ `DsPage{rows, total}`(分页结果)。`DsNode` 仅本地样本仓储继续用。
|
||
|
||
### 5.3 数据访问层 `data`
|
||
|
||
**`repo/IProjectRepository.hpp`** — 导航仓储抽象(同步,呼应既有 `IDatasetRepository` 风格;
|
||
但网络可失败,故用显式 `Result` 而非抛异常,便于 UI 出错误/空状态):
|
||
|
||
```cpp
|
||
template <class T>
|
||
struct RepoResult { bool ok = false; T value{}; std::string error; };
|
||
|
||
class IProjectRepository {
|
||
public:
|
||
virtual ~IProjectRepository() = default;
|
||
virtual RepoResult<std::vector<Workspace>> listWorkspaces() = 0;
|
||
virtual RepoResult<bool> switchWorkspace(const std::string& tenantId) = 0;
|
||
// 项目分页(名称/类型过滤)+ 项目类型列表(弹窗用)。
|
||
virtual RepoResult<ProjectListPage> pageProjects(const std::string& nameFilter,
|
||
const std::string& typeId, int pageNo,
|
||
int pageSize) = 0;
|
||
virtual RepoResult<std::vector<ProjectType>> listProjectTypes() = 0;
|
||
virtual RepoResult<std::vector<StructNode>> loadStructure(const std::string& projectId) = 0;
|
||
// 按 TM 分页拉数据/文件行:classifyType 3=数据 1=文件;pageNo 从 1 起,pageSize 固定 5。
|
||
virtual RepoResult<DsPage> loadTmRows(const std::string& projectId, const std::string& tmObjectId,
|
||
int classifyType, int pageNo) = 0;
|
||
};
|
||
```
|
||
|
||
**`api/ApiProjectRepository.{hpp,cpp}`** — 实现:持有 `net::ApiClient&`,
|
||
按 §3 路径发请求,把 `ApiResponse` 交给 `dto/` 映射;网络/业务码错误 → `RepoResult{ok=false, error=msg}`。
|
||
判定成功:`code==200`(沿用登录 `AuthService` 的约定,业务码即成功标志)。id 进 URL 路径/查询前
|
||
经 `QUrl::toPercentEncoding` 百分号编码(不可信后端数据:防 `? # & /` 空格 破坏 URL)。
|
||
|
||
**`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测**):
|
||
- `parseWorkspaces(QJsonArray) -> vector<Workspace>`(`isCurTenant==1 → isCurrent`)。
|
||
- `parseProjectList(QJsonArray) -> vector<ProjectSummary>` / `parseProjectPage(QJsonObject) -> ProjectListPage{rows,total}`(project/page)。
|
||
- `parseProjectTypes(QJsonArray) -> vector<ProjectType>`(type/list)。
|
||
- `parseStructNodes(QJsonArray) -> vector<StructNode>`。
|
||
- `parseDsRows(QJsonArray) -> vector<DsRow>` / `parseDsPage(QJsonObject) -> DsPage{rows,total}`(data/file page;`name→typeName`、`createTime`、`file{name,size,url}`)。
|
||
- `buildStructTree(vector<StructNode>) -> vector<StructTreeNode>`:扁平→**通用树**(不强塞 `Project/Gs/Tm` 刚性模型,
|
||
以适配任意层级 + TM 直挂项目)。`StructTreeNode{StructNode node; bool isTm; vector<StructTreeNode> children}`。
|
||
- 以 `parentId` 归并;`parentId` 为空或不在集合内(孤儿)的节点为根层。
|
||
- **叶子节点判定为 TM**(`isTm=true`,`node.id` 即 tmObjectId);非叶子为 GS。
|
||
- `visited` 集防环:不可信后端数据(多节点环 / 重复 id)也不会无限递归(规约:永不信任外部数据)。
|
||
- 纯函数、可单测;树→QTreeWidget 的填充由 `ObjectTreePanel` 调用本函数完成(见 §5.5)。
|
||
|
||
### 5.4 逻辑层 `controller/WorkbenchNavController`(QObject)
|
||
唯一持有导航状态;不碰 widget;经信号把模型推给 UI、经槽接收用户意图。
|
||
|
||
```cpp
|
||
class WorkbenchNavController : public QObject {
|
||
Q_OBJECT
|
||
public:
|
||
explicit WorkbenchNavController(data::IProjectRepository& repo, QObject* parent=nullptr);
|
||
void start(); // 启动:拉空间→项目→结构
|
||
public slots:
|
||
void switchWorkspace(const QString& tenantId); // 切空间→重载项目→重载结构
|
||
void switchProject(const QString& projectId); // 切项目→重载结构(清 DS/详情)
|
||
void selectTm(const QString& tmObjectId); // 选 TM→拉其 DS 首页(数据+文件)
|
||
void loadMoreData(); // 数据页签"加载更多"→下一页追加
|
||
void loadMoreFiles(); // 文件页签"加载更多"→下一页追加
|
||
signals:
|
||
void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId);
|
||
void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId, int total); // total 判断"全部项目"入口
|
||
// 发出项目名 + 扁平结构节点;建树(buildStructTree)在 ObjectTreePanel 内完成。
|
||
void structureLoaded(const QString& projectName, const std::vector<data::StructNode>&);
|
||
// total=总数、append=是否追加(加载更多 true / 首页 false)。
|
||
void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsRow>&, int total, bool append);
|
||
void filesLoaded(const QString& tmObjectId, const std::vector<data::DsRow>&, int total, bool append);
|
||
void loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态
|
||
void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor
|
||
private:
|
||
void loadProjectsAndStructure(); // start + switchWorkspace 共用
|
||
data::IProjectRepository& repo_;
|
||
std::vector<data::ProjectSummary> lastProjects_; // 供 switchProject 查 name/crsCode
|
||
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
|
||
std::string currentTmId_; // 加载更多用:当前选中 TM
|
||
int dataPageNo_ = 0, filePageNo_ = 0, dataTotal_ = 0, fileTotal_ = 0; // 数据/文件分页游标
|
||
bool busy_ = false; // 重入保护:同步请求期间拒绝再次进入
|
||
};
|
||
```
|
||
编排逻辑:`start()` → `listWorkspaces`(选 isCurrent/首个)→ `pageProjects`(首页 10,选首个)→ `loadStructure`→发扁平节点。
|
||
`selectTm` 拉数据/文件首页(每页 5);`loadMoreData/Files` 递增页码追加。`switchWorkspace` 成功后用返回的新 accessToken 重注入 ApiClient(否则后续请求仍落旧空间)。
|
||
切空间/项目按 §6 时序。每个阶段失败 emit `loadFailed(stage,msg)` 并停在该阶段。
|
||
**重入保护**:每个公共操作入口 `if (busy_) return;`,并用 RAII guard 在置忙/复位时配平 `busyChanged`
|
||
(同步 HTTP 会泵 Qt 事件循环,快速二次点击可能重入并污染状态)。
|
||
|
||
### 5.5 UI 层 `app`(被动视图,数据驱动)
|
||
|
||
**`app/TopBar`** —— 由"自由函数返回静态 QWidget"升级为**数据驱动类**(QWidget 子类):
|
||
- `setWorkspaces(list, currentId)` / `setProjects(list, currentId, hasMore)` 重建下拉项;`hasMore` 时下拉底部加「全部项目…」。
|
||
- `setProjectButtonText(name)` —— 弹窗切换项目后更新项目按钮文字。
|
||
- 信号 `workspaceSwitchRequested(id)` / `projectSwitchRequested(id)` / `allProjectsRequested()`(打开项目弹窗)。
|
||
- 工作空间/项目下拉用互斥 `QActionGroup`(避免"多选"),选中即更新按钮文字。
|
||
- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。`buildMenuBar` 不变。
|
||
|
||
**`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector<StructNode>)` 内部调
|
||
`dto::buildStructTree` 重建 `QTreeWidget`(项目根→GS→TM,叶子=TM 可勾选、`UserRole` 存 tmObjectId);
|
||
`showMessage(msg)` 显示空/错占位。信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)`
|
||
(后者为前瞻钩子,本轮无消费者)。
|
||
|
||
**`app/panels/DatasetListPanel`** —— `datasetsLoaded`→`populateDatasetList`(数据:`dsName / 创建时间 · 类型名`);
|
||
`filesLoaded`→`populateFileList`(文件:`文件名 / 创建时间 · 可读大小`,url 存角色备下载);空时占位。列表去隔行变色,改细分割线。
|
||
超过首页 → main 在列表末尾插入「加载更多(已/共)」item(角色 `kDsLoadMoreRole`),点击 → `loadMoreData()`/`loadMoreFiles()` 追加。
|
||
|
||
**`app/ProjectListDialog`**(新增 QDialog)—— 项目列表弹窗:顶部 项目名搜索框 + 类型下拉(`listProjectTypes`,含"全部类型") + 搜索/重置;
|
||
中部 8 列表格(序号/项目名称/项目编号/项目状态/项目类型/业主单位/负责人/创建时间);底部 上/下一页 + "共 N 条 第 x/y 页"。
|
||
持有 `IProjectRepository&` 自行分页(`pageProjects`,每页 20);状态列 1=未开始/2=进行中(其余数字)。点项目名列 → emit `projectChosen(id,name)` + `accept()`。
|
||
由 `TopBar::allProjectsRequested` 触发,main 创建并 `exec()`;选中 → `nav.switchProject(id)` + `TopBar::setProjectButtonText(name)`。
|
||
|
||
**中央/详情**:移除"启动自动渲染本地 demo";DS 点击 → 详情面板与中央视图显示占位文案
|
||
"该数据集渲染将在下一阶段接入 dd 接口"。渲染代码保留。
|
||
|
||
## 6. 数据流 / 交互时序
|
||
|
||
```
|
||
启动(登录后):
|
||
main 构造 ApiClient → ApiProjectRepository → WorkbenchNavController → TopBar/ObjectTreePanel
|
||
controller.start():
|
||
listWorkspaces → emit workspacesLoaded → TopBar.setWorkspaces
|
||
listProjects(empty) → emit projectsLoaded → TopBar.setProjects
|
||
loadStructure(currentProject) → emit structureLoaded(name,nodes) → ObjectTreePanel.setStructure(→buildStructTree)
|
||
|
||
切空间: TopBar.workspaceSwitchRequested(id)
|
||
→ controller.switchWorkspace: switchWorkspace(id) → listProjects → 选首个 → loadStructure
|
||
→ emit projectsLoaded + structureLoaded;清空 DS 列表/详情占位
|
||
|
||
切项目: TopBar.projectSwitchRequested(id)
|
||
→ controller.switchProject: loadStructure(id) → emit structureLoaded;清空 DS 列表/详情占位
|
||
|
||
全部项目: TopBar.allProjectsRequested → main 打开 ProjectListDialog(pageProjects 分页 + 名称/类型过滤)
|
||
→ 点项目名 → projectChosen(id,name) → nav.switchProject(id) + TopBar.setProjectButtonText(name) → 关弹窗
|
||
|
||
选 TM: ObjectTreePanel.tmClicked(tmObjectId)
|
||
→ controller.selectTm: loadTmRows(pid,tm,3,1)+loadTmRows(pid,tm,1,1) → emit datasetsLoaded/filesLoaded(total,append=false) → 数据/文件页签
|
||
加载更多: 点列表末尾"加载更多" → controller.loadMoreData/Files → emit …(total,append=true) → 追加
|
||
|
||
点 DS: DatasetListPanel → 中央/详情显示占位"待接入"(本轮不渲染真实数据)
|
||
```
|
||
|
||
## 7. 错误处理与边界
|
||
- 仓储层捕获网络错误(`rawError`)与业务错误码,归一为 `RepoResult.error`。
|
||
- controller 任一阶段失败 → `loadFailed(stage, msg)`;UI 在对应面板显示空/错状态 label + 状态栏提示,**不回退本地样本**。
|
||
- 空数据(无空间 / 无项目 / 无结构 / 无 DS)→ 各面板显示"暂无…"占位(识别优于回忆)。
|
||
- token 过期(业务码 401 类)→ `loadFailed` 文案提示重新登录(本轮先提示,自动跳登录留后续)。
|
||
- 输入边界:`tmObjectId` / `projectId` 为空时短路不发请求;URL 中的 id 一律百分号编码(见 §5.3)。
|
||
- 重入:同步请求期间 `busy_` 拒绝再次进入(避免快速点击重入污染状态,见 §5.4)。
|
||
|
||
## 8. 渲染解耦
|
||
|
||
现状:对象树(本地 grid1/grid2…)直接驱动中央与数据详情;`rebuildCentral`/`rebuildDetail` 都是
|
||
`main.cpp` 内捕获了本地 `tree`/`structure` 的 lambda。本轮真实树 id 与本地样本对不上,故解耦:
|
||
|
||
- 启动不再自动渲染本地 demo;真实 DS 点击 → 中央/详情显示占位文案。
|
||
- `render/*`、`LocalSampleRepository`、`VoxelFromScatters` 等全部保留,待下轮按 dd/ert 接口复用。
|
||
- 项目 `crsCode` 由 controller 存住,下一轮替换 `main.cpp` 中硬编码 `EPSG:4547`。
|
||
|
||
### 8.1 中央三维编排:保留并解耦为数据驱动 helper(关键改动)
|
||
|
||
旧 `rebuildCentral` 直接读对象树 + `repo.loadGrid`,与本地样本强耦合、无法复用到真实 DS。
|
||
**本轮把"每个剖面 section 的中央渲染"抽成显式数据驱动的 helper**,使下一轮"喂真实数据"即可复活,
|
||
无需重写编排:
|
||
|
||
```cpp
|
||
// src/app/CentralScene.{hpp,cpp}
|
||
namespace geopro::app {
|
||
enum class ViewMode { Map2D, View3D };
|
||
|
||
// 一个待渲染剖面:grid(2D 测线 / 3D 帘面都用)+ colorScale(3D 帘面上色用)。
|
||
struct SectionInput {
|
||
geopro::core::Grid grid;
|
||
geopro::core::ColorScale colorScale;
|
||
};
|
||
|
||
// 中央场景重建(脱离对象树,按显式 sections 渲染):
|
||
// 2D = 每个 section 的 buildSurveyLine(红线俯视);
|
||
// 3D = 每个 section 的 buildCurtain(断面墙,受 showCurtain 开关 + 纵向夸张)。
|
||
void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
|
||
vtkRenderWindow* renderWindow, ViewMode mode,
|
||
const std::vector<SectionInput>& sections, bool showCurtain,
|
||
const geopro::core::GeoLocalFrame& frame, double verticalExaggeration);
|
||
}
|
||
```
|
||
|
||
- **本轮**:`main.cpp` 用**空 `sections`** 调用该 helper → 中央为空背景(占位)。视图 2D/3D 切换、帘面勾选仍走它,只是无内容。
|
||
- **下一轮**:`main.cpp` 用真实 DS 数据构建 `std::vector<SectionInput>` 再调同一 helper —— 编排零改动。
|
||
- **`rebuildDetail`(数据详情:#18/#17/异常/电极)**:保留在 `main.cpp`(暂不触发),下一轮改触发条件即复活。
|
||
- **体素 / 切片 / 地形**:是 demo 专属派生层(来自两条交叉本地剖面散点 / 本地 DEM),**不绑定单个 DS**,
|
||
不纳入 `rebuildCentralScene`。本轮移除其 `main.cpp` 内联编排(`render/` 函数保留),其"视图详情"勾选项
|
||
本轮置灰并提示"(下一轮接入)"。它们的复活属独立未来工作(需真实体素/地形数据源),见 §12.1。
|
||
|
||
## 9. 测试策略
|
||
依既有无测试桩 + 依赖 live 服务器的现实,聚焦**纯逻辑单测**(GoogleTest + CTest):
|
||
- `dto/NavDto` 映射:喂样本 JSON(取自 OpenAPI example / 手造)验证
|
||
`parseWorkspaces / parseProjects / parseStructNodes / parseDsRows` 字段与 `name→typeName`、`isCurTenant→isCurrent`。
|
||
- `buildStructTree` 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS)、孤儿 parentId、空列表、防环 等场景。
|
||
- 不做 live 集成 / E2E(无桩、依赖真实后端)。控制器/UI 信号联动靠手动联调验证。
|
||
- 目标:纯逻辑文件(dto + tree builder)覆盖率优先达标;UI/网络 IO 不计入。
|
||
|
||
## 10. 线程 / 性能
|
||
- 同步阻塞 UI 线程(`ApiClient` 用 QEventLoop)+ `busyChanged` 置 `Qt::WaitCursor`,与现有登录一致。
|
||
- 切空间/项目可能稍慢但可接受(MVP)。异步(QFuture/取消)留 M1.5,届时 `IProjectRepository` 契约可平滑改造。
|
||
|
||
## 11. 文件清单
|
||
|
||
**新增**
|
||
- `src/data/repo/IProjectRepository.hpp`(含 `RepoResult<T>`、导航模型 `Workspace/ProjectSummary/StructNode`,或拆 `NavTypes.hpp`)
|
||
- `src/data/api/ApiProjectRepository.{hpp,cpp}`
|
||
- `src/data/dto/NavDto.{hpp,cpp}`
|
||
- `src/controller/WorkbenchNavController.{hpp,cpp}`
|
||
- `src/app/panels/ObjectTreePanel.{hpp,cpp}`(若不抽,则树构建函数留在 main,但 TopBar 必抽)
|
||
- `src/app/CentralScene.{hpp,cpp}`(中央三维编排的数据驱动 helper,见 §8.1)
|
||
- `src/app/ProjectListDialog.{hpp,cpp}`(项目列表弹窗,见 §5.5)
|
||
- 测试:`tests/data/test_nav_dto.cpp`(NavDto 映射 + buildStructTree)
|
||
|
||
**改造**
|
||
- `src/app/TopBar.{hpp,cpp}` — 升级为数据驱动类 + 信号
|
||
- `src/app/main.cpp` — 构造 repo/controller、接线信号;移除启动自动渲染 demo;DS 点击改占位
|
||
- 各层 `CMakeLists.txt` — 新增源文件 + `controller` 目标接入构建;controller 需 `Q_OBJECT`(AUTOMOC ON)
|
||
|
||
**保留不删**:`LocalSampleRepository`、`render/*`、`VoxelFromScatters`、现有详情/中央渲染代码。
|
||
|
||
## 12. 未决 / 下一轮(概览)
|
||
- dd/ert/gpr 真实剖面/反演/雷达数据渲染(替换占位)—— 详见 §12.1。
|
||
- 项目 `crsCode` 替换硬编码 `EPSG:4547`,重建 `GeoLocalFrame`。
|
||
- 异步仓储(QFuture + 取消 + 分页"加载更多")。
|
||
- 用户头像/姓名接 `auth/getUserInfo`;token 过期自动跳登录。
|
||
- 顶部菜单(视图/项目管理/业务工具/设备)接真实页面。
|
||
|
||
### 12.1 下一轮:对接真实 DS 数据要做什么
|
||
|
||
本轮已把导航与渲染解耦、并保留 render 层与数据驱动 helper(§8.1)。下一轮把"占位"换成真实剖面/反演,
|
||
**render 层函数原样复用**,只需补"取数 → 映射 → 接线 → 坐标系"四件事:
|
||
|
||
**A. 取数:新增 DS 内容仓储(接口/数据层)**
|
||
- 在 `IProjectRepository`(或新建 `IDatasetContentRepository`)补方法,按 DS 拉真实内容。候选接口:
|
||
- 反演网格(#18 来源):`GET /business/dd/ert/inversion/rows/{dsObjectId}`、`horizontal/rows/{dsObjectId}`、
|
||
`dynamic/form/{dsObjectId}`(按实际返回选定网格来源)。
|
||
- 原始散点(#17 来源):`GET /business/dd/ert/inversion/getErtRawDataScatterGraph/{dsObjectId}`
|
||
或 `dd/indicator/currentmethod/scatter/graph/{dsObjectId}`。
|
||
- 异常:`GET /business/exception/queryException/{dsId}`。
|
||
- 色阶:`GET /business/clr/colorGradation/queryCLRColorGradation/{projectId}` /
|
||
`lvlTemplate/queryLVLTemplate/{projectId}`。
|
||
- 在 `ApiProjectRepository` 实现;返回 `RepoResult<T>`。
|
||
|
||
**B. 映射:DTO → 现有 core 模型(数据层 `dto/`)**
|
||
- 把上述接口的 JSON 映射成**现有类型**:`core::Grid`、`core::ScatterField`、`core::ColorScale`、`core::Anomaly`。
|
||
(这是新增解析,每个 dd 接口形状不同;参考 `data/parse/SampleParsers` 对本地样本的映射约定:
|
||
`v` 为 `[j=y][i=x]`、east/north 名值约定、纵向夸张统一常量等。)
|
||
- 单测:喂样本 JSON 验证映射(沿用本轮 NavDto 测试套路)。
|
||
|
||
**C. 接线:复活中央 + 详情编排(UI 层,零改 render)**
|
||
- 中央三维:`controller.selectDataset(dsId)` → 取反演网格 + 色阶 → 构建 `std::vector<app::SectionInput>` →
|
||
调 **本轮已写好的** `app::rebuildCentralScene(...)`(§8.1)。勾选多 TM/DS 即多 section 共存。
|
||
- 数据详情:用真实 `Grid/ScatterField/ColorScale/Anomaly` 触发**本轮保留的** `rebuildDetail`(改其触发为真实数据)。
|
||
- 把本轮的"占位文案"替换为真实渲染调用。
|
||
|
||
**D. 坐标系:用真实项目 CRS**
|
||
- 用本轮控制器已存的 `currentCrsCode()` 重建 `GeoLocalFrame` 与 `CrsTransform`,替换 `main.cpp` 硬编码 `EPSG:4547`;
|
||
切项目时重建世界系,保证多视图配准。
|
||
|
||
**E.(可选)体素 / 切片 / 地形**
|
||
- 这些是不绑定单 DS 的派生层,需各自的真实数据源(多剖面体素插值 / DEM 影像服务)才能复活;
|
||
与 A–D 的"按 DS 渲染剖面"是独立工作,按需另起一轮。
|
||
```
|