Compare commits
No commits in common. "475af464d9029edca0cef819b35428f8b9bc4353" and "405fb2ae4ff42f471e1bb2b59ef85f2ac2a8e859" have entirely different histories.
475af464d9
...
405fb2ae4f
|
|
@ -24,11 +24,8 @@
|
|||
|
||||
**做(In Scope)**
|
||||
- 工作空间列表 / 切换(真实接口)。
|
||||
- 项目列表 / 切换(真实接口):下拉显示首页项目(首页 10);项目数超过首页 → 下拉底部「全部项目…」打开
|
||||
**项目列表弹窗**(名称/类型过滤 + 分页 + 8 列表格:序号/名称/编号/状态/类型/业主/负责人/创建时间;点项目名切换并关弹窗)。
|
||||
- 项目列表 / 切换(真实接口)。
|
||||
- 对象树:**按真实结构显示 GS 层**(项目根 → GS → TM);TM 在左下"数据真实显示栏"列出其 DS。
|
||||
- DS **数据/文件两个页签**接真实分页接口(每页 5);每行显示"名称 / 创建时间 · 类型(数据)或大小(文件)";
|
||||
超过首页 → 列表末尾「加载更多」追加下一页。
|
||||
- 真实接口失败(断网 / token 过期 / 无数据)→ **显示错误 / 空状态**,**不回退本地样本**。
|
||||
- 项目 `referenceCRSCode` 存入导航状态,供下一轮替换硬编码 `EPSG:4547`(本轮不改渲染)。
|
||||
|
||||
|
|
@ -37,7 +34,7 @@
|
|||
→ 点击真实 DS 时中央/详情显示**占位"待接入"**;`render/*` 与 `LocalSampleRepository` 代码**保留不删**。
|
||||
- 异步仓储(QFuture/回调)—— 本轮同步阻塞 + WaitCursor(与登录一致),异步留 M1.5。
|
||||
- 用户头像 / 姓名接真实 `auth/getUserInfo`(本轮先留静态)。
|
||||
- 文件下载(文件页签已展示文件名/大小、下载 `url` 已存入列表项备用,实际下载动作留后续)。
|
||||
- 项目分页"加载更多"的滚动 UI(接口支持 `hasNextPage`,本轮先取首页;翻页留待后续)。
|
||||
|
||||
## 3. 接口确认结论
|
||||
|
||||
|
|
@ -48,15 +45,13 @@ 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}` |
|
||||
| 项目列表 | GET | `/business/project/queryByUser?lastProjectId=` | `{hasNextPage, projectList:[{id, projectName, projectTypeName, referenceCRSCode, referenceCRSName, status, ...}]}`(游标分页,首页传空 lastProjectId) |
|
||||
| 项目结构 | POST | `/business/projectWorkbench/queryProjectStruct` | body `{projectId}`;data `{projectStructList:[{id, name, parentId, type, typeId, typeName, confCode}]}` |
|
||||
| TM 下 DS | GET | `/business/projectWorkbench/queryDsByTmObjectId/{tmObjectId}` | `[{id, name, ddCode, typeName}]` |
|
||||
|
||||
**层级确认(修正需求方假设)**:真实结构**不是** `项目→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 不在结构列表里,按 TM 单独拉取(`queryDsByTmObjectId`)。
|
||||
- **项目不能直接挂 DS**;DS 永远挂在 TM 下。但由于是 `parentId` 扁平结构,**TM 可直接挂在项目下(无中间 GS)**——这是"项目直接挂"印象的来源,但叶子仍是 TM→DS。
|
||||
|
||||
**节点判定**:结构列表只含 GS+TM,故 **TM = 该节点在结构列表中无子节点(叶子)**;非叶子 = GS。
|
||||
|
|
@ -113,19 +108,15 @@ token 已由登录注入(`geomativeauthorization` 头),下列接口直接
|
|||
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=进行中(其余显示数字)
|
||||
int status = 0;
|
||||
};
|
||||
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` 仅本地样本仓储继续用。
|
||||
`DsNode{id,name,ddType}` 复用;映射时 `ddCode → ddType`。
|
||||
|
||||
### 5.3 数据访问层 `data`
|
||||
|
||||
|
|
@ -141,35 +132,25 @@ 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<ProjectSummary>> listProjects(const std::string& lastProjectId) = 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;
|
||||
virtual RepoResult<std::vector<DsNode>> loadDatasetsOfTm(const std::string& tmObjectId) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
**`api/ApiProjectRepository.{hpp,cpp}`** — 实现:持有 `net::ApiClient&`,
|
||||
按 §3 路径发请求,把 `ApiResponse` 交给 `dto/` 映射;网络/业务码错误 → `RepoResult{ok=false, error=msg}`。
|
||||
判定成功:`code==200`(沿用登录 `AuthService` 的约定,业务码即成功标志)。id 进 URL 路径/查询前
|
||||
经 `QUrl::toPercentEncoding` 百分号编码(不可信后端数据:防 `? # & /` 空格 破坏 URL)。
|
||||
判定成功:`httpStatus==200 && code==<成功码>`(成功码沿用登录约定,实现时核对)。
|
||||
|
||||
**`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)。
|
||||
- `parseProjects(QJsonObject) -> {vector<ProjectSummary>, bool hasNextPage}`。
|
||||
- `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)。
|
||||
- `parseDatasets(QJsonArray) -> vector<DsNode>`(`ddCode→ddType`)。
|
||||
- `buildProjectTree(vector<StructNode>, projectName) -> Project`:扁平→树。
|
||||
- 以 `parentId` 归并;`parentId` 为空或不在集合内的节点挂到合成"项目根"。
|
||||
- **叶子节点判定为 TM**(进 `TmNode`,携带 `confCode`/真实 id 作 tmObjectId);非叶子为 GS。
|
||||
- TM 的 `dss` 本轮留空(DS 懒加载)。
|
||||
|
||||
### 5.4 逻辑层 `controller/WorkbenchNavController`(QObject)
|
||||
唯一持有导航状态;不碰 widget;经信号把模型推给 UI、经槽接收用户意图。
|
||||
|
|
@ -183,57 +164,35 @@ public:
|
|||
public slots:
|
||||
void switchWorkspace(const QString& tenantId); // 切空间→重载项目→重载结构
|
||||
void switchProject(const QString& projectId); // 切项目→重载结构(清 DS/详情)
|
||||
void selectTm(const QString& tmObjectId); // 选 TM→拉其 DS 首页(数据+文件)
|
||||
void loadMoreData(); // 数据页签"加载更多"→下一页追加
|
||||
void loadMoreFiles(); // 文件页签"加载更多"→下一页追加
|
||||
void selectTm(const QString& tmObjectId); // 选 TM→拉其 DS
|
||||
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 projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId);
|
||||
void structureLoaded(const data::Project&); // 已建好的树
|
||||
void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsNode>&);
|
||||
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; // 重入保护:同步请求期间拒绝再次进入
|
||||
QString currentWorkspaceId_, currentProjectId_, currentCrsCode_;
|
||||
};
|
||||
```
|
||||
编排逻辑:`start()` → `listWorkspaces`(选 isCurrent/首个)→ `pageProjects`(首页 10,选首个)→ `loadStructure`→发扁平节点。
|
||||
`selectTm` 拉数据/文件首页(每页 5);`loadMoreData/Files` 递增页码追加。`switchWorkspace` 成功后用返回的新 accessToken 重注入 ApiClient(否则后续请求仍落旧空间)。
|
||||
编排逻辑:`start()` → `listWorkspaces`(选 isCurrent/首个)→ `listProjects`(选首个)→ `loadStructure`→建树。
|
||||
切空间/项目按 §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` 不变。
|
||||
- `setWorkspaces(list, currentId)` / `setProjects(list, currentId)` 重建下拉项。
|
||||
- 信号 `workspaceSwitchRequested(QString id)` / `projectSwitchRequested(QString id)`。
|
||||
- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。
|
||||
- `buildMenuBar` 不变(静态菜单本轮不接)。
|
||||
|
||||
**`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector<StructNode>)` 内部调
|
||||
`dto::buildStructTree` 重建 `QTreeWidget`(项目根→GS→TM,叶子=TM 可勾选、`UserRole` 存 tmObjectId);
|
||||
`showMessage(msg)` 显示空/错占位。信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)`
|
||||
(后者为前瞻钩子,本轮无消费者)。
|
||||
**`app/panels/ObjectTreePanel`**(新增;或先以构建函数落在 main,二选一见 §11)—— 被动:
|
||||
`setProject(const data::Project&)` 重建 `QTreeWidget`(项目根→GS→TM,TM 可勾选、存 tmObjectId);
|
||||
信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)`。空/错状态:树区显示占位 label。
|
||||
|
||||
**`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)`。
|
||||
**`app/panels/DatasetListPanel`**(已有)—— `datasetsLoaded` → `populateDatasetList`;空时显示"暂无数据集"。
|
||||
|
||||
**中央/详情**:移除"启动自动渲染本地 demo";DS 点击 → 详情面板与中央视图显示占位文案
|
||||
"该数据集渲染将在下一阶段接入 dd 接口"。渲染代码保留。
|
||||
|
|
@ -246,7 +205,7 @@ private:
|
|||
controller.start():
|
||||
listWorkspaces → emit workspacesLoaded → TopBar.setWorkspaces
|
||||
listProjects(empty) → emit projectsLoaded → TopBar.setProjects
|
||||
loadStructure(currentProject) → emit structureLoaded(name,nodes) → ObjectTreePanel.setStructure(→buildStructTree)
|
||||
loadStructure(currentProject) → buildProjectTree → emit structureLoaded → ObjectTreePanel.setProject
|
||||
|
||||
切空间: TopBar.workspaceSwitchRequested(id)
|
||||
→ controller.switchWorkspace: switchWorkspace(id) → listProjects → 选首个 → loadStructure
|
||||
|
|
@ -255,12 +214,8 @@ private:
|
|||
切项目: 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) → 追加
|
||||
→ controller.selectTm: loadDatasetsOfTm → emit datasetsLoaded → DatasetListPanel 填充
|
||||
|
||||
点 DS: DatasetListPanel → 中央/详情显示占位"待接入"(本轮不渲染真实数据)
|
||||
```
|
||||
|
|
@ -270,8 +225,7 @@ private:
|
|||
- controller 任一阶段失败 → `loadFailed(stage, msg)`;UI 在对应面板显示空/错状态 label + 状态栏提示,**不回退本地样本**。
|
||||
- 空数据(无空间 / 无项目 / 无结构 / 无 DS)→ 各面板显示"暂无…"占位(识别优于回忆)。
|
||||
- token 过期(业务码 401 类)→ `loadFailed` 文案提示重新登录(本轮先提示,自动跳登录留后续)。
|
||||
- 输入边界:`tmObjectId` / `projectId` 为空时短路不发请求;URL 中的 id 一律百分号编码(见 §5.3)。
|
||||
- 重入:同步请求期间 `busy_` 拒绝再次进入(避免快速点击重入污染状态,见 §5.4)。
|
||||
- 输入边界:`tmObjectId` / `projectId` 为空时短路不发请求。
|
||||
|
||||
## 8. 渲染解耦
|
||||
|
||||
|
|
@ -319,8 +273,8 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
|
|||
## 9. 测试策略
|
||||
依既有无测试桩 + 依赖 live 服务器的现实,聚焦**纯逻辑单测**(GoogleTest + CTest):
|
||||
- `dto/NavDto` 映射:喂样本 JSON(取自 OpenAPI example / 手造)验证
|
||||
`parseWorkspaces / parseProjects / parseStructNodes / parseDsRows` 字段与 `name→typeName`、`isCurTenant→isCurrent`。
|
||||
- `buildStructTree` 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS)、孤儿 parentId、空列表、防环 等场景。
|
||||
`parseWorkspaces / parseProjects / parseStructNodes / parseDatasets` 字段与 `ddCode→ddType`、`isCurTenant→isCurrent`。
|
||||
- `buildProjectTree` 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS)、孤儿 parentId、空列表 等场景。
|
||||
- 不做 live 集成 / E2E(无桩、依赖真实后端)。控制器/UI 信号联动靠手动联调验证。
|
||||
- 目标:纯逻辑文件(dto + tree builder)覆盖率优先达标;UI/网络 IO 不计入。
|
||||
|
||||
|
|
@ -337,7 +291,6 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
|
|||
- `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)
|
||||
|
||||
**改造**
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/AnomalyListPanel.cpp
|
||||
panels/DatasetListPanel.cpp
|
||||
panels/ObjectTreePanel.cpp
|
||||
CentralScene.cpp
|
||||
ProjectListDialog.cpp)
|
||||
CentralScene.cpp)
|
||||
|
||||
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,160 +0,0 @@
|
|||
#include "ProjectListDialog.hpp"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QColor>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace geopro::app {
|
||||
namespace {
|
||||
QString statusText(int s) {
|
||||
switch (s) {
|
||||
case 1: return QStringLiteral("未开始");
|
||||
case 2: return QStringLiteral("进行中");
|
||||
default: return QString::number(s);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent)
|
||||
: QDialog(parent), repo_(repo) {
|
||||
setWindowTitle(QStringLiteral("全部项目"));
|
||||
resize(980, 560);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
|
||||
auto* filter = new QHBoxLayout();
|
||||
filter->addWidget(new QLabel(QStringLiteral("项目名称"), this));
|
||||
nameEdit_ = new QLineEdit(this);
|
||||
nameEdit_->setPlaceholderText(QStringLiteral("输入项目名称"));
|
||||
nameEdit_->setFixedWidth(200);
|
||||
filter->addWidget(nameEdit_);
|
||||
filter->addSpacing(8);
|
||||
filter->addWidget(new QLabel(QStringLiteral("项目类型"), this));
|
||||
typeCombo_ = new QComboBox(this);
|
||||
typeCombo_->setFixedWidth(160);
|
||||
filter->addWidget(typeCombo_);
|
||||
filter->addSpacing(8);
|
||||
auto* searchBtn = new QPushButton(QStringLiteral("搜索"), this);
|
||||
auto* resetBtn = new QPushButton(QStringLiteral("重置"), this);
|
||||
filter->addWidget(searchBtn);
|
||||
filter->addWidget(resetBtn);
|
||||
filter->addStretch();
|
||||
root->addLayout(filter);
|
||||
|
||||
table_ = new QTableWidget(this);
|
||||
table_->setColumnCount(8);
|
||||
table_->setHorizontalHeaderLabels(QStringList{
|
||||
QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"),
|
||||
QStringLiteral("项目状态"), QStringLiteral("项目类型"), QStringLiteral("业主单位"),
|
||||
QStringLiteral("负责人"), QStringLiteral("创建时间")});
|
||||
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table_->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table_->verticalHeader()->setVisible(false);
|
||||
table_->horizontalHeader()->setStretchLastSection(true);
|
||||
table_->setColumnWidth(0, 50);
|
||||
table_->setColumnWidth(1, 260);
|
||||
root->addWidget(table_, 1);
|
||||
|
||||
auto* bottom = new QHBoxLayout();
|
||||
pageLabel_ = new QLabel(this);
|
||||
bottom->addWidget(pageLabel_);
|
||||
bottom->addStretch();
|
||||
prevBtn_ = new QPushButton(QStringLiteral("上一页"), this);
|
||||
nextBtn_ = new QPushButton(QStringLiteral("下一页"), this);
|
||||
bottom->addWidget(prevBtn_);
|
||||
bottom->addWidget(nextBtn_);
|
||||
root->addLayout(bottom);
|
||||
|
||||
fillTypeFilter();
|
||||
|
||||
QObject::connect(searchBtn, &QPushButton::clicked, this, [this]() {
|
||||
pageNo_ = 1;
|
||||
query();
|
||||
});
|
||||
QObject::connect(resetBtn, &QPushButton::clicked, this, [this]() {
|
||||
nameEdit_->clear();
|
||||
typeCombo_->setCurrentIndex(0);
|
||||
pageNo_ = 1;
|
||||
query();
|
||||
});
|
||||
QObject::connect(prevBtn_, &QPushButton::clicked, this, [this]() {
|
||||
if (pageNo_ > 1) {
|
||||
--pageNo_;
|
||||
query();
|
||||
}
|
||||
});
|
||||
QObject::connect(nextBtn_, &QPushButton::clicked, this, [this]() {
|
||||
if (pageNo_ * pageSize_ < total_) {
|
||||
++pageNo_;
|
||||
query();
|
||||
}
|
||||
});
|
||||
QObject::connect(table_, &QTableWidget::cellClicked, this, [this](int row, int col) {
|
||||
if (col != 1) return; // 仅"项目名称"列触发切换
|
||||
auto* item = table_->item(row, 1);
|
||||
if (!item) return;
|
||||
const QString id = item->data(Qt::UserRole).toString();
|
||||
if (id.isEmpty()) return;
|
||||
emit projectChosen(id, item->text());
|
||||
accept();
|
||||
});
|
||||
|
||||
query();
|
||||
}
|
||||
|
||||
void ProjectListDialog::fillTypeFilter() {
|
||||
typeCombo_->addItem(QStringLiteral("全部类型"), QString());
|
||||
const auto r = repo_.listProjectTypes();
|
||||
if (!r.ok) return;
|
||||
for (const auto& t : r.value)
|
||||
typeCombo_->addItem(QString::fromStdString(t.name), QString::fromStdString(t.id));
|
||||
}
|
||||
|
||||
void ProjectListDialog::query() {
|
||||
const std::string name = nameEdit_->text().trimmed().toStdString();
|
||||
const std::string typeId = typeCombo_->currentData().toString().toStdString();
|
||||
const auto r = repo_.pageProjects(name, typeId, pageNo_, pageSize_);
|
||||
if (!r.ok) {
|
||||
table_->setRowCount(0);
|
||||
pageLabel_->setText(QStringLiteral("加载失败:%1").arg(QString::fromStdString(r.error)));
|
||||
prevBtn_->setEnabled(false);
|
||||
nextBtn_->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
total_ = r.value.total;
|
||||
const auto& rows = r.value.rows;
|
||||
table_->setRowCount(static_cast<int>(rows.size()));
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
const auto& p = rows[i];
|
||||
auto set = [&](int col, const QString& text) {
|
||||
table_->setItem(i, col, new QTableWidgetItem(text));
|
||||
};
|
||||
set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1));
|
||||
auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name));
|
||||
nameItem->setData(Qt::UserRole, QString::fromStdString(p.id));
|
||||
nameItem->setForeground(QColor("#2D6CB5"));
|
||||
table_->setItem(i, 1, nameItem);
|
||||
set(2, QString::fromStdString(p.code));
|
||||
set(3, statusText(p.status));
|
||||
set(4, QString::fromStdString(p.typeName));
|
||||
set(5, QString::fromStdString(p.ownerCompany));
|
||||
set(6, QString::fromStdString(p.responsiblePerson));
|
||||
set(7, QString::fromStdString(p.createTime));
|
||||
}
|
||||
const int pages = total_ > 0 ? (total_ + pageSize_ - 1) / pageSize_ : 1;
|
||||
pageLabel_->setText(QStringLiteral("共 %1 条 第 %2 / %3 页").arg(total_).arg(pageNo_).arg(pages));
|
||||
prevBtn_->setEnabled(pageNo_ > 1);
|
||||
nextBtn_->setEnabled(pageNo_ < pages);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#pragma once
|
||||
#include <QDialog>
|
||||
|
||||
#include "repo/IProjectRepository.hpp"
|
||||
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
class QTableWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 项目列表弹窗:名称/类型过滤 + 分页表格;点项目名 → 切换项目并关闭。
|
||||
class ProjectListDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ProjectListDialog(data::IProjectRepository& repo, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void projectChosen(const QString& projectId, const QString& projectName);
|
||||
|
||||
private:
|
||||
void query();
|
||||
void fillTypeFilter();
|
||||
|
||||
data::IProjectRepository& repo_;
|
||||
QLineEdit* nameEdit_ = nullptr;
|
||||
QComboBox* typeCombo_ = nullptr;
|
||||
QTableWidget* table_ = nullptr;
|
||||
QLabel* pageLabel_ = nullptr;
|
||||
QPushButton* prevBtn_ = nullptr;
|
||||
QPushButton* nextBtn_ = nullptr;
|
||||
int pageNo_ = 1;
|
||||
int pageSize_ = 20;
|
||||
int total_ = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -211,8 +211,6 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
|||
auto* header = menu->addAction(QStringLiteral("切换空间"));
|
||||
header->setEnabled(false);
|
||||
menu->addSeparator();
|
||||
auto* group = new QActionGroup(menu);
|
||||
group->setExclusive(true); // 互斥:只一个勾选,避免“多选”
|
||||
QString currentName;
|
||||
for (const auto& w : list) {
|
||||
const QString id = QString::fromStdString(w.id);
|
||||
|
|
@ -220,12 +218,9 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
|||
auto* a = menu->addAction(name);
|
||||
a->setCheckable(true);
|
||||
a->setChecked(id == currentId);
|
||||
group->addAction(a);
|
||||
if (id == currentId) currentName = name;
|
||||
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||
wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈
|
||||
emit workspaceSwitchRequested(id);
|
||||
});
|
||||
QObject::connect(a, &QAction::triggered, this,
|
||||
[this, id]() { emit workspaceSwitchRequested(id); });
|
||||
}
|
||||
if (list.empty()) {
|
||||
auto* none = menu->addAction(QStringLiteral("(暂无空间)"));
|
||||
|
|
@ -236,14 +231,11 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
|||
QStringLiteral(" ▾"));
|
||||
}
|
||||
|
||||
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
|
||||
bool hasMore) {
|
||||
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId) {
|
||||
auto* menu = new QMenu(projBtn_);
|
||||
auto* header = menu->addAction(QStringLiteral("切换项目"));
|
||||
header->setEnabled(false);
|
||||
menu->addSeparator();
|
||||
auto* group = new QActionGroup(menu);
|
||||
group->setExclusive(true);
|
||||
QString currentName;
|
||||
for (const auto& p : list) {
|
||||
const QString id = QString::fromStdString(p.id);
|
||||
|
|
@ -251,29 +243,17 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QS
|
|||
auto* a = menu->addAction(name);
|
||||
a->setCheckable(true);
|
||||
a->setChecked(id == currentId);
|
||||
group->addAction(a);
|
||||
if (id == currentId) currentName = name;
|
||||
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||
projBtn_->setText(name + QStringLiteral(" ▾"));
|
||||
emit projectSwitchRequested(id);
|
||||
});
|
||||
QObject::connect(a, &QAction::triggered, this,
|
||||
[this, id]() { emit projectSwitchRequested(id); });
|
||||
}
|
||||
if (list.empty()) {
|
||||
auto* none = menu->addAction(QStringLiteral("(暂无项目)"));
|
||||
none->setEnabled(false);
|
||||
}
|
||||
if (hasMore) {
|
||||
menu->addSeparator();
|
||||
auto* all = menu->addAction(QStringLiteral("全部项目…"));
|
||||
QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); });
|
||||
}
|
||||
projBtn_->setMenu(menu);
|
||||
projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) +
|
||||
QStringLiteral(" ▾"));
|
||||
}
|
||||
|
||||
void TopBar::setProjectButtonText(const QString& name) {
|
||||
projBtn_->setText(name + QStringLiteral(" ▾"));
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -17,14 +17,11 @@ public:
|
|||
explicit TopBar(QWidget* parent = nullptr);
|
||||
|
||||
void setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId);
|
||||
void setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
|
||||
bool hasMore);
|
||||
void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字
|
||||
void setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId);
|
||||
|
||||
signals:
|
||||
void workspaceSwitchRequested(const QString& tenantId);
|
||||
void projectSwitchRequested(const QString& projectId);
|
||||
void allProjectsRequested(); // 点击"全部项目…"
|
||||
|
||||
private:
|
||||
QToolButton* wsBtn_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@
|
|||
#include "Theme.hpp"
|
||||
#include "TopBar.hpp"
|
||||
#include "CentralScene.hpp"
|
||||
#include "ProjectListDialog.hpp"
|
||||
#include "WorkbenchNavController.hpp"
|
||||
#include "api/ApiProjectRepository.hpp"
|
||||
#include "panels/ObjectTreePanel.hpp"
|
||||
|
|
@ -139,7 +138,6 @@ constexpr const char* kWgs84 = "EPSG:4326";
|
|||
// 在给定 QMainWindow 上构建 M1 工作台。
|
||||
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
|
||||
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo,
|
||||
geopro::data::IProjectRepository& projectRepo,
|
||||
geopro::controller::WorkbenchNavController& nav)
|
||||
{
|
||||
// ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ──
|
||||
|
|
@ -339,15 +337,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||||
auto* datasetTabs = new QTabWidget();
|
||||
auto* datasetList = new QListWidget();
|
||||
// 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。
|
||||
datasetList->setStyleSheet(QStringLiteral(
|
||||
"QListWidget{ background:#FFFFFF; border:none; outline:none; }"
|
||||
"QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }"
|
||||
"QListWidget::item:hover{ background:#F5F8FD; }"
|
||||
"QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"));
|
||||
datasetList->setAlternatingRowColors(true);
|
||||
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||||
auto* fileList = new QListWidget();
|
||||
fileList->setStyleSheet(datasetList->styleSheet()); // 与数据页签同款简洁分割
|
||||
auto* fileList = new QListWidget(); // M1 文件 tab 占位
|
||||
{ // 空状态引导:M1 暂无文件来源,给出说明而非空白面板(识别优先于回忆)。
|
||||
auto* hint = new QListWidgetItem(QStringLiteral("(M1 暂无关联文件)"), fileList);
|
||||
hint->setFlags(Qt::NoItemFlags);
|
||||
hint->setForeground(QColor("#9AA6B6"));
|
||||
hint->setTextAlignment(Qt::AlignCenter);
|
||||
}
|
||||
datasetTabs->addTab(fileList, QStringLiteral("文件"));
|
||||
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
|
||||
auto* datasetBox = wrapWithHeader(
|
||||
|
|
@ -500,15 +498,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
||||
.arg(anomalies.size()));
|
||||
};
|
||||
(void)loadDataset; // 暂未触发:保留待下一轮真实 DS 详情渲染复用
|
||||
|
||||
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
|
||||
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
|
||||
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
|
||||
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
|
||||
nav.loadMoreData();
|
||||
return;
|
||||
}
|
||||
[propLabel, detailRendererPtr, detailRenderWindowPtr](QListWidgetItem* item) {
|
||||
const QString name =
|
||||
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
|
||||
detailRendererPtr->RemoveAllViewProps();
|
||||
|
|
@ -623,38 +616,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
}
|
||||
|
||||
// ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
|
||||
// "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。
|
||||
auto removeLoadMore = [](QListWidget* lw) {
|
||||
if (lw->count() > 0 &&
|
||||
lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool())
|
||||
delete lw->takeItem(lw->count() - 1);
|
||||
};
|
||||
auto addLoadMore = [](QListWidget* lw, int total) {
|
||||
const int loaded = lw->count();
|
||||
if (loaded < total) {
|
||||
auto* m = new QListWidgetItem(
|
||||
QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw);
|
||||
m->setData(geopro::app::kDsLoadMoreRole, true);
|
||||
m->setTextAlignment(Qt::AlignCenter);
|
||||
m->setForeground(QColor("#2D6CB5"));
|
||||
}
|
||||
return loaded;
|
||||
};
|
||||
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
|
||||
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
||||
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
|
||||
&geopro::controller::WorkbenchNavController::switchProject);
|
||||
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
|
||||
[&projectRepo, &nav, topBar, &window]() {
|
||||
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
QObject::connect(dlg, &geopro::app::ProjectListDialog::projectChosen, &nav,
|
||||
[&nav, topBar](const QString& id, const QString& name) {
|
||||
topBar->setProjectButtonText(name);
|
||||
nav.switchProject(id);
|
||||
});
|
||||
dlg->exec();
|
||||
});
|
||||
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::tmClicked, &nav,
|
||||
&geopro::controller::WorkbenchNavController::selectTm);
|
||||
|
||||
|
|
@ -664,46 +629,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
});
|
||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar,
|
||||
[topBar](const std::vector<geopro::data::ProjectSummary>& list,
|
||||
const QString& cur, int total) {
|
||||
topBar->setProjects(list, cur, total > static_cast<int>(list.size()));
|
||||
});
|
||||
const QString& cur) { topBar->setProjects(list, cur); });
|
||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
|
||||
[objectTree, datasetList, fileList, datasetTitle, datasetTabs](
|
||||
[objectTree, datasetList, datasetTitle, datasetTabs](
|
||||
const QString& projectName,
|
||||
const std::vector<geopro::data::StructNode>& nodes) {
|
||||
objectTree->setStructure(projectName, nodes);
|
||||
datasetList->clear();
|
||||
fileList->clear();
|
||||
datasetList->clear(); // 切项目清空 DS 列表
|
||||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
||||
datasetTabs->setTabText(0, QStringLiteral("数据"));
|
||||
datasetTabs->setTabText(1, QStringLiteral("文件"));
|
||||
});
|
||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
|
||||
[removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs](
|
||||
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
|
||||
bool append) {
|
||||
removeLoadMore(datasetList);
|
||||
geopro::app::populateDatasetList(datasetList, rows, append);
|
||||
const int loaded = addLoadMore(datasetList, total);
|
||||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
||||
[datasetList, datasetTitle, datasetTabs](
|
||||
const QString&, const std::vector<geopro::data::DsNode>& list) {
|
||||
geopro::app::populateDatasetList(datasetList, list);
|
||||
if (datasetTitle)
|
||||
datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
||||
datasetTabs->setTabText(
|
||||
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
|
||||
: QStringLiteral("数据"));
|
||||
});
|
||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
|
||||
[removeLoadMore, addLoadMore, fileList, datasetTabs](
|
||||
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
|
||||
bool append) {
|
||||
removeLoadMore(fileList);
|
||||
geopro::app::populateFileList(fileList, rows, append);
|
||||
const int loaded = addLoadMore(fileList, total);
|
||||
datasetTabs->setTabText(
|
||||
1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total)
|
||||
: QStringLiteral("文件"));
|
||||
});
|
||||
QObject::connect(fileList, &QListWidget::itemClicked, fileList,
|
||||
[&nav](QListWidgetItem* item) {
|
||||
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles();
|
||||
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(list.size())));
|
||||
});
|
||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
|
||||
[objectTree, &window](const QString& stage, const QString& msg) {
|
||||
|
|
@ -787,7 +730,7 @@ int main(int argc, char* argv[])
|
|||
window.resize(1280, 800);
|
||||
window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸
|
||||
|
||||
buildWorkbench(window, repo, projectRepo, nav);
|
||||
buildWorkbench(window, repo, nav);
|
||||
window.show();
|
||||
|
||||
nav.start(); // 进入工作台后拉真实 空间/项目/结构
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "panels/DatasetListPanel.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QString>
|
||||
|
|
@ -8,48 +7,30 @@
|
|||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
QString humanSize(long long b) {
|
||||
if (b < 1024) return QStringLiteral("%1 B").arg(b);
|
||||
const double kb = b / 1024.0;
|
||||
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
|
||||
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
|
||||
|
||||
// dd 类型 → 中文标注。
|
||||
QString ddTypeLabel(const std::string& ddType)
|
||||
{
|
||||
if (ddType == "dd_section") return QStringLiteral("剖面网格");
|
||||
if (ddType == "dd_voxel") return QStringLiteral("体素");
|
||||
return QString::fromStdString(ddType);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
|
||||
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss)
|
||||
{
|
||||
if (!list) return;
|
||||
if (!append) list->clear();
|
||||
for (const auto& d : rows) {
|
||||
QString text = QString::fromStdString(d.dsName);
|
||||
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
|
||||
if (!d.typeName.empty())
|
||||
sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型
|
||||
if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub);
|
||||
auto* item = new QListWidgetItem(text, list);
|
||||
item->setData(kDsIdRole, QString::fromStdString(d.id));
|
||||
item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode));
|
||||
}
|
||||
}
|
||||
list->clear();
|
||||
for (const auto& ds : dss) {
|
||||
const QString name = QString::fromStdString(ds.name);
|
||||
const QString label = ddTypeLabel(ds.ddType);
|
||||
QString text = name;
|
||||
if (!label.isEmpty()) text += QStringLiteral("\n%1").arg(label);
|
||||
|
||||
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
|
||||
if (!list) return;
|
||||
if (!append) list->clear();
|
||||
if (!append && rows.empty()) {
|
||||
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
|
||||
hint->setFlags(Qt::NoItemFlags);
|
||||
hint->setForeground(QColor("#9AA6B6"));
|
||||
hint->setTextAlignment(Qt::AlignCenter);
|
||||
return;
|
||||
}
|
||||
for (const auto& d : rows) {
|
||||
const QString fname =
|
||||
d.fileName.empty() ? QString::fromStdString(d.dsName) : QString::fromStdString(d.fileName);
|
||||
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
|
||||
sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小
|
||||
const QString text = fname + QStringLiteral("\n%1").arg(sub);
|
||||
auto* item = new QListWidgetItem(text, list);
|
||||
item->setData(kDsIdRole, QString::fromStdString(d.id));
|
||||
item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl));
|
||||
item->setData(kDsIdRole, QString::fromStdString(ds.id));
|
||||
item->setData(kDsDdTypeRole, QString::fromStdString(ds.ddType));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ namespace geopro::app {
|
|||
// 数据列表条目角色(与 main.cpp 树一致:Qt::UserRole=dsId、+1=ddType)。
|
||||
constexpr int kDsIdRole = 0x0100; // Qt::UserRole
|
||||
constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1
|
||||
constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2(文件下载 url,备用)
|
||||
constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行
|
||||
|
||||
// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。
|
||||
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
|
||||
// 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。
|
||||
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
|
||||
// 用某测线(TM)的数据集(采集批次)填充 QListWidget(对齐原型左下「数据真实显示栏」)。
|
||||
// 每条目 = 名称 +(ddType 标注);UserRole 存 dsId、+1 存 ddType(供单击驱动数据详情)。
|
||||
// 清空旧条目后重填。
|
||||
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss);
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -79,7 +79,9 @@ void ObjectTreePanel::setStructure(const QString& projectName,
|
|||
}
|
||||
hint_->setVisible(false);
|
||||
tree_->setVisible(true);
|
||||
addNodes(tree_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染
|
||||
auto* rootItem = new QTreeWidgetItem(tree_);
|
||||
rootItem->setText(0, projectName.isEmpty() ? QStringLiteral("项目") : projectName);
|
||||
addNodes(rootItem, roots);
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ public:
|
|||
|
||||
signals:
|
||||
void tmClicked(const QString& tmObjectId);
|
||||
// 前瞻钩子:勾选驱动中央渲染留待下一轮接真实 DS(本轮暂无消费者)。
|
||||
void tmCheckToggled(const QString& tmObjectId, bool checked);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -8,27 +8,11 @@ using data::Workspace;
|
|||
WorkbenchNavController::WorkbenchNavController(data::IProjectRepository& repo, QObject* parent)
|
||||
: QObject(parent), repo_(repo) {}
|
||||
|
||||
namespace {
|
||||
// RAII:进入公共导航操作时置忙(驱动等待光标),任何返回路径都复位——保证 busyChanged 配平。
|
||||
struct BusyGuard {
|
||||
WorkbenchNavController* self;
|
||||
bool* busy;
|
||||
BusyGuard(WorkbenchNavController* s, bool* b) : self(s), busy(b) {
|
||||
*busy = true;
|
||||
emit self->busyChanged(true);
|
||||
}
|
||||
~BusyGuard() {
|
||||
*busy = false;
|
||||
emit self->busyChanged(false);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void WorkbenchNavController::start() {
|
||||
if (busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
emit busyChanged(true);
|
||||
const auto ws = repo_.listWorkspaces();
|
||||
if (!ws.ok) {
|
||||
emit busyChanged(false);
|
||||
emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error));
|
||||
return;
|
||||
}
|
||||
|
|
@ -38,19 +22,21 @@ void WorkbenchNavController::start() {
|
|||
if (cur.isEmpty() && !ws.value.empty()) cur = QString::fromStdString(ws.value.front().id);
|
||||
currentWorkspaceId_ = cur.toStdString();
|
||||
emit workspacesLoaded(ws.value, cur);
|
||||
|
||||
loadProjectsAndStructure();
|
||||
emit busyChanged(false);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::loadProjectsAndStructure() {
|
||||
const auto ps = repo_.pageProjects(std::string(), std::string(), 1, 10); // 下拉首页 10
|
||||
const auto ps = repo_.listProjects(std::string());
|
||||
if (!ps.ok) {
|
||||
emit loadFailed(QStringLiteral("projects"), QString::fromStdString(ps.error));
|
||||
return;
|
||||
}
|
||||
lastProjects_ = ps.value.rows;
|
||||
lastProjects_ = ps.value;
|
||||
QString curP;
|
||||
if (!ps.value.rows.empty()) {
|
||||
const auto& first = ps.value.rows.front();
|
||||
if (!ps.value.empty()) {
|
||||
const auto& first = ps.value.front();
|
||||
curP = QString::fromStdString(first.id);
|
||||
currentProjectId_ = first.id;
|
||||
currentProjectName_ = first.name;
|
||||
|
|
@ -60,7 +46,7 @@ void WorkbenchNavController::loadProjectsAndStructure() {
|
|||
currentProjectName_.clear();
|
||||
currentCrsCode_.clear();
|
||||
}
|
||||
emit projectsLoaded(ps.value.rows, curP, ps.value.total);
|
||||
emit projectsLoaded(ps.value, curP);
|
||||
|
||||
if (curP.isEmpty()) {
|
||||
emit structureLoaded(QString(), {}); // 暂无项目 → 空树
|
||||
|
|
@ -75,20 +61,22 @@ void WorkbenchNavController::loadProjectsAndStructure() {
|
|||
}
|
||||
|
||||
void WorkbenchNavController::switchWorkspace(const QString& tenantId) {
|
||||
if (tenantId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
if (tenantId.isEmpty()) return;
|
||||
emit busyChanged(true);
|
||||
const auto r = repo_.switchWorkspace(tenantId.toStdString());
|
||||
if (!r.ok) {
|
||||
emit busyChanged(false);
|
||||
emit loadFailed(QStringLiteral("switchWorkspace"), QString::fromStdString(r.error));
|
||||
return;
|
||||
}
|
||||
currentWorkspaceId_ = tenantId.toStdString();
|
||||
loadProjectsAndStructure();
|
||||
emit busyChanged(false);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::switchProject(const QString& projectId) {
|
||||
if (projectId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
if (projectId.isEmpty()) return;
|
||||
emit busyChanged(true);
|
||||
currentProjectId_ = projectId.toStdString();
|
||||
for (const auto& p : lastProjects_)
|
||||
if (p.id == currentProjectId_) {
|
||||
|
|
@ -97,57 +85,24 @@ void WorkbenchNavController::switchProject(const QString& projectId) {
|
|||
}
|
||||
const auto st = repo_.loadStructure(currentProjectId_);
|
||||
if (!st.ok) {
|
||||
emit busyChanged(false);
|
||||
emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error));
|
||||
return;
|
||||
}
|
||||
emit structureLoaded(QString::fromStdString(currentProjectName_), st.value);
|
||||
emit busyChanged(false);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::selectTm(const QString& tmObjectId) {
|
||||
if (tmObjectId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
currentTmId_ = tmObjectId.toStdString();
|
||||
const std::string pid = currentProjectId_;
|
||||
dataPageNo_ = 1;
|
||||
filePageNo_ = 1;
|
||||
const auto d = repo_.loadTmRows(pid, currentTmId_, 3, dataPageNo_);
|
||||
if (!d.ok) {
|
||||
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
|
||||
if (tmObjectId.isEmpty()) return;
|
||||
emit busyChanged(true);
|
||||
const auto ds = repo_.loadDatasetsOfTm(tmObjectId.toStdString());
|
||||
emit busyChanged(false);
|
||||
if (!ds.ok) {
|
||||
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(ds.error));
|
||||
return;
|
||||
}
|
||||
dataTotal_ = d.value.total;
|
||||
emit datasetsLoaded(tmObjectId, d.value.rows, d.value.total, false);
|
||||
const auto f = repo_.loadTmRows(pid, currentTmId_, 1, filePageNo_);
|
||||
if (!f.ok) {
|
||||
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
|
||||
return;
|
||||
}
|
||||
fileTotal_ = f.value.total;
|
||||
emit filesLoaded(tmObjectId, f.value.rows, f.value.total, false);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::loadMoreData() {
|
||||
if (currentTmId_.empty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto d = repo_.loadTmRows(currentProjectId_, currentTmId_, 3, ++dataPageNo_);
|
||||
if (!d.ok) {
|
||||
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
|
||||
return;
|
||||
}
|
||||
dataTotal_ = d.value.total;
|
||||
emit datasetsLoaded(QString::fromStdString(currentTmId_), d.value.rows, d.value.total, true);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::loadMoreFiles() {
|
||||
if (currentTmId_.empty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto f = repo_.loadTmRows(currentProjectId_, currentTmId_, 1, ++filePageNo_);
|
||||
if (!f.ok) {
|
||||
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
|
||||
return;
|
||||
}
|
||||
fileTotal_ = f.value.total;
|
||||
emit filesLoaded(QString::fromStdString(currentTmId_), f.value.rows, f.value.total, true);
|
||||
emit datasetsLoaded(tmObjectId, ds.value);
|
||||
}
|
||||
|
||||
} // namespace geopro::controller
|
||||
|
|
|
|||
|
|
@ -22,33 +22,21 @@ public slots:
|
|||
void switchWorkspace(const QString& tenantId);
|
||||
void switchProject(const QString& projectId);
|
||||
void selectTm(const QString& tmObjectId);
|
||||
void loadMoreData();
|
||||
void loadMoreFiles();
|
||||
|
||||
signals:
|
||||
void busyChanged(bool busy);
|
||||
void workspacesLoaded(const std::vector<geopro::data::Workspace>& list, const QString& currentId);
|
||||
void projectsLoaded(const std::vector<geopro::data::ProjectSummary>& list,
|
||||
const QString& currentId, int total);
|
||||
void projectsLoaded(const std::vector<geopro::data::ProjectSummary>& list, const QString& currentId);
|
||||
void structureLoaded(const QString& projectName, const std::vector<geopro::data::StructNode>& nodes);
|
||||
void datasetsLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
|
||||
int total, bool append);
|
||||
void filesLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
|
||||
int total, bool append);
|
||||
void datasetsLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsNode>& list);
|
||||
void loadFailed(const QString& stage, const QString& message);
|
||||
|
||||
private:
|
||||
void loadProjectsAndStructure(); // start + switchWorkspace 共用
|
||||
|
||||
data::IProjectRepository& repo_;
|
||||
bool busy_ = false;
|
||||
std::vector<data::ProjectSummary> lastProjects_;
|
||||
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
|
||||
std::string currentTmId_;
|
||||
int dataPageNo_ = 0;
|
||||
int filePageNo_ = 0;
|
||||
int dataTotal_ = 0;
|
||||
int fileTotal_ = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::controller
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "ApiClient.hpp"
|
||||
#include "dto/NavDto.hpp"
|
||||
|
|
@ -20,11 +19,6 @@ std::string errorOf(const net::ApiResponse& r, const char* fallback) {
|
|||
if (!r.rawError.isEmpty()) return r.rawError.toStdString();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 后端 id 进 URL 前做百分号编码(不可信外部数据:防 ? # & / 空格 破坏路径/查询)。
|
||||
QString enc(const std::string& s) {
|
||||
return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s)));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ApiProjectRepository::ApiProjectRepository(net::ApiClient& api) : api_(api) {}
|
||||
|
|
@ -38,57 +32,35 @@ RepoResult<std::vector<Workspace>> ApiProjectRepository::listWorkspaces() {
|
|||
|
||||
RepoResult<bool> ApiProjectRepository::switchWorkspace(const std::string& tenantId) {
|
||||
const QString path =
|
||||
QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId));
|
||||
QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(QString::fromStdString(tenantId));
|
||||
const net::ApiResponse r = api_.postJson(path, QJsonObject{});
|
||||
if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")};
|
||||
// 切换空间返回新 accessToken:必须重新注入,后续请求才落到新空间。
|
||||
const QString token = r.data.value(QStringLiteral("accessToken")).toString();
|
||||
if (!token.isEmpty()) api_.setToken(token);
|
||||
return {true, true, {}};
|
||||
}
|
||||
|
||||
RepoResult<ProjectListPage> ApiProjectRepository::pageProjects(const std::string& nameFilter,
|
||||
const std::string& typeId, int pageNo,
|
||||
int pageSize) {
|
||||
QJsonObject body{{QStringLiteral("projectName"), QString::fromStdString(nameFilter)},
|
||||
{QStringLiteral("pageNo"), pageNo},
|
||||
{QStringLiteral("pageSize"), pageSize}};
|
||||
if (!typeId.empty()) body[QStringLiteral("projectTypeId")] = QString::fromStdString(typeId);
|
||||
const net::ApiResponse r = api_.postJson(QStringLiteral("/business/my/profile/project/page"), body);
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "pageProjects failed")};
|
||||
return {true, dto::parseProjectPage(r.data), {}};
|
||||
}
|
||||
|
||||
RepoResult<std::vector<ProjectType>> ApiProjectRepository::listProjectTypes() {
|
||||
const net::ApiResponse r = api_.get(QStringLiteral("/business/project/type/list"));
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "listProjectTypes failed")};
|
||||
return {true, dto::parseProjectTypes(r.data.value(QStringLiteral("value")).toArray()), {}};
|
||||
RepoResult<std::vector<ProjectSummary>> ApiProjectRepository::listProjects(
|
||||
const std::string& lastProjectId) {
|
||||
const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1")
|
||||
.arg(QString::fromStdString(lastProjectId));
|
||||
const net::ApiResponse r = api_.get(path);
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")};
|
||||
return {true, dto::parseProjects(r.data).projects, {}};
|
||||
}
|
||||
|
||||
RepoResult<std::vector<StructNode>> ApiProjectRepository::loadStructure(const std::string& projectId) {
|
||||
// 项目结构(项目根 + GS + TM;不含 DS)。比 projectWorkbench 干净。
|
||||
const QString path =
|
||||
QStringLiteral("/business/projectStruct/queryProjectStruct/%1").arg(enc(projectId));
|
||||
const net::ApiResponse r = api_.get(path);
|
||||
const QJsonObject body{{QStringLiteral("projectId"), QString::fromStdString(projectId)}};
|
||||
const net::ApiResponse r =
|
||||
api_.postJson(QStringLiteral("/business/projectWorkbench/queryProjectStruct"), body);
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "loadStructure failed")};
|
||||
return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}};
|
||||
return {true, dto::parseStructNodes(r.data.value(QStringLiteral("projectStructList")).toArray()), {}};
|
||||
}
|
||||
|
||||
RepoResult<DsPage> ApiProjectRepository::loadTmRows(const std::string& projectId,
|
||||
const std::string& tmObjectId, int classifyType,
|
||||
int pageNo) {
|
||||
const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page")
|
||||
: QStringLiteral("/business/dsObject/data/page");
|
||||
const QJsonObject body{
|
||||
{QStringLiteral("projectId"), QString::fromStdString(projectId)},
|
||||
{QStringLiteral("structParentId"), QString::fromStdString(tmObjectId)},
|
||||
{QStringLiteral("structParentConfType"), 2},
|
||||
{QStringLiteral("classifyTypeList"), QJsonArray{classifyType}},
|
||||
{QStringLiteral("pageNo"), pageNo},
|
||||
{QStringLiteral("pageSize"), 5}}; // 数据/文件页签每页 5;不足 total 时"加载更多"追加
|
||||
const net::ApiResponse r = api_.postJson(path, body);
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "loadTmRows failed")};
|
||||
return {true, dto::parseDsPage(r.data), {}};
|
||||
RepoResult<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) {
|
||||
const QString path = QStringLiteral("/business/projectWorkbench/queryDsByTmObjectId/%1")
|
||||
.arg(QString::fromStdString(tmObjectId));
|
||||
const net::ApiResponse r = api_.get(path);
|
||||
if (!ok(r)) return {false, {}, errorOf(r, "loadDatasetsOfTm failed")};
|
||||
return {true, dto::parseDatasets(r.data.value(QStringLiteral("value")).toArray()), {}};
|
||||
}
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
|
|||
|
|
@ -12,12 +12,9 @@ public:
|
|||
|
||||
RepoResult<std::vector<Workspace>> listWorkspaces() override;
|
||||
RepoResult<bool> switchWorkspace(const std::string& tenantId) override;
|
||||
RepoResult<ProjectListPage> pageProjects(const std::string& nameFilter, const std::string& typeId,
|
||||
int pageNo, int pageSize) override;
|
||||
RepoResult<std::vector<ProjectType>> listProjectTypes() override;
|
||||
RepoResult<std::vector<ProjectSummary>> listProjects(const std::string& lastProjectId) override;
|
||||
RepoResult<std::vector<StructNode>> loadStructure(const std::string& projectId) override;
|
||||
RepoResult<DsPage> loadTmRows(const std::string& projectId, const std::string& tmObjectId,
|
||||
int classifyType, int pageNo) override;
|
||||
RepoResult<std::vector<DsNode>> loadDatasetsOfTm(const std::string& tmObjectId) override;
|
||||
|
||||
private:
|
||||
net::ApiClient& api_;
|
||||
|
|
|
|||
|
|
@ -11,22 +11,6 @@ namespace {
|
|||
std::string str(const QJsonObject& o, const char* key) {
|
||||
return o.value(QString::fromLatin1(key)).toString().toStdString();
|
||||
}
|
||||
|
||||
ProjectSummary parseProjectItem(const QJsonObject& o) {
|
||||
ProjectSummary p;
|
||||
p.id = str(o, "id");
|
||||
p.name = str(o, "projectName");
|
||||
p.typeName = str(o, "projectTypeName");
|
||||
p.crsCode = str(o, "referenceCRSCode");
|
||||
p.crsName = str(o, "referenceCRSName");
|
||||
p.code = str(o, "projectCode");
|
||||
p.projectTypeId = str(o, "projectTypeId");
|
||||
p.ownerCompany = str(o, "ownerCompanyName");
|
||||
p.responsiblePerson = str(o, "responsiblePersonName");
|
||||
p.createTime = str(o, "createTime");
|
||||
p.status = o.value(QStringLiteral("status")).toInt();
|
||||
return p;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<Workspace> parseWorkspaces(const QJsonArray& arr) {
|
||||
|
|
@ -49,32 +33,18 @@ ProjectPage parseProjects(const QJsonObject& data) {
|
|||
page.hasNextPage = data.value(QStringLiteral("hasNextPage")).toBool();
|
||||
const QJsonArray list = data.value(QStringLiteral("projectList")).toArray();
|
||||
page.projects.reserve(static_cast<size_t>(list.size()));
|
||||
for (const QJsonValue& v : list) page.projects.push_back(parseProjectItem(v.toObject()));
|
||||
return page;
|
||||
}
|
||||
|
||||
std::vector<ProjectSummary> parseProjectList(const QJsonArray& arr) {
|
||||
std::vector<ProjectSummary> out;
|
||||
out.reserve(static_cast<size_t>(arr.size()));
|
||||
for (const QJsonValue& v : arr) out.push_back(parseProjectItem(v.toObject()));
|
||||
return out;
|
||||
}
|
||||
|
||||
ProjectListPage parseProjectPage(const QJsonObject& data) {
|
||||
ProjectListPage p;
|
||||
p.rows = parseProjectList(data.value(QStringLiteral("list")).toArray());
|
||||
p.total = data.value(QStringLiteral("total")).toInt();
|
||||
return p;
|
||||
}
|
||||
|
||||
std::vector<ProjectType> parseProjectTypes(const QJsonArray& arr) {
|
||||
std::vector<ProjectType> out;
|
||||
out.reserve(static_cast<size_t>(arr.size()));
|
||||
for (const QJsonValue& v : arr) {
|
||||
for (const QJsonValue& v : list) {
|
||||
const QJsonObject o = v.toObject();
|
||||
out.push_back(ProjectType{str(o, "id"), str(o, "name")});
|
||||
ProjectSummary p;
|
||||
p.id = str(o, "id");
|
||||
p.name = str(o, "projectName");
|
||||
p.typeName = str(o, "projectTypeName");
|
||||
p.crsCode = str(o, "referenceCRSCode");
|
||||
p.crsName = str(o, "referenceCRSName");
|
||||
p.status = o.value(QStringLiteral("status")).toInt();
|
||||
page.projects.push_back(std::move(p));
|
||||
}
|
||||
return out;
|
||||
return page;
|
||||
}
|
||||
|
||||
std::vector<StructNode> parseStructNodes(const QJsonArray& arr) {
|
||||
|
|
@ -94,58 +64,47 @@ std::vector<StructNode> parseStructNodes(const QJsonArray& arr) {
|
|||
return out;
|
||||
}
|
||||
|
||||
std::vector<DsRow> parseDsRows(const QJsonArray& arr) {
|
||||
std::vector<DsRow> out;
|
||||
std::vector<DsNode> parseDatasets(const QJsonArray& arr) {
|
||||
std::vector<DsNode> out;
|
||||
out.reserve(static_cast<size_t>(arr.size()));
|
||||
for (const QJsonValue& v : arr) {
|
||||
const QJsonObject o = v.toObject();
|
||||
DsRow d;
|
||||
DsNode d;
|
||||
d.id = str(o, "id");
|
||||
d.dsName = str(o, "dsName");
|
||||
d.typeName = str(o, "name"); // 注意:name 字段=ds类型名
|
||||
d.ddCode = str(o, "ddCode");
|
||||
d.createTime = str(o, "createTime");
|
||||
const QJsonObject f = o.value(QStringLiteral("file")).toObject();
|
||||
d.fileName = str(f, "name");
|
||||
d.fileUrl = str(f, "url");
|
||||
d.fileSize = static_cast<long long>(f.value(QStringLiteral("size")).toDouble());
|
||||
d.name = str(o, "name");
|
||||
d.ddType = str(o, "ddCode");
|
||||
out.push_back(std::move(d));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
DsPage parseDsPage(const QJsonObject& data) {
|
||||
DsPage p;
|
||||
p.rows = parseDsRows(data.value(QStringLiteral("list")).toArray());
|
||||
p.total = data.value(QStringLiteral("total")).toInt();
|
||||
return p;
|
||||
}
|
||||
|
||||
std::vector<StructTreeNode> buildStructTree(const std::vector<StructNode>& flat) {
|
||||
// 过滤 DS(type==3):DS 不进对象树(按 TM 单独拉取到数据列表)。
|
||||
std::vector<StructNode> nodes;
|
||||
nodes.reserve(flat.size());
|
||||
for (const auto& n : flat)
|
||||
if (n.type != 3) nodes.push_back(n);
|
||||
|
||||
std::set<std::string> ids;
|
||||
for (const auto& n : nodes) ids.insert(n.id);
|
||||
// 根层:parentId 为空 / "0" / 不在集合内(孤儿)。
|
||||
std::set<std::string> hasChild;
|
||||
for (const auto& n : flat) {
|
||||
ids.insert(n.id);
|
||||
if (!n.parentId.empty()) hasChild.insert(n.parentId);
|
||||
}
|
||||
// 叶子(无子节点)= TM。
|
||||
auto isLeaf = [&](const std::string& id) { return hasChild.find(id) == hasChild.end(); };
|
||||
// 根层节点:parentId 为空或 parentId 不在 id 集合内(孤儿)。
|
||||
auto isRootLevel = [&](const StructNode& n) {
|
||||
return n.parentId.empty() || n.parentId == "0" || ids.find(n.parentId) == ids.end();
|
||||
return n.parentId.empty() || ids.find(n.parentId) == ids.end();
|
||||
};
|
||||
std::set<std::string> visited; // 防环:每个 id 最多进树一次。
|
||||
// visited 防环:每个 id 最多进树一次。对正常树(单父)等价于原逻辑;
|
||||
// 对不可信后端数据的多节点环 / 重复 id 环,避免无限递归(规约:永不信任外部数据)。
|
||||
std::set<std::string> visited;
|
||||
std::function<std::vector<StructTreeNode>(const std::string&, bool)> build =
|
||||
[&](const std::string& parentId, bool root) {
|
||||
std::vector<StructTreeNode> out;
|
||||
for (const auto& n : nodes) {
|
||||
for (const auto& n : flat) {
|
||||
const bool belongs = root ? isRootLevel(n) : (n.parentId == parentId);
|
||||
if (!belongs) continue;
|
||||
if (visited.count(n.id)) continue;
|
||||
if (visited.count(n.id)) continue; // 已进树 → 跳过,防环/防重复
|
||||
visited.insert(n.id);
|
||||
StructTreeNode t;
|
||||
t.node = n;
|
||||
t.isTm = (n.type == 2); // type: 1=项目根 2=TM(测线) 3=DS(已过滤)
|
||||
t.isTm = isLeaf(n.id);
|
||||
t.children = build(n.id, false);
|
||||
out.push_back(std::move(t));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,22 +13,11 @@ std::vector<Workspace> parseWorkspaces(const QJsonArray& arr);
|
|||
struct ProjectPage { std::vector<ProjectSummary> projects; bool hasNextPage = false; };
|
||||
ProjectPage parseProjects(const QJsonObject& data);
|
||||
|
||||
// my/profile/queryProject 的 data["value"] 数组 → 模型(与 parseProjects 同字段映射)。
|
||||
std::vector<ProjectSummary> parseProjectList(const QJsonArray& arr);
|
||||
|
||||
// my/profile/project/page 的整个 data 对象 → ProjectListPage(rows + total)。
|
||||
ProjectListPage parseProjectPage(const QJsonObject& data);
|
||||
// project/type/list 的 data["value"] 数组 → 项目类型列表。
|
||||
std::vector<ProjectType> parseProjectTypes(const QJsonArray& arr);
|
||||
|
||||
// 结构扁平节点数组(queryProjectStruct 的 data["projectStructList"])→ 模型。
|
||||
std::vector<StructNode> parseStructNodes(const QJsonArray& arr);
|
||||
|
||||
// data/page / file/page 的 data["list"] 数组 → DsRow(数据行无 file;文件行含 file{name,size,url})。
|
||||
std::vector<DsRow> parseDsRows(const QJsonArray& arr);
|
||||
|
||||
// data/page 或 file/page 的整个 data 对象 → DsPage(rows + total)。
|
||||
DsPage parseDsPage(const QJsonObject& data);
|
||||
// DS 聚合数组(queryDsByTmObjectId 的 data["value"])→ DsNode。ddCode → ddType。
|
||||
std::vector<DsNode> parseDatasets(const QJsonArray& arr);
|
||||
|
||||
// 扁平 StructNode 按 parentId 建树。叶子(无子节点)=TM。处理:项目直挂 TM、孤儿 parentId、空表。
|
||||
struct StructTreeNode {
|
||||
|
|
|
|||
|
|
@ -19,17 +19,9 @@ public:
|
|||
virtual ~IProjectRepository() = default;
|
||||
virtual RepoResult<std::vector<Workspace>> listWorkspaces() = 0;
|
||||
virtual RepoResult<bool> switchWorkspace(const std::string& tenantId) = 0;
|
||||
// 项目分页:nameFilter 名称模糊(可空)、typeId 类型过滤(空=不限)、pageNo 从 1 起。
|
||||
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<ProjectSummary>> listProjects(const std::string& lastProjectId) = 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;
|
||||
virtual RepoResult<std::vector<DsNode>> loadDatasetsOfTm(const std::string& tmObjectId) = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
|
|||
|
|
@ -3,14 +3,6 @@
|
|||
#include <vector>
|
||||
namespace geopro::data {
|
||||
struct DsNode { std::string id, name, ddType; };
|
||||
|
||||
// data/page 或 file/page 的一条 ds。数据行只用 dsName/typeName/ddCode;文件行另含 file*。
|
||||
struct DsRow {
|
||||
std::string id, dsName, typeName, ddCode, createTime;
|
||||
std::string fileName, fileUrl;
|
||||
long long fileSize = 0;
|
||||
};
|
||||
struct DsPage { std::vector<DsRow> rows; int total = 0; };
|
||||
struct TmNode { std::string id, name, confCode; std::vector<DsNode> dss; };
|
||||
struct GsNode { std::string id, name; std::vector<TmNode> tms; };
|
||||
struct Project { std::string id, name; std::vector<GsNode> gss; };
|
||||
|
|
@ -18,17 +10,8 @@ struct Project { std::string id, name; std::vector<GsNode> gss; };
|
|||
// 工作空间(=企业租户/空间)。ownerType: 1 个人空间 2 企业空间。
|
||||
struct Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; };
|
||||
|
||||
// 项目摘要(下拉/弹窗列表用)。crsCode 供下一轮替换硬编码 EPSG:4547。
|
||||
struct ProjectSummary {
|
||||
std::string id, name, typeName, crsCode, crsName;
|
||||
std::string code, projectTypeId, ownerCompany, responsiblePerson, createTime;
|
||||
int status = 0;
|
||||
};
|
||||
|
||||
// 项目类型(弹窗类型过滤下拉用)。
|
||||
struct ProjectType { std::string id, name; };
|
||||
// 项目分页结果。
|
||||
struct ProjectListPage { std::vector<ProjectSummary> rows; int total = 0; };
|
||||
// 项目摘要(列表用)。crsCode/crsName 为项目参考坐标系,下一轮替换硬编码 EPSG:4547。
|
||||
struct ProjectSummary { std::string id, name, typeName, crsCode, crsName; int status = 0; };
|
||||
|
||||
// 项目结构扁平节点(仅 GS / TM)。客户端按 parentId 建树,叶子=TM。
|
||||
struct StructNode { std::string id, name, parentId, typeName, confCode; int type = 0; };
|
||||
|
|
|
|||
|
|
@ -64,6 +64,17 @@ TEST(NavDto, ParseStructNodesMapsParentAndType) {
|
|||
EXPECT_EQ(ns[1].type, 2);
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseDatasetsMapsDdCodeToDdType) {
|
||||
const auto arr = arrOf(R"([
|
||||
{"id":"ds1","name":"批次1","ddCode":"dd_section","typeName":"剖面"}
|
||||
])");
|
||||
const auto ds = dto::parseDatasets(arr);
|
||||
ASSERT_EQ(ds.size(), 1u);
|
||||
EXPECT_EQ(ds[0].id, "ds1");
|
||||
EXPECT_EQ(ds[0].name, "批次1");
|
||||
EXPECT_EQ(ds[0].ddType, "dd_section");
|
||||
}
|
||||
|
||||
TEST(NavDto, BuildStructTreeNestsGsTmAndDirectTm) {
|
||||
const std::vector<StructNode> flat = {
|
||||
{"gs1", "工区1", "", "GS", "", 1},
|
||||
|
|
@ -108,96 +119,3 @@ TEST(NavDto, BuildStructTreeHandlesCycleWithoutInfiniteRecursion) {
|
|||
ASSERT_EQ(roots.size(), 1u);
|
||||
EXPECT_EQ(roots[0].node.id, "R");
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseProjectsEmptyAndMissingListGraceful) {
|
||||
EXPECT_TRUE(dto::parseProjects(objOf(R"({})")).projects.empty());
|
||||
EXPECT_FALSE(dto::parseProjects(objOf(R"({"hasNextPage":false})")).hasNextPage);
|
||||
const auto p = dto::parseProjects(objOf(R"({"projectList":[]})"));
|
||||
EXPECT_TRUE(p.projects.empty());
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseProjectListArrayMapsItem) {
|
||||
const auto arr = arrOf(R"([
|
||||
{"id":"p1","projectName":"演示","projectTypeName":"ERT","referenceCRSCode":"EPSG:4547","status":1}
|
||||
])");
|
||||
const auto v = dto::parseProjectList(arr);
|
||||
ASSERT_EQ(v.size(), 1u);
|
||||
EXPECT_EQ(v[0].id, "p1");
|
||||
EXPECT_EQ(v[0].name, "演示");
|
||||
EXPECT_EQ(v[0].crsCode, "EPSG:4547");
|
||||
}
|
||||
|
||||
TEST(NavDto, BuildStructTreeDropsDsAndTmStaysLeaf) {
|
||||
// 真实形态:项目(1) → TM(2) → DS(3)。DS 不进树;带 DS 子节点的 TM 仍是 TM 叶子。
|
||||
const std::vector<StructNode> flat = {
|
||||
{"P", "项目", "0", "PRJ", "", 1},
|
||||
{"T1", "ERT1", "P", "ERT", "ERT", 2},
|
||||
{"D1", "批次1","T1", "", "", 3}, // DS:应被过滤
|
||||
{"T2", "ERT2", "P", "ERT", "ERT", 2},
|
||||
};
|
||||
const auto roots = dto::buildStructTree(flat);
|
||||
ASSERT_EQ(roots.size(), 1u); // 仅项目根(parentId "0")
|
||||
EXPECT_EQ(roots[0].node.id, "P");
|
||||
EXPECT_FALSE(roots[0].isTm); // 项目根 type1
|
||||
ASSERT_EQ(roots[0].children.size(), 2u); // T1、T2(D1 被过滤)
|
||||
EXPECT_EQ(roots[0].children[0].node.id, "T1");
|
||||
EXPECT_TRUE(roots[0].children[0].isTm); // TM type2
|
||||
EXPECT_TRUE(roots[0].children[0].children.empty()); // DS 不进树
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseDsRowsDataAndFile) {
|
||||
const auto d = dto::parseDsRows(arrOf(R"([
|
||||
{"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data","createTime":"2026-03-25 16:48:57"}
|
||||
])"));
|
||||
ASSERT_EQ(d.size(), 1u);
|
||||
EXPECT_EQ(d[0].id, "d1");
|
||||
EXPECT_EQ(d[0].dsName, "ERT1-WS");
|
||||
EXPECT_EQ(d[0].typeName, "电阻率数据");
|
||||
EXPECT_EQ(d[0].createTime, "2026-03-25 16:48:57");
|
||||
EXPECT_TRUE(d[0].fileName.empty());
|
||||
|
||||
const auto f = dto::parseDsRows(arrOf(R"([
|
||||
{"id":"f1","dsName":"ERT1-WS.xlsx","name":"","ddCode":"dd_file",
|
||||
"file":{"name":"ERT1-WS.xlsx","size":62760,"url":"/common/file/x.xlsx"}}
|
||||
])"));
|
||||
ASSERT_EQ(f.size(), 1u);
|
||||
EXPECT_EQ(f[0].fileName, "ERT1-WS.xlsx");
|
||||
EXPECT_EQ(f[0].fileSize, 62760);
|
||||
EXPECT_EQ(f[0].fileUrl, "/common/file/x.xlsx");
|
||||
|
||||
const auto page = dto::parseDsPage(objOf(R"({
|
||||
"total": 18,
|
||||
"list": [{"id":"x","dsName":"a","name":"t","ddCode":"dd","createTime":"2026-01-01 00:00:00"}]
|
||||
})"));
|
||||
EXPECT_EQ(page.total, 18);
|
||||
ASSERT_EQ(page.rows.size(), 1u);
|
||||
EXPECT_EQ(page.rows[0].dsName, "a");
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseProjectItemFullFields) {
|
||||
const auto v = dto::parseProjectList(arrOf(R"([
|
||||
{"id":"p1","projectName":"演示","projectCode":"001","status":2,
|
||||
"projectTypeId":"t1","projectTypeName":"全量类型",
|
||||
"ownerCompanyName":"华南所","responsiblePersonName":"张三","createTime":"2026-01-01 00:00:00"}
|
||||
])"));
|
||||
ASSERT_EQ(v.size(), 1u);
|
||||
EXPECT_EQ(v[0].code, "001");
|
||||
EXPECT_EQ(v[0].status, 2);
|
||||
EXPECT_EQ(v[0].projectTypeId, "t1");
|
||||
EXPECT_EQ(v[0].typeName, "全量类型");
|
||||
EXPECT_EQ(v[0].ownerCompany, "华南所");
|
||||
EXPECT_EQ(v[0].responsiblePerson, "张三");
|
||||
EXPECT_EQ(v[0].createTime, "2026-01-01 00:00:00");
|
||||
}
|
||||
|
||||
TEST(NavDto, ParseProjectPageAndTypes) {
|
||||
const auto page =
|
||||
dto::parseProjectPage(objOf(R"({"total":20,"list":[{"id":"p1","projectName":"a"}]})"));
|
||||
EXPECT_EQ(page.total, 20);
|
||||
ASSERT_EQ(page.rows.size(), 1u);
|
||||
EXPECT_EQ(page.rows[0].name, "a");
|
||||
const auto types = dto::parseProjectTypes(arrOf(R"([{"id":"t1","name":"全量类型"}])"));
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types[0].id, "t1");
|
||||
EXPECT_EQ(types[0].name, "全量类型");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue