From 601706d1207809e353901eca4108c1fb6a8452a0 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 12:15:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=A3=E7=A0=81=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E6=95=B4=E6=94=B9=EF=BC=88=E6=8E=A7=E5=88=B6=E5=99=A8=E9=98=B2?= =?UTF-8?q?=E9=87=8D=E5=85=A5=20+=20URL=20=E7=99=BE=E5=88=86=E5=8F=B7?= =?UTF-8?q?=E7=BC=96=E7=A0=81=20+=20=E6=B5=8B=E8=AF=95/=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 1 + src/app/panels/ObjectTreePanel.hpp | 1 + src/controller/WorkbenchNavController.cpp | 39 ++++++++++++++--------- src/controller/WorkbenchNavController.hpp | 1 + src/data/api/ApiProjectRepository.cpp | 13 ++++++-- tests/data/test_nav_dto.cpp | 7 ++++ 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 0e8156a..8884de5 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -498,6 +498,7 @@ 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, diff --git a/src/app/panels/ObjectTreePanel.hpp b/src/app/panels/ObjectTreePanel.hpp index 58d54a8..3621386 100644 --- a/src/app/panels/ObjectTreePanel.hpp +++ b/src/app/panels/ObjectTreePanel.hpp @@ -20,6 +20,7 @@ public: signals: void tmClicked(const QString& tmObjectId); + // 前瞻钩子:勾选驱动中央渲染留待下一轮接真实 DS(本轮暂无消费者)。 void tmCheckToggled(const QString& tmObjectId, bool checked); private: diff --git a/src/controller/WorkbenchNavController.cpp b/src/controller/WorkbenchNavController.cpp index 4783d07..6e42046 100644 --- a/src/controller/WorkbenchNavController.cpp +++ b/src/controller/WorkbenchNavController.cpp @@ -8,11 +8,27 @@ 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() { - emit busyChanged(true); + if (busy_) return; + BusyGuard guard(this, &busy_); const auto ws = repo_.listWorkspaces(); if (!ws.ok) { - emit busyChanged(false); emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error)); return; } @@ -22,9 +38,7 @@ 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() { @@ -61,22 +75,20 @@ void WorkbenchNavController::loadProjectsAndStructure() { } void WorkbenchNavController::switchWorkspace(const QString& tenantId) { - if (tenantId.isEmpty()) return; - emit busyChanged(true); + if (tenantId.isEmpty() || busy_) return; + BusyGuard guard(this, &busy_); 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()) return; - emit busyChanged(true); + if (projectId.isEmpty() || busy_) return; + BusyGuard guard(this, &busy_); currentProjectId_ = projectId.toStdString(); for (const auto& p : lastProjects_) if (p.id == currentProjectId_) { @@ -85,19 +97,16 @@ 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()) return; - emit busyChanged(true); + if (tmObjectId.isEmpty() || busy_) return; + BusyGuard guard(this, &busy_); const auto ds = repo_.loadDatasetsOfTm(tmObjectId.toStdString()); - emit busyChanged(false); if (!ds.ok) { emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(ds.error)); return; diff --git a/src/controller/WorkbenchNavController.hpp b/src/controller/WorkbenchNavController.hpp index c96d7d1..dc5c08e 100644 --- a/src/controller/WorkbenchNavController.hpp +++ b/src/controller/WorkbenchNavController.hpp @@ -35,6 +35,7 @@ private: void loadProjectsAndStructure(); // start + switchWorkspace 共用 data::IProjectRepository& repo_; + bool busy_ = false; std::vector lastProjects_; std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; }; diff --git a/src/data/api/ApiProjectRepository.cpp b/src/data/api/ApiProjectRepository.cpp index 39f383d..0c9fdd8 100644 --- a/src/data/api/ApiProjectRepository.cpp +++ b/src/data/api/ApiProjectRepository.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "ApiClient.hpp" #include "dto/NavDto.hpp" @@ -19,6 +20,11 @@ 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) {} @@ -32,7 +38,7 @@ RepoResult> ApiProjectRepository::listWorkspaces() { RepoResult ApiProjectRepository::switchWorkspace(const std::string& tenantId) { const QString path = - QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(QString::fromStdString(tenantId)); + QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId)); const net::ApiResponse r = api_.postJson(path, QJsonObject{}); if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")}; return {true, true, {}}; @@ -41,7 +47,8 @@ RepoResult ApiProjectRepository::switchWorkspace(const std::string& tenant RepoResult> ApiProjectRepository::listProjects( const std::string& lastProjectId) { const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1") - .arg(QString::fromStdString(lastProjectId)); + .arg(enc(lastProjectId)); + // 本轮仅取首页;hasNextPage 暂不跟进(分页"加载更多"留下一轮)。 const net::ApiResponse r = api_.get(path); if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")}; return {true, dto::parseProjects(r.data).projects, {}}; @@ -57,7 +64,7 @@ RepoResult> ApiProjectRepository::loadStructure(const st RepoResult> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) { const QString path = QStringLiteral("/business/projectWorkbench/queryDsByTmObjectId/%1") - .arg(QString::fromStdString(tmObjectId)); + .arg(enc(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()), {}}; diff --git a/tests/data/test_nav_dto.cpp b/tests/data/test_nav_dto.cpp index e7e4368..312a1fa 100644 --- a/tests/data/test_nav_dto.cpp +++ b/tests/data/test_nav_dto.cpp @@ -119,3 +119,10 @@ 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()); +}