fix(nav): 实测整改——项目用my/profile/queryProject、切换空间重注入token、结构按type建树(过滤DS)、下拉互斥、去重复项目根

This commit is contained in:
gaozheng 2026-06-09 13:58:59 +08:00
parent 1f1cf5cd3c
commit 60d46cf1db
6 changed files with 91 additions and 44 deletions

View File

@ -211,6 +211,8 @@ 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);
@ -218,9 +220,12 @@ 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]() { 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<data::ProjectSummary>& 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<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]() { 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("(暂无项目)"));

View File

@ -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();
}

View File

@ -41,25 +41,26 @@ RepoResult<bool> 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<std::vector<ProjectSummary>> 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<std::vector<ProjectSummary>> 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<std::vector<StructNode>> 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<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) {

View File

@ -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<Workspace> 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<size_t>(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<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;
}
std::vector<StructNode> parseStructNodes(const QJsonArray& arr) {
std::vector<StructNode> out;
out.reserve(static_cast<size_t>(arr.size()));
@ -79,32 +87,30 @@ std::vector<DsNode> parseDatasets(const QJsonArray& arr) {
}
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;
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 集合内(孤儿)。
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<std::string> visited;
std::set<std::string> visited; // 防环:每个 id 最多进树一次。
std::function<std::vector<StructTreeNode>(const std::string&, bool)> build =
[&](const std::string& parentId, bool root) {
std::vector<StructTreeNode> 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));
}

View File

@ -13,6 +13,9 @@ 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);
// 结构扁平节点数组queryProjectStruct 的 data["projectStructList"])→ 模型。
std::vector<StructNode> parseStructNodes(const QJsonArray& arr);

View File

@ -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<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、T2D1 被过滤)
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 不进树
}