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("切换空间"));
|
||||
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("(暂无项目)"));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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、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