From b4824a6e4e184f435be8575a325cebfc795465ef Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 18:17:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(nav):=20=E9=A1=B9=E7=9B=AE=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=BC=B9=E7=AA=97=EF=BC=88=E5=90=8D=E7=A7=B0/?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=BF=87=E6=BB=A4+=E5=88=86=E9=A1=B5+8?= =?UTF-8?q?=E5=88=97=EF=BC=8C=E7=82=B9=E9=A1=B9=E7=9B=AE=E5=90=8D=E5=88=87?= =?UTF-8?q?=E6=8D=A2=EF=BC=89+=20=E4=B8=8B=E6=8B=89=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/CMakeLists.txt | 3 +- src/app/ProjectListDialog.cpp | 160 ++++++++++++++++++++++ src/app/ProjectListDialog.hpp | 39 ++++++ src/app/TopBar.cpp | 12 +- src/app/TopBar.hpp | 5 +- src/app/main.cpp | 19 ++- src/controller/WorkbenchNavController.cpp | 10 +- src/controller/WorkbenchNavController.hpp | 3 +- src/data/api/ApiProjectRepository.cpp | 21 ++- src/data/api/ApiProjectRepository.hpp | 4 +- src/data/dto/NavDto.cpp | 22 +++ src/data/dto/NavDto.hpp | 5 + src/data/repo/IProjectRepository.hpp | 7 +- src/data/repo/RepoTypes.hpp | 13 +- tests/data/test_nav_dto.cpp | 28 ++++ 15 files changed, 331 insertions(+), 20 deletions(-) create mode 100644 src/app/ProjectListDialog.cpp create mode 100644 src/app/ProjectListDialog.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index f398122..cb04be7 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -23,7 +23,8 @@ add_executable(geopro_desktop WIN32 panels/AnomalyListPanel.cpp panels/DatasetListPanel.cpp panels/ObjectTreePanel.cpp - CentralScene.cpp) + CentralScene.cpp + ProjectListDialog.cpp) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp new file mode 100644 index 0000000..02f0574 --- /dev/null +++ b/src/app/ProjectListDialog.cpp @@ -0,0 +1,160 @@ +#include "ProjectListDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace geopro::app { +namespace { +QString statusText(int s) { + switch (s) { + case 1: return QStringLiteral("未开始"); + case 2: return QStringLiteral("进行中"); + default: return QString::number(s); + } +} +} // namespace + +ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent) + : QDialog(parent), repo_(repo) { + setWindowTitle(QStringLiteral("全部项目")); + resize(980, 560); + + auto* root = new QVBoxLayout(this); + + auto* filter = new QHBoxLayout(); + filter->addWidget(new QLabel(QStringLiteral("项目名称"), this)); + nameEdit_ = new QLineEdit(this); + nameEdit_->setPlaceholderText(QStringLiteral("输入项目名称")); + nameEdit_->setFixedWidth(200); + filter->addWidget(nameEdit_); + filter->addSpacing(8); + filter->addWidget(new QLabel(QStringLiteral("项目类型"), this)); + typeCombo_ = new QComboBox(this); + typeCombo_->setFixedWidth(160); + filter->addWidget(typeCombo_); + filter->addSpacing(8); + auto* searchBtn = new QPushButton(QStringLiteral("搜索"), this); + auto* resetBtn = new QPushButton(QStringLiteral("重置"), this); + filter->addWidget(searchBtn); + filter->addWidget(resetBtn); + filter->addStretch(); + root->addLayout(filter); + + table_ = new QTableWidget(this); + table_->setColumnCount(8); + table_->setHorizontalHeaderLabels(QStringList{ + QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"), + QStringLiteral("项目状态"), QStringLiteral("项目类型"), QStringLiteral("业主单位"), + QStringLiteral("负责人"), QStringLiteral("创建时间")}); + table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + table_->setSelectionBehavior(QAbstractItemView::SelectRows); + table_->setSelectionMode(QAbstractItemView::SingleSelection); + table_->verticalHeader()->setVisible(false); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setColumnWidth(0, 50); + table_->setColumnWidth(1, 260); + root->addWidget(table_, 1); + + auto* bottom = new QHBoxLayout(); + pageLabel_ = new QLabel(this); + bottom->addWidget(pageLabel_); + bottom->addStretch(); + prevBtn_ = new QPushButton(QStringLiteral("上一页"), this); + nextBtn_ = new QPushButton(QStringLiteral("下一页"), this); + bottom->addWidget(prevBtn_); + bottom->addWidget(nextBtn_); + root->addLayout(bottom); + + fillTypeFilter(); + + QObject::connect(searchBtn, &QPushButton::clicked, this, [this]() { + pageNo_ = 1; + query(); + }); + QObject::connect(resetBtn, &QPushButton::clicked, this, [this]() { + nameEdit_->clear(); + typeCombo_->setCurrentIndex(0); + pageNo_ = 1; + query(); + }); + QObject::connect(prevBtn_, &QPushButton::clicked, this, [this]() { + if (pageNo_ > 1) { + --pageNo_; + query(); + } + }); + QObject::connect(nextBtn_, &QPushButton::clicked, this, [this]() { + if (pageNo_ * pageSize_ < total_) { + ++pageNo_; + query(); + } + }); + QObject::connect(table_, &QTableWidget::cellClicked, this, [this](int row, int col) { + if (col != 1) return; // 仅"项目名称"列触发切换 + auto* item = table_->item(row, 1); + if (!item) return; + const QString id = item->data(Qt::UserRole).toString(); + if (id.isEmpty()) return; + emit projectChosen(id, item->text()); + accept(); + }); + + query(); +} + +void ProjectListDialog::fillTypeFilter() { + typeCombo_->addItem(QStringLiteral("全部类型"), QString()); + const auto r = repo_.listProjectTypes(); + if (!r.ok) return; + for (const auto& t : r.value) + typeCombo_->addItem(QString::fromStdString(t.name), QString::fromStdString(t.id)); +} + +void ProjectListDialog::query() { + const std::string name = nameEdit_->text().trimmed().toStdString(); + const std::string typeId = typeCombo_->currentData().toString().toStdString(); + const auto r = repo_.pageProjects(name, typeId, pageNo_, pageSize_); + if (!r.ok) { + table_->setRowCount(0); + pageLabel_->setText(QStringLiteral("加载失败:%1").arg(QString::fromStdString(r.error))); + prevBtn_->setEnabled(false); + nextBtn_->setEnabled(false); + return; + } + total_ = r.value.total; + const auto& rows = r.value.rows; + table_->setRowCount(static_cast(rows.size())); + for (int i = 0; i < static_cast(rows.size()); ++i) { + const auto& p = rows[i]; + auto set = [&](int col, const QString& text) { + table_->setItem(i, col, new QTableWidgetItem(text)); + }; + set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1)); + auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name)); + nameItem->setData(Qt::UserRole, QString::fromStdString(p.id)); + nameItem->setForeground(QColor("#2D6CB5")); + table_->setItem(i, 1, nameItem); + set(2, QString::fromStdString(p.code)); + set(3, statusText(p.status)); + set(4, QString::fromStdString(p.typeName)); + set(5, QString::fromStdString(p.ownerCompany)); + set(6, QString::fromStdString(p.responsiblePerson)); + set(7, QString::fromStdString(p.createTime)); + } + const int pages = total_ > 0 ? (total_ + pageSize_ - 1) / pageSize_ : 1; + pageLabel_->setText(QStringLiteral("共 %1 条 第 %2 / %3 页").arg(total_).arg(pageNo_).arg(pages)); + prevBtn_->setEnabled(pageNo_ > 1); + nextBtn_->setEnabled(pageNo_ < pages); +} + +} // namespace geopro::app diff --git a/src/app/ProjectListDialog.hpp b/src/app/ProjectListDialog.hpp new file mode 100644 index 0000000..c0675a8 --- /dev/null +++ b/src/app/ProjectListDialog.hpp @@ -0,0 +1,39 @@ +#pragma once +#include + +#include "repo/IProjectRepository.hpp" + +class QLineEdit; +class QComboBox; +class QTableWidget; +class QLabel; +class QPushButton; + +namespace geopro::app { + +// 项目列表弹窗:名称/类型过滤 + 分页表格;点项目名 → 切换项目并关闭。 +class ProjectListDialog : public QDialog { + Q_OBJECT +public: + explicit ProjectListDialog(data::IProjectRepository& repo, QWidget* parent = nullptr); + +signals: + void projectChosen(const QString& projectId, const QString& projectName); + +private: + void query(); + void fillTypeFilter(); + + data::IProjectRepository& repo_; + QLineEdit* nameEdit_ = nullptr; + QComboBox* typeCombo_ = nullptr; + QTableWidget* table_ = nullptr; + QLabel* pageLabel_ = nullptr; + QPushButton* prevBtn_ = nullptr; + QPushButton* nextBtn_ = nullptr; + int pageNo_ = 1; + int pageSize_ = 20; + int total_ = 0; +}; + +} // namespace geopro::app diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index c1df2af..9d20fc7 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -236,7 +236,8 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri QStringLiteral(" ▾")); } -void TopBar::setProjects(const std::vector& list, const QString& currentId) { +void TopBar::setProjects(const std::vector& list, const QString& currentId, + bool hasMore) { auto* menu = new QMenu(projBtn_); auto* header = menu->addAction(QStringLiteral("切换项目")); header->setEnabled(false); @@ -261,9 +262,18 @@ void TopBar::setProjects(const std::vector& list, const QS auto* none = menu->addAction(QStringLiteral("(暂无项目)")); none->setEnabled(false); } + if (hasMore) { + menu->addSeparator(); + auto* all = menu->addAction(QStringLiteral("全部项目…")); + QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); }); + } projBtn_->setMenu(menu); projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) + QStringLiteral(" ▾")); } +void TopBar::setProjectButtonText(const QString& name) { + projBtn_->setText(name + QStringLiteral(" ▾")); +} + } // namespace geopro::app diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index 3937f9f..b6388ae 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -17,11 +17,14 @@ public: explicit TopBar(QWidget* parent = nullptr); void setWorkspaces(const std::vector& list, const QString& currentId); - void setProjects(const std::vector& list, const QString& currentId); + void setProjects(const std::vector& list, const QString& currentId, + bool hasMore); + void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字 signals: void workspaceSwitchRequested(const QString& tenantId); void projectSwitchRequested(const QString& projectId); + void allProjectsRequested(); // 点击"全部项目…" private: QToolButton* wsBtn_ = nullptr; diff --git a/src/app/main.cpp b/src/app/main.cpp index 48cfa4c..8fe93f5 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -61,6 +61,7 @@ #include "Theme.hpp" #include "TopBar.hpp" #include "CentralScene.hpp" +#include "ProjectListDialog.hpp" #include "WorkbenchNavController.hpp" #include "api/ApiProjectRepository.hpp" #include "panels/ObjectTreePanel.hpp" @@ -138,6 +139,7 @@ constexpr const char* kWgs84 = "EPSG:4326"; // 在给定 QMainWindow 上构建 M1 工作台。 // repo 生命周期须覆盖到事件循环结束(由调用方保证)。 void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo, + geopro::data::IProjectRepository& projectRepo, geopro::controller::WorkbenchNavController& nav) { // ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ── @@ -642,6 +644,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re &geopro::controller::WorkbenchNavController::switchWorkspace); QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav, &geopro::controller::WorkbenchNavController::switchProject); + QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window, + [&projectRepo, &nav, topBar, &window]() { + auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window); + dlg->setAttribute(Qt::WA_DeleteOnClose); + QObject::connect(dlg, &geopro::app::ProjectListDialog::projectChosen, &nav, + [&nav, topBar](const QString& id, const QString& name) { + topBar->setProjectButtonText(name); + nav.switchProject(id); + }); + dlg->exec(); + }); QObject::connect(objectTree, &geopro::app::ObjectTreePanel::tmClicked, &nav, &geopro::controller::WorkbenchNavController::selectTm); @@ -651,7 +664,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar, [topBar](const std::vector& list, - const QString& cur) { topBar->setProjects(list, cur); }); + const QString& cur, int total) { + topBar->setProjects(list, cur, total > static_cast(list.size())); + }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree, [objectTree, datasetList, fileList, datasetTitle, datasetTabs]( const QString& projectName, @@ -772,7 +787,7 @@ int main(int argc, char* argv[]) window.resize(1280, 800); window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸 - buildWorkbench(window, repo, nav); + buildWorkbench(window, repo, projectRepo, nav); window.show(); nav.start(); // 进入工作台后拉真实 空间/项目/结构 diff --git a/src/controller/WorkbenchNavController.cpp b/src/controller/WorkbenchNavController.cpp index 093268a..ecdefff 100644 --- a/src/controller/WorkbenchNavController.cpp +++ b/src/controller/WorkbenchNavController.cpp @@ -42,15 +42,15 @@ void WorkbenchNavController::start() { } void WorkbenchNavController::loadProjectsAndStructure() { - const auto ps = repo_.listProjects(std::string()); + const auto ps = repo_.pageProjects(std::string(), std::string(), 1, 20); if (!ps.ok) { emit loadFailed(QStringLiteral("projects"), QString::fromStdString(ps.error)); return; } - lastProjects_ = ps.value; + lastProjects_ = ps.value.rows; QString curP; - if (!ps.value.empty()) { - const auto& first = ps.value.front(); + if (!ps.value.rows.empty()) { + const auto& first = ps.value.rows.front(); curP = QString::fromStdString(first.id); currentProjectId_ = first.id; currentProjectName_ = first.name; @@ -60,7 +60,7 @@ void WorkbenchNavController::loadProjectsAndStructure() { currentProjectName_.clear(); currentCrsCode_.clear(); } - emit projectsLoaded(ps.value, curP); + emit projectsLoaded(ps.value.rows, curP, ps.value.total); if (curP.isEmpty()) { emit structureLoaded(QString(), {}); // 暂无项目 → 空树 diff --git a/src/controller/WorkbenchNavController.hpp b/src/controller/WorkbenchNavController.hpp index 6d0b7d6..04f6a66 100644 --- a/src/controller/WorkbenchNavController.hpp +++ b/src/controller/WorkbenchNavController.hpp @@ -28,7 +28,8 @@ public slots: signals: void busyChanged(bool busy); void workspacesLoaded(const std::vector& list, const QString& currentId); - void projectsLoaded(const std::vector& list, const QString& currentId); + void projectsLoaded(const std::vector& list, + const QString& currentId, int total); void structureLoaded(const QString& projectName, const std::vector& nodes); void datasetsLoaded(const QString& tmObjectId, const std::vector& rows, int total, bool append); diff --git a/src/data/api/ApiProjectRepository.cpp b/src/data/api/ApiProjectRepository.cpp index e6765fd..4f3b73a 100644 --- a/src/data/api/ApiProjectRepository.cpp +++ b/src/data/api/ApiProjectRepository.cpp @@ -47,11 +47,22 @@ RepoResult ApiProjectRepository::switchWorkspace(const std::string& tenant return {true, true, {}}; } -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::parseProjectList(r.data.value(QStringLiteral("value")).toArray()), {}}; +RepoResult ApiProjectRepository::pageProjects(const std::string& nameFilter, + const std::string& typeId, int pageNo, + int pageSize) { + QJsonObject body{{QStringLiteral("projectName"), QString::fromStdString(nameFilter)}, + {QStringLiteral("pageNo"), pageNo}, + {QStringLiteral("pageSize"), pageSize}}; + if (!typeId.empty()) body[QStringLiteral("projectTypeId")] = QString::fromStdString(typeId); + const net::ApiResponse r = api_.postJson(QStringLiteral("/business/my/profile/project/page"), body); + if (!ok(r)) return {false, {}, errorOf(r, "pageProjects failed")}; + return {true, dto::parseProjectPage(r.data), {}}; +} + +RepoResult> ApiProjectRepository::listProjectTypes() { + const net::ApiResponse r = api_.get(QStringLiteral("/business/project/type/list")); + if (!ok(r)) return {false, {}, errorOf(r, "listProjectTypes failed")}; + return {true, dto::parseProjectTypes(r.data.value(QStringLiteral("value")).toArray()), {}}; } RepoResult> ApiProjectRepository::loadStructure(const std::string& projectId) { diff --git a/src/data/api/ApiProjectRepository.hpp b/src/data/api/ApiProjectRepository.hpp index 9501532..9423897 100644 --- a/src/data/api/ApiProjectRepository.hpp +++ b/src/data/api/ApiProjectRepository.hpp @@ -12,7 +12,9 @@ public: RepoResult> listWorkspaces() override; RepoResult switchWorkspace(const std::string& tenantId) override; - RepoResult> listProjects(const std::string& lastProjectId) override; + RepoResult pageProjects(const std::string& nameFilter, const std::string& typeId, + int pageNo, int pageSize) override; + RepoResult> listProjectTypes() override; RepoResult> loadStructure(const std::string& projectId) override; RepoResult loadTmRows(const std::string& projectId, const std::string& tmObjectId, int classifyType, int pageNo) override; diff --git a/src/data/dto/NavDto.cpp b/src/data/dto/NavDto.cpp index f45e26c..ef2adcd 100644 --- a/src/data/dto/NavDto.cpp +++ b/src/data/dto/NavDto.cpp @@ -19,6 +19,11 @@ ProjectSummary parseProjectItem(const QJsonObject& o) { p.typeName = str(o, "projectTypeName"); p.crsCode = str(o, "referenceCRSCode"); p.crsName = str(o, "referenceCRSName"); + p.code = str(o, "projectCode"); + p.projectTypeId = str(o, "projectTypeId"); + p.ownerCompany = str(o, "ownerCompanyName"); + p.responsiblePerson = str(o, "responsiblePersonName"); + p.createTime = str(o, "createTime"); p.status = o.value(QStringLiteral("status")).toInt(); return p; } @@ -55,6 +60,23 @@ std::vector parseProjectList(const QJsonArray& arr) { return out; } +ProjectListPage parseProjectPage(const QJsonObject& data) { + ProjectListPage p; + p.rows = parseProjectList(data.value(QStringLiteral("list")).toArray()); + p.total = data.value(QStringLiteral("total")).toInt(); + return p; +} + +std::vector parseProjectTypes(const QJsonArray& arr) { + std::vector out; + out.reserve(static_cast(arr.size())); + for (const QJsonValue& v : arr) { + const QJsonObject o = v.toObject(); + out.push_back(ProjectType{str(o, "id"), str(o, "name")}); + } + return out; +} + std::vector parseStructNodes(const QJsonArray& arr) { std::vector out; out.reserve(static_cast(arr.size())); diff --git a/src/data/dto/NavDto.hpp b/src/data/dto/NavDto.hpp index 2acbb8a..6a3c2b7 100644 --- a/src/data/dto/NavDto.hpp +++ b/src/data/dto/NavDto.hpp @@ -16,6 +16,11 @@ ProjectPage parseProjects(const QJsonObject& data); // my/profile/queryProject 的 data["value"] 数组 → 模型(与 parseProjects 同字段映射)。 std::vector parseProjectList(const QJsonArray& arr); +// my/profile/project/page 的整个 data 对象 → ProjectListPage(rows + total)。 +ProjectListPage parseProjectPage(const QJsonObject& data); +// project/type/list 的 data["value"] 数组 → 项目类型列表。 +std::vector parseProjectTypes(const QJsonArray& arr); + // 结构扁平节点数组(queryProjectStruct 的 data["projectStructList"])→ 模型。 std::vector parseStructNodes(const QJsonArray& arr); diff --git a/src/data/repo/IProjectRepository.hpp b/src/data/repo/IProjectRepository.hpp index ce9a3ea..eb857db 100644 --- a/src/data/repo/IProjectRepository.hpp +++ b/src/data/repo/IProjectRepository.hpp @@ -19,7 +19,12 @@ public: virtual ~IProjectRepository() = default; virtual RepoResult> listWorkspaces() = 0; virtual RepoResult switchWorkspace(const std::string& tenantId) = 0; - virtual RepoResult> listProjects(const std::string& lastProjectId) = 0; + // 项目分页:nameFilter 名称模糊(可空)、typeId 类型过滤(空=不限)、pageNo 从 1 起。 + virtual RepoResult pageProjects(const std::string& nameFilter, + const std::string& typeId, int pageNo, + int pageSize) = 0; + // 项目类型列表(弹窗类型过滤下拉)。 + virtual RepoResult> listProjectTypes() = 0; virtual RepoResult> loadStructure(const std::string& projectId) = 0; // 按 TM 分页拉数据/文件行:classifyType 3=数据 1=文件;pageNo 从 1 起,pageSize 固定 100。 virtual RepoResult loadTmRows(const std::string& projectId, diff --git a/src/data/repo/RepoTypes.hpp b/src/data/repo/RepoTypes.hpp index ab5015c..1bb99a6 100644 --- a/src/data/repo/RepoTypes.hpp +++ b/src/data/repo/RepoTypes.hpp @@ -18,8 +18,17 @@ struct Project { std::string id, name; std::vector gss; }; // 工作空间(=企业租户/空间)。ownerType: 1 个人空间 2 企业空间。 struct Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; }; -// 项目摘要(列表用)。crsCode/crsName 为项目参考坐标系,下一轮替换硬编码 EPSG:4547。 -struct ProjectSummary { std::string id, name, typeName, crsCode, crsName; int status = 0; }; +// 项目摘要(下拉/弹窗列表用)。crsCode 供下一轮替换硬编码 EPSG:4547。 +struct ProjectSummary { + std::string id, name, typeName, crsCode, crsName; + std::string code, projectTypeId, ownerCompany, responsiblePerson, createTime; + int status = 0; +}; + +// 项目类型(弹窗类型过滤下拉用)。 +struct ProjectType { std::string id, name; }; +// 项目分页结果。 +struct ProjectListPage { std::vector rows; int total = 0; }; // 项目结构扁平节点(仅 GS / TM)。客户端按 parentId 建树,叶子=TM。 struct StructNode { std::string id, name, parentId, typeName, confCode; int type = 0; }; diff --git a/tests/data/test_nav_dto.cpp b/tests/data/test_nav_dto.cpp index 7a769c0..2f0e281 100644 --- a/tests/data/test_nav_dto.cpp +++ b/tests/data/test_nav_dto.cpp @@ -173,3 +173,31 @@ TEST(NavDto, ParseDsRowsDataAndFile) { ASSERT_EQ(page.rows.size(), 1u); EXPECT_EQ(page.rows[0].dsName, "a"); } + +TEST(NavDto, ParseProjectItemFullFields) { + const auto v = dto::parseProjectList(arrOf(R"([ + {"id":"p1","projectName":"演示","projectCode":"001","status":2, + "projectTypeId":"t1","projectTypeName":"全量类型", + "ownerCompanyName":"华南所","responsiblePersonName":"张三","createTime":"2026-01-01 00:00:00"} + ])")); + ASSERT_EQ(v.size(), 1u); + EXPECT_EQ(v[0].code, "001"); + EXPECT_EQ(v[0].status, 2); + EXPECT_EQ(v[0].projectTypeId, "t1"); + EXPECT_EQ(v[0].typeName, "全量类型"); + EXPECT_EQ(v[0].ownerCompany, "华南所"); + EXPECT_EQ(v[0].responsiblePerson, "张三"); + EXPECT_EQ(v[0].createTime, "2026-01-01 00:00:00"); +} + +TEST(NavDto, ParseProjectPageAndTypes) { + const auto page = + dto::parseProjectPage(objOf(R"({"total":20,"list":[{"id":"p1","projectName":"a"}]})")); + EXPECT_EQ(page.total, 20); + ASSERT_EQ(page.rows.size(), 1u); + EXPECT_EQ(page.rows[0].name, "a"); + const auto types = dto::parseProjectTypes(arrOf(R"([{"id":"t1","name":"全量类型"}])")); + ASSERT_EQ(types.size(), 1u); + EXPECT_EQ(types[0].id, "t1"); + EXPECT_EQ(types[0].name, "全量类型"); +}