fix(nav): 实测整改——项目用my/profile/queryProject、切换空间重注入token、结构按type建树(过滤DS)、下拉互斥、去重复项目根
This commit is contained in:
parent
1f1cf5cd3c
commit
60d46cf1db
|
|
@ -211,6 +211,8 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
||||||
auto* header = menu->addAction(QStringLiteral("切换空间"));
|
auto* header = menu->addAction(QStringLiteral("切换空间"));
|
||||||
header->setEnabled(false);
|
header->setEnabled(false);
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
|
auto* group = new QActionGroup(menu);
|
||||||
|
group->setExclusive(true); // 互斥:只一个勾选,避免“多选”
|
||||||
QString currentName;
|
QString currentName;
|
||||||
for (const auto& w : list) {
|
for (const auto& w : list) {
|
||||||
const QString id = QString::fromStdString(w.id);
|
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);
|
auto* a = menu->addAction(name);
|
||||||
a->setCheckable(true);
|
a->setCheckable(true);
|
||||||
a->setChecked(id == currentId);
|
a->setChecked(id == currentId);
|
||||||
|
group->addAction(a);
|
||||||
if (id == currentId) currentName = name;
|
if (id == currentId) currentName = name;
|
||||||
QObject::connect(a, &QAction::triggered, this,
|
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||||
[this, id]() { emit workspaceSwitchRequested(id); });
|
wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈
|
||||||
|
emit workspaceSwitchRequested(id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (list.empty()) {
|
if (list.empty()) {
|
||||||
auto* none = menu->addAction(QStringLiteral("(暂无空间)"));
|
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("切换项目"));
|
auto* header = menu->addAction(QStringLiteral("切换项目"));
|
||||||
header->setEnabled(false);
|
header->setEnabled(false);
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
|
auto* group = new QActionGroup(menu);
|
||||||
|
group->setExclusive(true);
|
||||||
QString currentName;
|
QString currentName;
|
||||||
for (const auto& p : list) {
|
for (const auto& p : list) {
|
||||||
const QString id = QString::fromStdString(p.id);
|
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);
|
auto* a = menu->addAction(name);
|
||||||
a->setCheckable(true);
|
a->setCheckable(true);
|
||||||
a->setChecked(id == currentId);
|
a->setChecked(id == currentId);
|
||||||
|
group->addAction(a);
|
||||||
if (id == currentId) currentName = name;
|
if (id == currentId) currentName = name;
|
||||||
QObject::connect(a, &QAction::triggered, this,
|
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||||
[this, id]() { emit projectSwitchRequested(id); });
|
projBtn_->setText(name + QStringLiteral(" ▾"));
|
||||||
|
emit projectSwitchRequested(id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (list.empty()) {
|
if (list.empty()) {
|
||||||
auto* none = menu->addAction(QStringLiteral("(暂无项目)"));
|
auto* none = menu->addAction(QStringLiteral("(暂无项目)"));
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ void ObjectTreePanel::setStructure(const QString& projectName,
|
||||||
}
|
}
|
||||||
hint_->setVisible(false);
|
hint_->setVisible(false);
|
||||||
tree_->setVisible(true);
|
tree_->setVisible(true);
|
||||||
auto* rootItem = new QTreeWidgetItem(tree_);
|
addNodes(tree_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染
|
||||||
rootItem->setText(0, projectName.isEmpty() ? QStringLiteral("项目") : projectName);
|
|
||||||
addNodes(rootItem, roots);
|
|
||||||
tree_->expandAll();
|
tree_->expandAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,25 +41,26 @@ RepoResult<bool> ApiProjectRepository::switchWorkspace(const std::string& tenant
|
||||||
QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId));
|
QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId));
|
||||||
const net::ApiResponse r = api_.postJson(path, QJsonObject{});
|
const net::ApiResponse r = api_.postJson(path, QJsonObject{});
|
||||||
if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")};
|
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, {}};
|
return {true, true, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
RepoResult<std::vector<ProjectSummary>> ApiProjectRepository::listProjects(
|
RepoResult<std::vector<ProjectSummary>> ApiProjectRepository::listProjects(const std::string&) {
|
||||||
const std::string& lastProjectId) {
|
// 我的工作台项目列表(当前空间全部项目)。queryByUser 实测为空,故用此接口。
|
||||||
const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1")
|
const net::ApiResponse r = api_.get(QStringLiteral("/business/my/profile/queryProject"));
|
||||||
.arg(enc(lastProjectId));
|
|
||||||
// 本轮仅取首页;hasNextPage 暂不跟进(分页"加载更多"留下一轮)。
|
|
||||||
const net::ApiResponse r = api_.get(path);
|
|
||||||
if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")};
|
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) {
|
RepoResult<std::vector<StructNode>> ApiProjectRepository::loadStructure(const std::string& projectId) {
|
||||||
const QJsonObject body{{QStringLiteral("projectId"), QString::fromStdString(projectId)}};
|
// 项目结构(项目根 + GS + TM;不含 DS)。比 projectWorkbench 干净。
|
||||||
const net::ApiResponse r =
|
const QString path =
|
||||||
api_.postJson(QStringLiteral("/business/projectWorkbench/queryProjectStruct"), body);
|
QStringLiteral("/business/projectStruct/queryProjectStruct/%1").arg(enc(projectId));
|
||||||
|
const net::ApiResponse r = api_.get(path);
|
||||||
if (!ok(r)) return {false, {}, errorOf(r, "loadStructure failed")};
|
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) {
|
RepoResult<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,17 @@ namespace {
|
||||||
std::string str(const QJsonObject& o, const char* key) {
|
std::string str(const QJsonObject& o, const char* key) {
|
||||||
return o.value(QString::fromLatin1(key)).toString().toStdString();
|
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
|
} // namespace
|
||||||
|
|
||||||
std::vector<Workspace> parseWorkspaces(const QJsonArray& arr) {
|
std::vector<Workspace> parseWorkspaces(const QJsonArray& arr) {
|
||||||
|
|
@ -33,20 +44,17 @@ ProjectPage parseProjects(const QJsonObject& data) {
|
||||||
page.hasNextPage = data.value(QStringLiteral("hasNextPage")).toBool();
|
page.hasNextPage = data.value(QStringLiteral("hasNextPage")).toBool();
|
||||||
const QJsonArray list = data.value(QStringLiteral("projectList")).toArray();
|
const QJsonArray list = data.value(QStringLiteral("projectList")).toArray();
|
||||||
page.projects.reserve(static_cast<size_t>(list.size()));
|
page.projects.reserve(static_cast<size_t>(list.size()));
|
||||||
for (const QJsonValue& v : list) {
|
for (const QJsonValue& v : list) page.projects.push_back(parseProjectItem(v.toObject()));
|
||||||
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));
|
|
||||||
}
|
|
||||||
return page;
|
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> parseStructNodes(const QJsonArray& arr) {
|
||||||
std::vector<StructNode> out;
|
std::vector<StructNode> out;
|
||||||
out.reserve(static_cast<size_t>(arr.size()));
|
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) {
|
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> ids;
|
||||||
std::set<std::string> hasChild;
|
for (const auto& n : nodes) ids.insert(n.id);
|
||||||
for (const auto& n : flat) {
|
// 根层:parentId 为空 / "0" / 不在集合内(孤儿)。
|
||||||
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 集合内(孤儿)。
|
|
||||||
auto isRootLevel = [&](const StructNode& n) {
|
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 最多进树一次。对正常树(单父)等价于原逻辑;
|
std::set<std::string> visited; // 防环:每个 id 最多进树一次。
|
||||||
// 对不可信后端数据的多节点环 / 重复 id 环,避免无限递归(规约:永不信任外部数据)。
|
|
||||||
std::set<std::string> visited;
|
|
||||||
std::function<std::vector<StructTreeNode>(const std::string&, bool)> build =
|
std::function<std::vector<StructTreeNode>(const std::string&, bool)> build =
|
||||||
[&](const std::string& parentId, bool root) {
|
[&](const std::string& parentId, bool root) {
|
||||||
std::vector<StructTreeNode> out;
|
std::vector<StructTreeNode> out;
|
||||||
for (const auto& n : flat) {
|
for (const auto& n : nodes) {
|
||||||
const bool belongs = root ? isRootLevel(n) : (n.parentId == parentId);
|
const bool belongs = root ? isRootLevel(n) : (n.parentId == parentId);
|
||||||
if (!belongs) continue;
|
if (!belongs) continue;
|
||||||
if (visited.count(n.id)) continue; // 已进树 → 跳过,防环/防重复
|
if (visited.count(n.id)) continue;
|
||||||
visited.insert(n.id);
|
visited.insert(n.id);
|
||||||
StructTreeNode t;
|
StructTreeNode t;
|
||||||
t.node = n;
|
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);
|
t.children = build(n.id, false);
|
||||||
out.push_back(std::move(t));
|
out.push_back(std::move(t));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ std::vector<Workspace> parseWorkspaces(const QJsonArray& arr);
|
||||||
struct ProjectPage { std::vector<ProjectSummary> projects; bool hasNextPage = false; };
|
struct ProjectPage { std::vector<ProjectSummary> projects; bool hasNextPage = false; };
|
||||||
ProjectPage parseProjects(const QJsonObject& data);
|
ProjectPage parseProjects(const QJsonObject& data);
|
||||||
|
|
||||||
|
// my/profile/queryProject 的 data["value"] 数组 → 模型(与 parseProjects 同字段映射)。
|
||||||
|
std::vector<ProjectSummary> parseProjectList(const QJsonArray& arr);
|
||||||
|
|
||||||
// 结构扁平节点数组(queryProjectStruct 的 data["projectStructList"])→ 模型。
|
// 结构扁平节点数组(queryProjectStruct 的 data["projectStructList"])→ 模型。
|
||||||
std::vector<StructNode> parseStructNodes(const QJsonArray& arr);
|
std::vector<StructNode> parseStructNodes(const QJsonArray& arr);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,3 +126,32 @@ TEST(NavDto, ParseProjectsEmptyAndMissingListGraceful) {
|
||||||
const auto p = dto::parseProjects(objOf(R"({"projectList":[]})"));
|
const auto p = dto::parseProjects(objOf(R"({"projectList":[]})"));
|
||||||
EXPECT_TRUE(p.projects.empty());
|
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、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 不进树
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue