From 475af464d9029edca0cef819b35428f8b9bc4353 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 18:45:25 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20=E8=A1=A5=E9=BD=90=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=88=97=E8=A1=A8=E5=BC=B9=E7=AA=97/ds=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=9B=B4=E5=A4=9A=E5=88=86=E9=A1=B5/=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA/=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E6=9D=A1=E6=95=B0(10=C2=B75)/=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=98=A0=E5=B0=84(1=E6=9C=AA=E5=BC=80=E5=A7=8B2=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-09-real-api-navigation-design.md | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/docs/superpowers/specs/2026-06-09-real-api-navigation-design.md b/docs/superpowers/specs/2026-06-09-real-api-navigation-design.md index 0af9ee9..4992013 100644 --- a/docs/superpowers/specs/2026-06-09-real-api-navigation-design.md +++ b/docs/superpowers/specs/2026-06-09-real-api-navigation-design.md @@ -24,8 +24,11 @@ **做(In Scope)** - 工作空间列表 / 切换(真实接口)。 -- 项目列表 / 切换(真实接口)。 +- 项目列表 / 切换(真实接口):下拉显示首页项目(首页 10);项目数超过首页 → 下拉底部「全部项目…」打开 + **项目列表弹窗**(名称/类型过滤 + 分页 + 8 列表格:序号/名称/编号/状态/类型/业主/负责人/创建时间;点项目名切换并关弹窗)。 - 对象树:**按真实结构显示 GS 层**(项目根 → GS → TM);TM 在左下"数据真实显示栏"列出其 DS。 +- DS **数据/文件两个页签**接真实分页接口(每页 5);每行显示"名称 / 创建时间 · 类型(数据)或大小(文件)"; + 超过首页 → 列表末尾「加载更多」追加下一页。 - 真实接口失败(断网 / token 过期 / 无数据)→ **显示错误 / 空状态**,**不回退本地样本**。 - 项目 `referenceCRSCode` 存入导航状态,供下一轮替换硬编码 `EPSG:4547`(本轮不改渲染)。 @@ -34,7 +37,7 @@ → 点击真实 DS 时中央/详情显示**占位"待接入"**;`render/*` 与 `LocalSampleRepository` 代码**保留不删**。 - 异步仓储(QFuture/回调)—— 本轮同步阻塞 + WaitCursor(与登录一致),异步留 M1.5。 - 用户头像 / 姓名接真实 `auth/getUserInfo`(本轮先留静态)。 -- 项目分页"加载更多"的滚动 UI(接口支持 `hasNextPage`,本轮先取首页;翻页留待后续)。 +- 文件下载(文件页签已展示文件名/大小、下载 `url` 已存入列表项备用,实际下载动作留后续)。 ## 3. 接口确认结论 @@ -45,10 +48,11 @@ 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 | -| 项目列表 | 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 下数据(页签) | POST | `/business/dsObject/data/page` | body `{projectId, structParentId:, structParentConfType:2, classifyTypeList:[3], pageNo, pageSize}`;`data.list[{id, dsName, name(类型名), ddCode}]` | -| TM 下文件(页签) | POST | `/business/dsObject/file/page` | body 同上但 `classifyTypeList:[1]`;项另含 `file{name, size, url}` | +| 项目分页 | 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:, 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` 自建树。 @@ -109,15 +113,19 @@ token 已由登录注入(`geomativeauthorization` 头),下列接口直接 struct Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; }; struct ProjectSummary { std::string id, name, typeName, crsCode, crsName; - int status = 0; + std::string code, projectTypeId, ownerCompany, responsiblePerson, createTime; // 弹窗 8 列用 + int status = 0; // 1=未开始 2=进行中(其余显示数字) }; +struct ProjectType { std::string id, name; }; // 类型过滤下拉 +struct ProjectListPage { std::vector rows; int total = 0; }; // 项目分页结果 // 项目结构扁平节点(GS / TM);客户端按 parentId 建树。 struct StructNode { std::string id, name, parentId, typeName, confCode; int type = 0; }; ``` -新增 `DsRow{id, dsName, typeName, ddCode, fileName, fileUrl, fileSize}`(数据/文件页签行通用;文件行含 file*)。`DsNode` 仅本地样本仓储继续用。 +新增 `DsRow{id, dsName, typeName, ddCode, createTime, fileName, fileUrl, fileSize}`(数据/文件页签行通用;文件行含 file*) ++ `DsPage{rows, total}`(分页结果)。`DsNode` 仅本地样本仓储继续用。 ### 5.3 数据访问层 `data` @@ -133,11 +141,15 @@ public: virtual ~IProjectRepository() = default; virtual RepoResult> listWorkspaces() = 0; virtual RepoResult switchWorkspace(const std::string& tenantId) = 0; - virtual RepoResult> listProjects(const std::string& lastProjectId) = 0; + // 项目分页(名称/类型过滤)+ 项目类型列表(弹窗用)。 + virtual RepoResult pageProjects(const std::string& nameFilter, + const std::string& typeId, int pageNo, + int pageSize) = 0; + virtual RepoResult> listProjectTypes() = 0; virtual RepoResult> loadStructure(const std::string& projectId) = 0; - virtual RepoResult> loadTmRows(const std::string& projectId, - const std::string& tmObjectId, - int classifyType) = 0; // 3=数据 1=文件 + // 按 TM 分页拉数据/文件行:classifyType 3=数据 1=文件;pageNo 从 1 起,pageSize 固定 5。 + virtual RepoResult loadTmRows(const std::string& projectId, const std::string& tmObjectId, + int classifyType, int pageNo) = 0; }; ``` @@ -148,9 +160,10 @@ public: **`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测**): - `parseWorkspaces(QJsonArray) -> vector`(`isCurTenant==1 → isCurrent`)。 -- `parseProjects(QJsonObject) -> {vector, bool hasNextPage}`。 +- `parseProjectList(QJsonArray) -> vector` / `parseProjectPage(QJsonObject) -> ProjectListPage{rows,total}`(project/page)。 +- `parseProjectTypes(QJsonArray) -> vector`(type/list)。 - `parseStructNodes(QJsonArray) -> vector`。 -- `parseDsRows(QJsonArray) -> vector`(data/file page 的 `data.list`;`name→typeName`,`file{name,size,url}`)。 +- `parseDsRows(QJsonArray) -> vector` / `parseDsPage(QJsonObject) -> DsPage{rows,total}`(data/file page;`name→typeName`、`createTime`、`file{name,size,url}`)。 - `buildStructTree(vector) -> vector`:扁平→**通用树**(不强塞 `Project/Gs/Tm` 刚性模型, 以适配任意层级 + TM 直挂项目)。`StructTreeNode{StructNode node; bool isTm; vector children}`。 - 以 `parentId` 归并;`parentId` 为空或不在集合内(孤儿)的节点为根层。 @@ -170,14 +183,17 @@ public: public slots: void switchWorkspace(const QString& tenantId); // 切空间→重载项目→重载结构 void switchProject(const QString& projectId); // 切项目→重载结构(清 DS/详情) - void selectTm(const QString& tmObjectId); // 选 TM→拉其 DS + void selectTm(const QString& tmObjectId); // 选 TM→拉其 DS 首页(数据+文件) + void loadMoreData(); // 数据页签"加载更多"→下一页追加 + void loadMoreFiles(); // 文件页签"加载更多"→下一页追加 signals: void workspacesLoaded(const std::vector&, QString currentId); - void projectsLoaded(const std::vector&, QString currentId); + void projectsLoaded(const std::vector&, QString currentId, int total); // total 判断"全部项目"入口 // 发出项目名 + 扁平结构节点;建树(buildStructTree)在 ObjectTreePanel 内完成。 void structureLoaded(const QString& projectName, const std::vector&); - void datasetsLoaded(const QString& tmObjectId, const std::vector&); // 数据页签 - void filesLoaded(const QString& tmObjectId, const std::vector&); // 文件页签 + // total=总数、append=是否追加(加载更多 true / 首页 false)。 + void datasetsLoaded(const QString& tmObjectId, const std::vector&, int total, bool append); + void filesLoaded(const QString& tmObjectId, const std::vector&, int total, bool append); void loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态 void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor private: @@ -185,10 +201,13 @@ private: data::IProjectRepository& repo_; std::vector 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/首个)→ `listProjects`(选首个)→ `loadStructure`→发扁平节点。 +编排逻辑:`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 事件循环,快速二次点击可能重入并污染状态)。 @@ -196,17 +215,25 @@ private: ### 5.5 UI 层 `app`(被动视图,数据驱动) **`app/TopBar`** —— 由"自由函数返回静态 QWidget"升级为**数据驱动类**(QWidget 子类): -- `setWorkspaces(list, currentId)` / `setProjects(list, currentId)` 重建下拉项。 -- 信号 `workspaceSwitchRequested(QString id)` / `projectSwitchRequested(QString id)`。 -- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。 -- `buildMenuBar` 不变(静态菜单本轮不接)。 +- `setWorkspaces(list, currentId)` / `setProjects(list, currentId, hasMore)` 重建下拉项;`hasMore` 时下拉底部加「全部项目…」。 +- `setProjectButtonText(name)` —— 弹窗切换项目后更新项目按钮文字。 +- 信号 `workspaceSwitchRequested(id)` / `projectSwitchRequested(id)` / `allProjectsRequested()`(打开项目弹窗)。 +- 工作空间/项目下拉用互斥 `QActionGroup`(避免"多选"),选中即更新按钮文字。 +- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。`buildMenuBar` 不变。 **`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector)` 内部调 `dto::buildStructTree` 重建 `QTreeWidget`(项目根→GS→TM,叶子=TM 可勾选、`UserRole` 存 tmObjectId); `showMessage(msg)` 显示空/错占位。信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)` (后者为前瞻钩子,本轮无消费者)。 -**`app/panels/DatasetListPanel`** —— `datasetsLoaded`→`populateDatasetList`(数据:dsName+类型名);`filesLoaded`→`populateFileList`(文件:文件名+可读大小,url 存角色备下载);空时占位。列表去隔行变色,改细分割线。 +**`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 接口"。渲染代码保留。 @@ -228,8 +255,12 @@ 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)+loadTmRows(pid,tm,1) → emit datasetsLoaded + filesLoaded → 数据/文件页签 + → 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 → 中央/详情显示占位"待接入"(本轮不渲染真实数据) ``` @@ -306,6 +337,7 @@ 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) **改造**