From 60d46cf1db4b01c743192f4168219ecd71f9eb14 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 13:58:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(nav):=20=E5=AE=9E=E6=B5=8B=E6=95=B4?= =?UTF-8?q?=E6=94=B9=E2=80=94=E2=80=94=E9=A1=B9=E7=9B=AE=E7=94=A8my/profil?= =?UTF-8?q?e/queryProject=E3=80=81=E5=88=87=E6=8D=A2=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E9=87=8D=E6=B3=A8=E5=85=A5token=E3=80=81=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=8C=89type=E5=BB=BA=E6=A0=91(=E8=BF=87=E6=BB=A4DS)=E3=80=81?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E4=BA=92=E6=96=A5=E3=80=81=E5=8E=BB=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E9=A1=B9=E7=9B=AE=E6=A0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/TopBar.cpp | 18 +++++++-- src/app/panels/ObjectTreePanel.cpp | 4 +- src/data/api/ApiProjectRepository.cpp | 23 ++++++----- src/data/dto/NavDto.cpp | 58 +++++++++++++++------------ src/data/dto/NavDto.hpp | 3 ++ tests/data/test_nav_dto.cpp | 29 ++++++++++++++ 6 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 97e35a3..c1df2af 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -211,6 +211,8 @@ void TopBar::setWorkspaces(const std::vector& 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); @@ -218,9 +220,12 @@ void TopBar::setWorkspaces(const std::vector& 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]() { emit workspaceSwitchRequested(id); }); + QObject::connect(a, &QAction::triggered, this, [this, id, name]() { + wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈 + emit workspaceSwitchRequested(id); + }); } if (list.empty()) { auto* none = menu->addAction(QStringLiteral("(暂无空间)")); @@ -236,6 +241,8 @@ void TopBar::setProjects(const std::vector& list, const QS 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); @@ -243,9 +250,12 @@ void TopBar::setProjects(const std::vector& 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]() { emit projectSwitchRequested(id); }); + QObject::connect(a, &QAction::triggered, this, [this, id, name]() { + projBtn_->setText(name + QStringLiteral(" ▾")); + emit projectSwitchRequested(id); + }); } if (list.empty()) { auto* none = menu->addAction(QStringLiteral("(暂无项目)")); diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 39aabe6..4d14f23 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -79,9 +79,7 @@ void ObjectTreePanel::setStructure(const QString& projectName, } hint_->setVisible(false); tree_->setVisible(true); - auto* rootItem = new QTreeWidgetItem(tree_); - rootItem->setText(0, projectName.isEmpty() ? QStringLiteral("项目") : projectName); - addNodes(rootItem, roots); + addNodes(tree_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染 tree_->expandAll(); } diff --git a/src/data/api/ApiProjectRepository.cpp b/src/data/api/ApiProjectRepository.cpp index 0c9fdd8..1629256 100644 --- a/src/data/api/ApiProjectRepository.cpp +++ b/src/data/api/ApiProjectRepository.cpp @@ -41,25 +41,26 @@ RepoResult ApiProjectRepository::switchWorkspace(const std::string& tenant 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")}; + // 切换空间返回新 accessToken:必须重新注入,后续请求才落到新空间。 + const QString token = r.data.value(QStringLiteral("accessToken")).toString(); + if (!token.isEmpty()) api_.setToken(token); return {true, true, {}}; } -RepoResult> ApiProjectRepository::listProjects( - const std::string& lastProjectId) { - const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1") - .arg(enc(lastProjectId)); - // 本轮仅取首页;hasNextPage 暂不跟进(分页"加载更多"留下一轮)。 - const net::ApiResponse r = api_.get(path); +RepoResult> ApiProjectRepository::listProjects(const std::string&) { + // 我的工作台项目列表(当前空间全部项目)。queryByUser 实测为空,故用此接口。 + const net::ApiResponse r = api_.get(QStringLiteral("/business/my/profile/queryProject")); if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")}; - return {true, dto::parseProjects(r.data).projects, {}}; + return {true, dto::parseProjectList(r.data.value(QStringLiteral("value")).toArray()), {}}; } RepoResult> ApiProjectRepository::loadStructure(const std::string& projectId) { - const QJsonObject body{{QStringLiteral("projectId"), QString::fromStdString(projectId)}}; - const net::ApiResponse r = - api_.postJson(QStringLiteral("/business/projectWorkbench/queryProjectStruct"), body); + // 项目结构(项目根 + GS + TM;不含 DS)。比 projectWorkbench 干净。 + const QString path = + QStringLiteral("/business/projectStruct/queryProjectStruct/%1").arg(enc(projectId)); + const net::ApiResponse r = api_.get(path); if (!ok(r)) return {false, {}, errorOf(r, "loadStructure failed")}; - return {true, dto::parseStructNodes(r.data.value(QStringLiteral("projectStructList")).toArray()), {}}; + return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}}; } RepoResult> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) { diff --git a/src/data/dto/NavDto.cpp b/src/data/dto/NavDto.cpp index e4a45f2..098d4ae 100644 --- a/src/data/dto/NavDto.cpp +++ b/src/data/dto/NavDto.cpp @@ -11,6 +11,17 @@ 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.status = o.value(QStringLiteral("status")).toInt(); + return p; +} } // namespace std::vector parseWorkspaces(const QJsonArray& arr) { @@ -33,20 +44,17 @@ 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(list.size())); - for (const QJsonValue& v : list) { - const QJsonObject o = v.toObject(); - 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)); - } + for (const QJsonValue& v : list) page.projects.push_back(parseProjectItem(v.toObject())); return page; } +std::vector parseProjectList(const QJsonArray& arr) { + std::vector out; + out.reserve(static_cast(arr.size())); + for (const QJsonValue& v : arr) out.push_back(parseProjectItem(v.toObject())); + return out; +} + std::vector parseStructNodes(const QJsonArray& arr) { std::vector out; out.reserve(static_cast(arr.size())); @@ -79,32 +87,30 @@ std::vector parseDatasets(const QJsonArray& arr) { } std::vector buildStructTree(const std::vector& flat) { + // 过滤 DS(type==3):DS 不进对象树(按 TM 单独拉取到数据列表)。 + std::vector nodes; + nodes.reserve(flat.size()); + for (const auto& n : flat) + if (n.type != 3) nodes.push_back(n); + std::set ids; - std::set 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 集合内(孤儿)。 + for (const auto& n : nodes) ids.insert(n.id); + // 根层:parentId 为空 / "0" / 不在集合内(孤儿)。 auto isRootLevel = [&](const StructNode& n) { - return n.parentId.empty() || ids.find(n.parentId) == ids.end(); + return n.parentId.empty() || n.parentId == "0" || ids.find(n.parentId) == ids.end(); }; - // visited 防环:每个 id 最多进树一次。对正常树(单父)等价于原逻辑; - // 对不可信后端数据的多节点环 / 重复 id 环,避免无限递归(规约:永不信任外部数据)。 - std::set visited; + std::set visited; // 防环:每个 id 最多进树一次。 std::function(const std::string&, bool)> build = [&](const std::string& parentId, bool root) { std::vector out; - for (const auto& n : flat) { + for (const auto& n : nodes) { 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 = isLeaf(n.id); + t.isTm = (n.type == 2); // type: 1=项目根 2=TM(测线) 3=DS(已过滤) t.children = build(n.id, false); out.push_back(std::move(t)); } diff --git a/src/data/dto/NavDto.hpp b/src/data/dto/NavDto.hpp index 20141c8..79955b9 100644 --- a/src/data/dto/NavDto.hpp +++ b/src/data/dto/NavDto.hpp @@ -13,6 +13,9 @@ std::vector parseWorkspaces(const QJsonArray& arr); struct ProjectPage { std::vector projects; bool hasNextPage = false; }; ProjectPage parseProjects(const QJsonObject& data); +// my/profile/queryProject 的 data["value"] 数组 → 模型(与 parseProjects 同字段映射)。 +std::vector parseProjectList(const QJsonArray& arr); + // 结构扁平节点数组(queryProjectStruct 的 data["projectStructList"])→ 模型。 std::vector parseStructNodes(const QJsonArray& arr); diff --git a/tests/data/test_nav_dto.cpp b/tests/data/test_nav_dto.cpp index 312a1fa..0c515ef 100644 --- a/tests/data/test_nav_dto.cpp +++ b/tests/data/test_nav_dto.cpp @@ -126,3 +126,32 @@ TEST(NavDto, ParseProjectsEmptyAndMissingListGraceful) { 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 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 不进树 +}