From ee8342f4bf8ea2374a55f77ebee9640a47b2421b Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 15:29:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(nav):=20ds=E6=95=B0=E6=8D=AE/=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E9=A1=B5=E7=AD=BE=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=20+=20=E5=8A=A0=E8=BD=BD=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E5=88=86=E9=A1=B5=EF=BC=88loadTmRows=E5=88=86=E9=A1=B5+total?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 51 +++++++++++++++++++---- src/app/panels/DatasetListPanel.cpp | 19 +++++---- src/app/panels/DatasetListPanel.hpp | 5 ++- src/controller/WorkbenchNavController.cpp | 46 ++++++++++++++++---- src/controller/WorkbenchNavController.hpp | 13 +++++- src/data/api/ApiProjectRepository.cpp | 10 ++--- src/data/api/ApiProjectRepository.hpp | 5 +-- src/data/dto/NavDto.cpp | 8 ++++ src/data/dto/NavDto.hpp | 3 ++ src/data/repo/IProjectRepository.hpp | 8 ++-- src/data/repo/RepoTypes.hpp | 3 +- tests/data/test_nav_dto.cpp | 11 ++++- 12 files changed, 139 insertions(+), 43 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 2842a80..48cfa4c 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -502,7 +502,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, - [propLabel, detailRendererPtr, detailRenderWindowPtr](QListWidgetItem* item) { + [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) { + if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { + nav.loadMoreData(); + return; + } const QString name = item->data(Qt::DisplayRole).toString().section('\n', 0, 0); detailRendererPtr->RemoveAllViewProps(); @@ -617,6 +621,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } // ── 控制器 ↔ UI 信号接线(导航壳)────────────────────────────────────── + // "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。 + auto removeLoadMore = [](QListWidget* lw) { + if (lw->count() > 0 && + lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) + delete lw->takeItem(lw->count() - 1); + }; + auto addLoadMore = [](QListWidget* lw, int total) { + const int loaded = lw->count(); + if (loaded < total) { + auto* m = new QListWidgetItem( + QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw); + m->setData(geopro::app::kDsLoadMoreRole, true); + m->setTextAlignment(Qt::AlignCenter); + m->setForeground(QColor("#2D6CB5")); + } + return loaded; + }; QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav, &geopro::controller::WorkbenchNavController::switchWorkspace); QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav, @@ -643,19 +664,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re datasetTabs->setTabText(1, QStringLiteral("文件")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, - [datasetList, datasetTitle, datasetTabs]( - const QString&, const std::vector& list) { - geopro::app::populateDatasetList(datasetList, list); + [removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs]( + const QString&, const std::vector& rows, int total, + bool append) { + removeLoadMore(datasetList); + geopro::app::populateDatasetList(datasetList, rows, append); + const int loaded = addLoadMore(datasetList, total); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); datasetTabs->setTabText( - 0, QStringLiteral("数据 (%1)").arg(static_cast(list.size()))); + 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) + : QStringLiteral("数据")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList, - [fileList, datasetTabs](const QString&, - const std::vector& list) { - geopro::app::populateFileList(fileList, list); + [removeLoadMore, addLoadMore, fileList, datasetTabs]( + const QString&, const std::vector& rows, int total, + bool append) { + removeLoadMore(fileList); + geopro::app::populateFileList(fileList, rows, append); + const int loaded = addLoadMore(fileList, total); datasetTabs->setTabText( - 1, QStringLiteral("文件 (%1)").arg(static_cast(list.size()))); + 1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total) + : QStringLiteral("文件")); + }); + QObject::connect(fileList, &QListWidget::itemClicked, fileList, + [&nav](QListWidgetItem* item) { + if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree, [objectTree, &window](const QString& stage, const QString& msg) { diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index f1c6f4c..2ca911f 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -16,22 +16,25 @@ QString humanSize(long long b) { } } // namespace -void populateDatasetList(QListWidget* list, const std::vector& rows) { +void populateDatasetList(QListWidget* list, const std::vector& rows, bool append) { if (!list) return; - list->clear(); + if (!append) list->clear(); for (const auto& d : rows) { QString text = QString::fromStdString(d.dsName); - if (!d.typeName.empty()) text += QStringLiteral("\n%1").arg(QString::fromStdString(d.typeName)); + QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 + if (!d.typeName.empty()) + sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 + if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); auto* item = new QListWidgetItem(text, list); item->setData(kDsIdRole, QString::fromStdString(d.id)); item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode)); } } -void populateFileList(QListWidget* list, const std::vector& rows) { +void populateFileList(QListWidget* list, const std::vector& rows, bool append) { if (!list) return; - list->clear(); - if (rows.empty()) { + if (!append) list->clear(); + if (!append && rows.empty()) { auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); hint->setFlags(Qt::NoItemFlags); hint->setForeground(QColor("#9AA6B6")); @@ -41,7 +44,9 @@ void populateFileList(QListWidget* list, const std::vector& for (const auto& d : rows) { const QString fname = d.fileName.empty() ? QString::fromStdString(d.dsName) : QString::fromStdString(d.fileName); - const QString text = fname + QStringLiteral("\n%1").arg(humanSize(d.fileSize)); + QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 + sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小 + const QString text = fname + QStringLiteral("\n%1").arg(sub); auto* item = new QListWidgetItem(text, list); item->setData(kDsIdRole, QString::fromStdString(d.id)); item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl)); diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index da04fb5..0356760 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -11,10 +11,11 @@ namespace geopro::app { constexpr int kDsIdRole = 0x0100; // Qt::UserRole constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1 constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2(文件下载 url,备用) +constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行 // 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。 -void populateDatasetList(QListWidget* list, const std::vector& rows); +void populateDatasetList(QListWidget* list, const std::vector& rows, bool append); // 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。 -void populateFileList(QListWidget* list, const std::vector& rows); +void populateFileList(QListWidget* list, const std::vector& rows, bool append); } // namespace geopro::app diff --git a/src/controller/WorkbenchNavController.cpp b/src/controller/WorkbenchNavController.cpp index 03b060c..093268a 100644 --- a/src/controller/WorkbenchNavController.cpp +++ b/src/controller/WorkbenchNavController.cpp @@ -106,20 +106,48 @@ void WorkbenchNavController::switchProject(const QString& projectId) { void WorkbenchNavController::selectTm(const QString& tmObjectId) { if (tmObjectId.isEmpty() || busy_) return; BusyGuard guard(this, &busy_); + currentTmId_ = tmObjectId.toStdString(); const std::string pid = currentProjectId_; - const std::string tm = tmObjectId.toStdString(); - const auto data = repo_.loadTmRows(pid, tm, 3); // 数据 - if (!data.ok) { - emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(data.error)); + dataPageNo_ = 1; + filePageNo_ = 1; + const auto d = repo_.loadTmRows(pid, currentTmId_, 3, dataPageNo_); + if (!d.ok) { + emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error)); return; } - emit datasetsLoaded(tmObjectId, data.value); - const auto files = repo_.loadTmRows(pid, tm, 1); // 文件 - if (!files.ok) { - emit loadFailed(QStringLiteral("files"), QString::fromStdString(files.error)); + dataTotal_ = d.value.total; + emit datasetsLoaded(tmObjectId, d.value.rows, d.value.total, false); + const auto f = repo_.loadTmRows(pid, currentTmId_, 1, filePageNo_); + if (!f.ok) { + emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error)); return; } - emit filesLoaded(tmObjectId, files.value); + fileTotal_ = f.value.total; + emit filesLoaded(tmObjectId, f.value.rows, f.value.total, false); +} + +void WorkbenchNavController::loadMoreData() { + if (currentTmId_.empty() || busy_) return; + BusyGuard guard(this, &busy_); + const auto d = repo_.loadTmRows(currentProjectId_, currentTmId_, 3, ++dataPageNo_); + if (!d.ok) { + emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error)); + return; + } + dataTotal_ = d.value.total; + emit datasetsLoaded(QString::fromStdString(currentTmId_), d.value.rows, d.value.total, true); +} + +void WorkbenchNavController::loadMoreFiles() { + if (currentTmId_.empty() || busy_) return; + BusyGuard guard(this, &busy_); + const auto f = repo_.loadTmRows(currentProjectId_, currentTmId_, 1, ++filePageNo_); + if (!f.ok) { + emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error)); + return; + } + fileTotal_ = f.value.total; + emit filesLoaded(QString::fromStdString(currentTmId_), f.value.rows, f.value.total, true); } } // namespace geopro::controller diff --git a/src/controller/WorkbenchNavController.hpp b/src/controller/WorkbenchNavController.hpp index e676685..6d0b7d6 100644 --- a/src/controller/WorkbenchNavController.hpp +++ b/src/controller/WorkbenchNavController.hpp @@ -22,14 +22,18 @@ public slots: void switchWorkspace(const QString& tenantId); void switchProject(const QString& projectId); void selectTm(const QString& tmObjectId); + void loadMoreData(); + void loadMoreFiles(); signals: void busyChanged(bool busy); void workspacesLoaded(const std::vector& list, const QString& currentId); void projectsLoaded(const std::vector& list, const QString& currentId); void structureLoaded(const QString& projectName, const std::vector& nodes); - void datasetsLoaded(const QString& tmObjectId, const std::vector& list); - void filesLoaded(const QString& tmObjectId, const std::vector& list); + void datasetsLoaded(const QString& tmObjectId, const std::vector& rows, + int total, bool append); + void filesLoaded(const QString& tmObjectId, const std::vector& rows, + int total, bool append); void loadFailed(const QString& stage, const QString& message); private: @@ -39,6 +43,11 @@ private: bool busy_ = false; std::vector lastProjects_; std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; + std::string currentTmId_; + int dataPageNo_ = 0; + int filePageNo_ = 0; + int dataTotal_ = 0; + int fileTotal_ = 0; }; } // namespace geopro::controller diff --git a/src/data/api/ApiProjectRepository.cpp b/src/data/api/ApiProjectRepository.cpp index e6c406c..e6765fd 100644 --- a/src/data/api/ApiProjectRepository.cpp +++ b/src/data/api/ApiProjectRepository.cpp @@ -63,9 +63,9 @@ RepoResult> ApiProjectRepository::loadStructure(const st return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}}; } -RepoResult> ApiProjectRepository::loadTmRows(const std::string& projectId, - const std::string& tmObjectId, - int classifyType) { +RepoResult ApiProjectRepository::loadTmRows(const std::string& projectId, + const std::string& tmObjectId, int classifyType, + int pageNo) { const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page") : QStringLiteral("/business/dsObject/data/page"); const QJsonObject body{ @@ -73,11 +73,11 @@ RepoResult> ApiProjectRepository::loadTmRows(const std::strin {QStringLiteral("structParentId"), QString::fromStdString(tmObjectId)}, {QStringLiteral("structParentConfType"), 2}, {QStringLiteral("classifyTypeList"), QJsonArray{classifyType}}, - {QStringLiteral("pageNo"), 1}, + {QStringLiteral("pageNo"), pageNo}, {QStringLiteral("pageSize"), 100}}; const net::ApiResponse r = api_.postJson(path, body); if (!ok(r)) return {false, {}, errorOf(r, "loadTmRows failed")}; - return {true, dto::parseDsRows(r.data.value(QStringLiteral("list")).toArray()), {}}; + return {true, dto::parseDsPage(r.data), {}}; } } // namespace geopro::data diff --git a/src/data/api/ApiProjectRepository.hpp b/src/data/api/ApiProjectRepository.hpp index 354e350..9501532 100644 --- a/src/data/api/ApiProjectRepository.hpp +++ b/src/data/api/ApiProjectRepository.hpp @@ -14,9 +14,8 @@ public: RepoResult switchWorkspace(const std::string& tenantId) override; RepoResult> listProjects(const std::string& lastProjectId) override; RepoResult> loadStructure(const std::string& projectId) override; - RepoResult> loadTmRows(const std::string& projectId, - const std::string& tmObjectId, - int classifyType) override; + RepoResult loadTmRows(const std::string& projectId, const std::string& tmObjectId, + int classifyType, int pageNo) override; private: net::ApiClient& api_; diff --git a/src/data/dto/NavDto.cpp b/src/data/dto/NavDto.cpp index 48e5bb8..f45e26c 100644 --- a/src/data/dto/NavDto.cpp +++ b/src/data/dto/NavDto.cpp @@ -82,6 +82,7 @@ std::vector parseDsRows(const QJsonArray& arr) { d.dsName = str(o, "dsName"); d.typeName = str(o, "name"); // 注意:name 字段=ds类型名 d.ddCode = str(o, "ddCode"); + d.createTime = str(o, "createTime"); const QJsonObject f = o.value(QStringLiteral("file")).toObject(); d.fileName = str(f, "name"); d.fileUrl = str(f, "url"); @@ -91,6 +92,13 @@ std::vector parseDsRows(const QJsonArray& arr) { return out; } +DsPage parseDsPage(const QJsonObject& data) { + DsPage p; + p.rows = parseDsRows(data.value(QStringLiteral("list")).toArray()); + p.total = data.value(QStringLiteral("total")).toInt(); + return p; +} + std::vector buildStructTree(const std::vector& flat) { // 过滤 DS(type==3):DS 不进对象树(按 TM 单独拉取到数据列表)。 std::vector nodes; diff --git a/src/data/dto/NavDto.hpp b/src/data/dto/NavDto.hpp index 3d36326..2acbb8a 100644 --- a/src/data/dto/NavDto.hpp +++ b/src/data/dto/NavDto.hpp @@ -22,6 +22,9 @@ std::vector parseStructNodes(const QJsonArray& arr); // data/page / file/page 的 data["list"] 数组 → DsRow(数据行无 file;文件行含 file{name,size,url})。 std::vector parseDsRows(const QJsonArray& arr); +// data/page 或 file/page 的整个 data 对象 → DsPage(rows + total)。 +DsPage parseDsPage(const QJsonObject& data); + // 扁平 StructNode 按 parentId 建树。叶子(无子节点)=TM。处理:项目直挂 TM、孤儿 parentId、空表。 struct StructTreeNode { StructNode node; diff --git a/src/data/repo/IProjectRepository.hpp b/src/data/repo/IProjectRepository.hpp index 1f46922..ce9a3ea 100644 --- a/src/data/repo/IProjectRepository.hpp +++ b/src/data/repo/IProjectRepository.hpp @@ -21,10 +21,10 @@ public: virtual RepoResult switchWorkspace(const std::string& tenantId) = 0; virtual RepoResult> listProjects(const std::string& lastProjectId) = 0; virtual RepoResult> loadStructure(const std::string& projectId) = 0; - // 按 TM 拉数据集/文件行:classifyType 3=数据(data/page) 1=文件(file/page)。 - virtual RepoResult> loadTmRows(const std::string& projectId, - const std::string& tmObjectId, - int classifyType) = 0; + // 按 TM 分页拉数据/文件行:classifyType 3=数据 1=文件;pageNo 从 1 起,pageSize 固定 100。 + virtual RepoResult loadTmRows(const std::string& projectId, + const std::string& tmObjectId, int classifyType, + int pageNo) = 0; }; } // namespace geopro::data diff --git a/src/data/repo/RepoTypes.hpp b/src/data/repo/RepoTypes.hpp index 4307e93..ab5015c 100644 --- a/src/data/repo/RepoTypes.hpp +++ b/src/data/repo/RepoTypes.hpp @@ -6,10 +6,11 @@ struct DsNode { std::string id, name, ddType; }; // data/page 或 file/page 的一条 ds。数据行只用 dsName/typeName/ddCode;文件行另含 file*。 struct DsRow { - std::string id, dsName, typeName, ddCode; + std::string id, dsName, typeName, ddCode, createTime; std::string fileName, fileUrl; long long fileSize = 0; }; +struct DsPage { std::vector rows; int total = 0; }; struct TmNode { std::string id, name, confCode; std::vector dss; }; struct GsNode { std::string id, name; std::vector tms; }; struct Project { std::string id, name; std::vector gss; }; diff --git a/tests/data/test_nav_dto.cpp b/tests/data/test_nav_dto.cpp index 918b384..7a769c0 100644 --- a/tests/data/test_nav_dto.cpp +++ b/tests/data/test_nav_dto.cpp @@ -147,12 +147,13 @@ TEST(NavDto, BuildStructTreeDropsDsAndTmStaysLeaf) { TEST(NavDto, ParseDsRowsDataAndFile) { const auto d = dto::parseDsRows(arrOf(R"([ - {"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data"} + {"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data","createTime":"2026-03-25 16:48:57"} ])")); ASSERT_EQ(d.size(), 1u); EXPECT_EQ(d[0].id, "d1"); EXPECT_EQ(d[0].dsName, "ERT1-WS"); EXPECT_EQ(d[0].typeName, "电阻率数据"); + EXPECT_EQ(d[0].createTime, "2026-03-25 16:48:57"); EXPECT_TRUE(d[0].fileName.empty()); const auto f = dto::parseDsRows(arrOf(R"([ @@ -163,4 +164,12 @@ TEST(NavDto, ParseDsRowsDataAndFile) { EXPECT_EQ(f[0].fileName, "ERT1-WS.xlsx"); EXPECT_EQ(f[0].fileSize, 62760); EXPECT_EQ(f[0].fileUrl, "/common/file/x.xlsx"); + + const auto page = dto::parseDsPage(objOf(R"({ + "total": 18, + "list": [{"id":"x","dsName":"a","name":"t","ddCode":"dd","createTime":"2026-01-01 00:00:00"}] + })")); + EXPECT_EQ(page.total, 18); + ASSERT_EQ(page.rows.size(), 1u); + EXPECT_EQ(page.rows[0].dsName, "a"); }