docs(spec): 补齐项目列表弹窗/ds加载更多分页/创建时间显示/分页条数(10·5)/状态映射(1未开始2进行中)

This commit is contained in:
gaozheng 2026-06-09 18:45:25 +08:00
parent a37596f0d3
commit 475af464d9
1 changed files with 57 additions and 25 deletions

View File

@ -24,8 +24,11 @@
**做In Scope** **做In Scope**
- 工作空间列表 / 切换(真实接口)。 - 工作空间列表 / 切换(真实接口)。
- 项目列表 / 切换(真实接口)。 - 项目列表 / 切换(真实接口):下拉显示首页项目(首页 10项目数超过首页 → 下拉底部「全部项目…」打开
**项目列表弹窗**(名称/类型过滤 + 分页 + 8 列表格:序号/名称/编号/状态/类型/业主/负责人/创建时间;点项目名切换并关弹窗)。
- 对象树:**按真实结构显示 GS 层**(项目根 → GS → TMTM 在左下"数据真实显示栏"列出其 DS。 - 对象树:**按真实结构显示 GS 层**(项目根 → GS → TMTM 在左下"数据真实显示栏"列出其 DS。
- DS **数据/文件两个页签**接真实分页接口(每页 5每行显示"名称 / 创建时间 · 类型(数据)或大小(文件)"
超过首页 → 列表末尾「加载更多」追加下一页。
- 真实接口失败(断网 / token 过期 / 无数据)→ **显示错误 / 空状态****不回退本地样本**。 - 真实接口失败(断网 / token 过期 / 无数据)→ **显示错误 / 空状态****不回退本地样本**。
- 项目 `referenceCRSCode` 存入导航状态,供下一轮替换硬编码 `EPSG:4547`(本轮不改渲染)。 - 项目 `referenceCRSCode` 存入导航状态,供下一轮替换硬编码 `EPSG:4547`(本轮不改渲染)。
@ -34,7 +37,7 @@
→ 点击真实 DS 时中央/详情显示**占位"待接入"**`render/*` 与 `LocalSampleRepository` 代码**保留不删**。 → 点击真实 DS 时中央/详情显示**占位"待接入"**`render/*` 与 `LocalSampleRepository` 代码**保留不删**。
- 异步仓储QFuture/回调)—— 本轮同步阻塞 + WaitCursor与登录一致异步留 M1.5。 - 异步仓储QFuture/回调)—— 本轮同步阻塞 + WaitCursor与登录一致异步留 M1.5。
- 用户头像 / 姓名接真实 `auth/getUserInfo`(本轮先留静态)。 - 用户头像 / 姓名接真实 `auth/getUserInfo`(本轮先留静态)。
- 项目分页"加载更多"的滚动 UI接口支持 `hasNextPage`,本轮先取首页;翻页留待后续)。 - 文件下载(文件页签已展示文件名/大小、下载 `url` 已存入列表项备用,实际下载动作留后续)。
## 3. 接口确认结论 ## 3. 接口确认结论
@ -45,10 +48,11 @@ token 已由登录注入(`geomativeauthorization` 头),下列接口直接
|---|---|---|---| |---|---|---|---|
| 工作空间列表 | GET | `/business/system/tenant/enterprise/joined/list` | `[{id, name, ownerType(1个人/2企业), isCurTenant(0/1), logoPath}]` | | 工作空间列表 | 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/system/tenant/enterprise/switch/{tenantId}` | 信封 code/msg |
| 项目列表 | GET | `/business/project/queryByUser?lastProjectId=` | `{hasNextPage, projectList:[{id, projectName, projectTypeName, referenceCRSCode, referenceCRSName, status, ...}]}`(游标分页,首页传空 lastProjectId | | 项目分页 | 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` 返回空或不带分页,弃用) |
| 项目结构 | POST | `/business/projectWorkbench/queryProjectStruct` | body `{projectId}`data `{projectStructList:[{id, name, parentId, type, typeId, typeName, confCode}]}` | | 项目类型列表 | GET | `/business/project/type/list` | `data.value:[{id, name}]`(弹窗"项目类型"过滤下拉) |
| TM 下数据(页签) | POST | `/business/dsObject/data/page` | body `{projectId, structParentId:<tmObjectId>, structParentConfType:2, classifyTypeList:[3], pageNo, pageSize}``data.list[{id, dsName, name(类型名), ddCode}]` | | 项目结构 | GET | `/business/projectStruct/queryProjectStruct/{projectId}` | `data.value:[{id, name, parentId, type(1项目/2TM), typeId, typeName, confCode}]`(仅项目根+TM不含 DS |
| TM 下文件(页签) | POST | `/business/dsObject/file/page` | body 同上但 `classifyTypeList:[1]`;项另含 `file{name, size, url}` | | 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`**。 **层级确认(修正需求方假设)**:真实结构**不是** `项目→tm→ds`,而是 **`项目 → GS(工区) → TM(测线) → DS`**。
- `queryProjectStruct` 返回一个**扁平 parent-child 列表**(仅含 GS + TM 两类节点,**不含 DS**),客户端按 `parentId` 自建树。 - `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 Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; };
struct ProjectSummary { struct ProjectSummary {
std::string id, name, typeName, crsCode, crsName; 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<ProjectSummary> rows; int total = 0; }; // 项目分页结果
// 项目结构扁平节点GS / TM客户端按 parentId 建树。 // 项目结构扁平节点GS / TM客户端按 parentId 建树。
struct StructNode { struct StructNode {
std::string id, name, parentId, typeName, confCode; std::string id, name, parentId, typeName, confCode;
int type = 0; 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` ### 5.3 数据访问层 `data`
@ -133,11 +141,15 @@ public:
virtual ~IProjectRepository() = default; virtual ~IProjectRepository() = default;
virtual RepoResult<std::vector<Workspace>> listWorkspaces() = 0; virtual RepoResult<std::vector<Workspace>> listWorkspaces() = 0;
virtual RepoResult<bool> switchWorkspace(const std::string& tenantId) = 0; virtual RepoResult<bool> switchWorkspace(const std::string& tenantId) = 0;
virtual RepoResult<std::vector<ProjectSummary>> listProjects(const std::string& lastProjectId) = 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; virtual RepoResult<std::vector<StructNode>> loadStructure(const std::string& projectId) = 0;
virtual RepoResult<std::vector<DsRow>> loadTmRows(const std::string& projectId, // 按 TM 分页拉数据/文件行classifyType 3=数据 1=文件pageNo 从 1 起pageSize 固定 5。
const std::string& tmObjectId, virtual RepoResult<DsPage> loadTmRows(const std::string& projectId, const std::string& tmObjectId,
int classifyType) = 0; // 3=数据 1=文件 int classifyType, int pageNo) = 0;
}; };
``` ```
@ -148,9 +160,10 @@ public:
**`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测** **`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测**
- `parseWorkspaces(QJsonArray) -> vector<Workspace>``isCurTenant==1 → isCurrent`)。 - `parseWorkspaces(QJsonArray) -> vector<Workspace>``isCurTenant==1 → isCurrent`)。
- `parseProjects(QJsonObject) -> {vector<ProjectSummary>, bool hasNextPage}` - `parseProjectList(QJsonArray) -> vector<ProjectSummary>` / `parseProjectPage(QJsonObject) -> ProjectListPage{rows,total}`project/page
- `parseProjectTypes(QJsonArray) -> vector<ProjectType>`type/list
- `parseStructNodes(QJsonArray) -> vector<StructNode>` - `parseStructNodes(QJsonArray) -> vector<StructNode>`
- `parseDsRows(QJsonArray) -> vector<DsRow>`data/file page 的 `data.list``name→typeName``file{name,size,url}`)。 - `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` 刚性模型, - `buildStructTree(vector<StructNode>) -> vector<StructTreeNode>`:扁平→**通用树**(不强塞 `Project/Gs/Tm` 刚性模型,
以适配任意层级 + TM 直挂项目)。`StructTreeNode{StructNode node; bool isTm; vector<StructTreeNode> children}`。 以适配任意层级 + TM 直挂项目)。`StructTreeNode{StructNode node; bool isTm; vector<StructTreeNode> children}`。
- 以 `parentId` 归并;`parentId` 为空或不在集合内(孤儿)的节点为根层。 - 以 `parentId` 归并;`parentId` 为空或不在集合内(孤儿)的节点为根层。
@ -170,14 +183,17 @@ public:
public slots: public slots:
void switchWorkspace(const QString& tenantId); // 切空间→重载项目→重载结构 void switchWorkspace(const QString& tenantId); // 切空间→重载项目→重载结构
void switchProject(const QString& projectId); // 切项目→重载结构(清 DS/详情) 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: signals:
void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId); void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId);
void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId); void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId, int total); // total 判断"全部项目"入口
// 发出项目名 + 扁平结构节点建树buildStructTree在 ObjectTreePanel 内完成。 // 发出项目名 + 扁平结构节点建树buildStructTree在 ObjectTreePanel 内完成。
void structureLoaded(const QString& projectName, const std::vector<data::StructNode>&); void structureLoaded(const QString& projectName, const std::vector<data::StructNode>&);
void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsRow>&); // 数据页签 // total=总数、append=是否追加(加载更多 true / 首页 false
void filesLoaded(const QString& tmObjectId, const std::vector<data::DsRow>&); // 文件页签 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 loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态
void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor
private: private:
@ -185,10 +201,13 @@ private:
data::IProjectRepository& repo_; data::IProjectRepository& repo_;
std::vector<data::ProjectSummary> lastProjects_; // 供 switchProject 查 name/crsCode std::vector<data::ProjectSummary> lastProjects_; // 供 switchProject 查 name/crsCode
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
std::string currentTmId_; // 加载更多用:当前选中 TM
int dataPageNo_ = 0, filePageNo_ = 0, dataTotal_ = 0, fileTotal_ = 0; // 数据/文件分页游标
bool busy_ = false; // 重入保护:同步请求期间拒绝再次进入 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)` 并停在该阶段。 切空间/项目按 §6 时序。每个阶段失败 emit `loadFailed(stage,msg)` 并停在该阶段。
**重入保护**:每个公共操作入口 `if (busy_) return;`,并用 RAII guard 在置忙/复位时配平 `busyChanged` **重入保护**:每个公共操作入口 `if (busy_) return;`,并用 RAII guard 在置忙/复位时配平 `busyChanged`
(同步 HTTP 会泵 Qt 事件循环,快速二次点击可能重入并污染状态)。 (同步 HTTP 会泵 Qt 事件循环,快速二次点击可能重入并污染状态)。
@ -196,17 +215,25 @@ private:
### 5.5 UI 层 `app`(被动视图,数据驱动) ### 5.5 UI 层 `app`(被动视图,数据驱动)
**`app/TopBar`** —— 由"自由函数返回静态 QWidget"升级为**数据驱动类**QWidget 子类): **`app/TopBar`** —— 由"自由函数返回静态 QWidget"升级为**数据驱动类**QWidget 子类):
- `setWorkspaces(list, currentId)` / `setProjects(list, currentId)` 重建下拉项。 - `setWorkspaces(list, currentId)` / `setProjects(list, currentId, hasMore)` 重建下拉项;`hasMore` 时下拉底部加「全部项目…」。
- 信号 `workspaceSwitchRequested(QString id)` / `projectSwitchRequested(QString id)` - `setProjectButtonText(name)` —— 弹窗切换项目后更新项目按钮文字。
- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。 - 信号 `workspaceSwitchRequested(id)` / `projectSwitchRequested(id)` / `allProjectsRequested()`(打开项目弹窗)。
- `buildMenuBar` 不变(静态菜单本轮不接)。 - 工作空间/项目下拉用互斥 `QActionGroup`(避免"多选"),选中即更新按钮文字。
- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。`buildMenuBar` 不变。
**`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector<StructNode>)` 内部调 **`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector<StructNode>)` 内部调
`dto::buildStructTree` 重建 `QTreeWidget`项目根→GS→TM叶子=TM 可勾选、`UserRole` 存 tmObjectId `dto::buildStructTree` 重建 `QTreeWidget`项目根→GS→TM叶子=TM 可勾选、`UserRole` 存 tmObjectId
`showMessage(msg)` 显示空/错占位。信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)` `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 点击 → 详情面板与中央视图显示占位文案 **中央/详情**:移除"启动自动渲染本地 demo"DS 点击 → 详情面板与中央视图显示占位文案
"该数据集渲染将在下一阶段接入 dd 接口"。渲染代码保留。 "该数据集渲染将在下一阶段接入 dd 接口"。渲染代码保留。
@ -228,8 +255,12 @@ private:
切项目: TopBar.projectSwitchRequested(id) 切项目: TopBar.projectSwitchRequested(id)
→ controller.switchProject: loadStructure(id) → emit structureLoaded清空 DS 列表/详情占位 → controller.switchProject: loadStructure(id) → emit structureLoaded清空 DS 列表/详情占位
全部项目: TopBar.allProjectsRequested → main 打开 ProjectListDialogpageProjects 分页 + 名称/类型过滤)
→ 点项目名 → projectChosen(id,name) → nav.switchProject(id) + TopBar.setProjectButtonText(name) → 关弹窗
选 TM: ObjectTreePanel.tmClicked(tmObjectId) 选 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 → 中央/详情显示占位"待接入"(本轮不渲染真实数据) 点 DS: DatasetListPanel → 中央/详情显示占位"待接入"(本轮不渲染真实数据)
``` ```
@ -306,6 +337,7 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
- `src/controller/WorkbenchNavController.{hpp,cpp}` - `src/controller/WorkbenchNavController.{hpp,cpp}`
- `src/app/panels/ObjectTreePanel.{hpp,cpp}`(若不抽,则树构建函数留在 main但 TopBar 必抽) - `src/app/panels/ObjectTreePanel.{hpp,cpp}`(若不抽,则树构建函数留在 main但 TopBar 必抽)
- `src/app/CentralScene.{hpp,cpp}`(中央三维编排的数据驱动 helper见 §8.1 - `src/app/CentralScene.{hpp,cpp}`(中央三维编排的数据驱动 helper见 §8.1
- `src/app/ProjectListDialog.{hpp,cpp}`(项目列表弹窗,见 §5.5
- 测试:`tests/data/test_nav_dto.cpp`NavDto 映射 + buildStructTree - 测试:`tests/data/test_nav_dto.cpp`NavDto 映射 + buildStructTree
**改造** **改造**