From 2cf2b6aaa77ac479d808e3ba70b01bca3446e660 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Sat, 13 Jun 2026 17:27:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(dataset-detail):=20dd=5Fgrid=20=E7=99=BD?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E5=88=97=E8=A1=A8=20+=20=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=88=86=E9=A1=B5(vxe-page?= =?UTF-8?q?r)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⑤ dd_grid 详情:单「列表」页签,序号/x/y 三列(均居中),服务端分页。 按原版(vxe-table)实测复刻:序号列前插、按页偏移自增;total 取 data.total; 分页器对齐 vxe-pager(上一页/页码…/下一页 + 前往N页 + 每页条数 50/100/500/1000 默认50 + 共N条记录)。 引擎新增分页能力(通用,后续分页型详情复用): - TablePayload 加 pageNo/pageSize(>0 才渲染分页器;0=全量列表,measurement/trajectory 不受影响) - GridDto.parseGridTable 复用通用 parseGridHeaderTable,前插序号列 + 回填分页态 - 仓储 loadAsync 增 pageNo/pageSize 透传,新增 grid.rows 加载器(端点 dd/ert/grid/rows,默认50条/页) - 控制器新增 loadTabPaged(保留 3 参 loadTab 以维持 tabNeeded 连接) - TablePager 分页器组件 + DataTableView 按 pageSize>0 显隐并转发 pageRequested → DatasetDetailPage/Panel.tabPageNeeded → Controller.loadTabPaged 反向链路 - GridStrategy(dd_grid 单分页页签) 注册入 main 测试:test_grid_dto(序号偏移/total/分页态/空数据) + grid.rows 分派 + GridStrategy 注册 + 控制器 loadTabPaged 透传/默认页参;154/154 通过。 ABI 关键头(DetailPayloads.hpp)变更后全量重编 geopro 代码并验 obj 新鲜度。 --- src/app/CMakeLists.txt | 1 + src/app/main.cpp | 5 + src/app/panels/DatasetDetailPage.cpp | 11 + src/app/panels/DatasetDetailPage.hpp | 3 + src/app/panels/DatasetDetailPanel.cpp | 2 + src/app/panels/DatasetDetailPanel.hpp | 3 + src/app/panels/chart/DataTableView.cpp | 16 ++ src/app/panels/chart/DataTableView.hpp | 11 +- src/app/panels/chart/GridStrategy.hpp | 18 ++ src/app/panels/chart/TablePager.cpp | 188 ++++++++++++++++++ src/app/panels/chart/TablePager.hpp | 48 +++++ src/controller/DatasetDetailController.cpp | 12 +- src/controller/DatasetDetailController.hpp | 8 + src/core/model/detail/DetailPayloads.hpp | 4 + src/data/CMakeLists.txt | 1 + src/data/api/ApiDatasetRepository.cpp | 28 ++- src/data/api/ApiDatasetRepository.hpp | 4 +- src/data/dto/GridDto.cpp | 34 ++++ src/data/dto/GridDto.hpp | 18 ++ src/data/repo/IAsyncDatasetRepository.hpp | 5 +- tests/CMakeLists.txt | 1 + tests/app/test_chart_strategy_registry.cpp | 14 ++ .../test_dataset_detail_controller.cpp | 26 ++- tests/data/test_async_repo_dispatch.cpp | 4 + tests/data/test_grid_dto.cpp | 78 ++++++++ tests/fixtures/dd/ert-grid-rows.json | 19 ++ 26 files changed, 556 insertions(+), 6 deletions(-) create mode 100644 src/app/panels/chart/GridStrategy.hpp create mode 100644 src/app/panels/chart/TablePager.cpp create mode 100644 src/app/panels/chart/TablePager.hpp create mode 100644 src/data/dto/GridDto.cpp create mode 100644 src/data/dto/GridDto.hpp create mode 100644 tests/data/test_grid_dto.cpp create mode 100644 tests/fixtures/dd/ert-grid-rows.json diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 12c4a11..04437d0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(geopro_desktop WIN32 panels/chart/RawDataChartView.cpp panels/chart/GridDataChartView.cpp panels/chart/DataTableView.cpp + panels/chart/TablePager.cpp panels/chart/BarChartView.cpp panels/chart/LineChartView.cpp panels/chart/TrajectoryMapView.cpp diff --git a/src/app/main.cpp b/src/app/main.cpp index 41cdbab..e602d49 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -90,6 +90,7 @@ #include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/GrMeasurementStrategy.hpp" #include "panels/chart/TrajectoryStrategy.hpp" +#include "panels/chart/GridStrategy.hpp" #include "api/ApiProjectRepository.hpp" #include "api/ApiDatasetRepository.hpp" #include "panels/ObjectTreePanel.hpp" @@ -531,6 +532,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 页签懒加载:lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ── QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl, &geopro::controller::DatasetDetailController::loadTab); + // ── 分页:分页器翻页/改每页条数 → 控制器按页加载 → 回填(同 tabReady 路径,刷新表格+分页器)── + QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabPageNeeded, &detailCtrl, + &geopro::controller::DatasetDetailController::loadTabPaged); // context 用 detailPanel:析构即自动断连,避免野指针。window 比 detailPanel 活得久, // 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。 QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel, @@ -924,6 +928,7 @@ int main(int argc, char* argv[]) chartRegistry.add(std::make_unique()); chartRegistry.add(std::make_unique()); chartRegistry.add(std::make_unique()); + chartRegistry.add(std::make_unique()); geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); // ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其 diff --git a/src/app/panels/DatasetDetailPage.cpp b/src/app/panels/DatasetDetailPage.cpp index aacae48..16bfd00 100644 --- a/src/app/panels/DatasetDetailPage.cpp +++ b/src/app/panels/DatasetDetailPage.cpp @@ -6,6 +6,7 @@ #include "Glyphs.hpp" #include "PanelHeader.hpp" #include "panels/LoadingOverlay.hpp" +#include "panels/chart/DataTableView.hpp" #include "panels/chart/DetailViewFactory.hpp" #include "panels/chart/IDetailView.hpp" @@ -39,6 +40,16 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const views_[i] = raw; // lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。 if (spec.lazy) overlays_[static_cast(i)] = new LoadingOverlay(raw->widget()); + // 分页型页签:把表格视图的分页请求冒泡为页信号(携带 dsId/ddCode/tabIndex + 页参数)。 + if (spec.paginated) { + if (auto* table = qobject_cast(raw->widget())) { + const int idx = static_cast(i); + connect(table, &DataTableView::pageRequested, this, + [this, idx](int pageNo, int pageSize) { + emit tabPageNeeded(dsId_, ddCode_, idx, pageNo, pageSize); + }); + } + } panelTabs.append({Glyph::Detail, spec.title, raw->widget(), false}); } const QVector actions = { diff --git a/src/app/panels/DatasetDetailPage.hpp b/src/app/panels/DatasetDetailPage.hpp index 323b90d..98dc161 100644 --- a/src/app/panels/DatasetDetailPage.hpp +++ b/src/app/panels/DatasetDetailPage.hpp @@ -34,6 +34,9 @@ public: signals: // lazy 页签首次激活且未加载 → 请求懒加载。 void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); + // 分页型页签(paginated)分页器翻页/改每页条数 → 请求按页加载。 + void tabPageNeeded(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); private: QString dsId_; diff --git a/src/app/panels/DatasetDetailPanel.cpp b/src/app/panels/DatasetDetailPanel.cpp index 4567cb6..4688d62 100644 --- a/src/app/panels/DatasetDetailPanel.cpp +++ b/src/app/panels/DatasetDetailPanel.cpp @@ -30,6 +30,8 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名 // 页内 lazy 页签首次激活 → 冒泡为面板信号(外部接控制器 loadTab)。 connect(p, &DatasetDetailPage::tabNeeded, this, &DatasetDetailPanel::tabNeeded); + // 页内分页器翻页 → 冒泡为面板信号(外部接控制器 loadTabPaged)。 + connect(p, &DatasetDetailPage::tabPageNeeded, this, &DatasetDetailPanel::tabPageNeeded); } setCurrentWidget(p); } diff --git a/src/app/panels/DatasetDetailPanel.hpp b/src/app/panels/DatasetDetailPanel.hpp index 5fcf7e8..1707a5d 100644 --- a/src/app/panels/DatasetDetailPanel.hpp +++ b/src/app/panels/DatasetDetailPanel.hpp @@ -23,6 +23,9 @@ public: signals: void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表 void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); // lazy 页首激活 → 懒加载 + // 分页型页签分页器翻页 → 按页加载(外部接控制器 loadTabPaged)。 + void tabPageNeeded(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); private: DatasetDetailPage* pageFor(const QString& dsId) const; diff --git a/src/app/panels/chart/DataTableView.cpp b/src/app/panels/chart/DataTableView.cpp index c6a9bcf..410edf0 100644 --- a/src/app/panels/chart/DataTableView.cpp +++ b/src/app/panels/chart/DataTableView.cpp @@ -5,6 +5,8 @@ #include #include +#include "panels/chart/TablePager.hpp" + namespace geopro::app { namespace { @@ -126,6 +128,12 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) { table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_)); lay->addWidget(table_); + + // 分页器(默认隐藏;分页型载荷 pageSize>0 时显示)。转发翻页请求给壳。 + pager_ = new TablePager(this); + pager_->hide(); + connect(pager_, &TablePager::pageRequested, this, &DataTableView::pageRequested); + lay->addWidget(pager_); } void DataTableView::setPayload(const QVariant& payload) { @@ -146,6 +154,14 @@ void DataTableView::setPayload(const QVariant& payload) { header->setSectionResizeMode(col, QHeaderView::Stretch); } } + + // 分页器:分页型载荷(pageSize>0,dd_grid)显示并同步状态;否则隐藏(全量列表)。 + if (t.pageSize > 0) { + pager_->setState(t.total, t.pageNo, t.pageSize); + pager_->show(); + } else { + pager_->hide(); + } } } // namespace geopro::app diff --git a/src/app/panels/chart/DataTableView.hpp b/src/app/panels/chart/DataTableView.hpp index 72cf498..d0eaaa5 100644 --- a/src/app/panels/chart/DataTableView.hpp +++ b/src/app/panels/chart/DataTableView.hpp @@ -43,7 +43,11 @@ private: const TablePayloadModel* model_; // 不拥有 }; -// 通用数据列表视图:IDetailView + QTableView。measurement/grid/trajectory 列表共用。 +class TablePager; + +// 通用数据列表视图:IDetailView + QTableView(+ 分页型载荷时底部 TablePager 分页器)。 +// measurement/grid/trajectory 列表共用。载荷 pageSize>0(dd_grid)时显示分页器并转发翻页请求; +// 否则隐藏分页器(全量列表)。 class DataTableView : public QWidget, public IDetailView { Q_OBJECT public: @@ -52,9 +56,14 @@ public: QWidget* widget() override { return this; } void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩 +signals: + // 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。 + void pageRequested(int pageNo, int pageSize); + private: QTableView* table_; TablePayloadModel* model_; + TablePager* pager_; // 分页器(pageSize>0 时显示,否则隐藏) }; } // namespace geopro::app diff --git a/src/app/panels/chart/GridStrategy.hpp b/src/app/panels/chart/GridStrategy.hpp new file mode 100644 index 0000000..45a9613 --- /dev/null +++ b/src/app/panels/chart/GridStrategy.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// dd_grid(白化数据)策略:单「列表」页签,服务端分页(vxe-pager)。 +// 列表 = Table(paginated;grid.rows 产 TablePayload:序号/x/y 列 + total/pageNo/pageSize; +// 端点 dd/ert/grid/rows)。非 lazy(单页签,开页即载首页)。 +struct GridStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_grid"; } + std::vector tabs() const override { + return { + {QStringLiteral("列表"), controller::ViewKind::Table, + QStringLiteral("grid.rows"), /*lazy*/ false, /*paginated*/ true}, + }; + } +}; +} // namespace geopro::app diff --git a/src/app/panels/chart/TablePager.cpp b/src/app/panels/chart/TablePager.cpp new file mode 100644 index 0000000..3656647 --- /dev/null +++ b/src/app/panels/chart/TablePager.cpp @@ -0,0 +1,188 @@ +#include "panels/chart/TablePager.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace geopro::app { + +namespace { +// 每页条数选项(实测原版 vxe-pager 下拉:50/100/500/1000,默认 50)。 +const int kPageSizes[] = {50, 100, 500, 1000}; +constexpr int kPagerCount = 7; // 页码窗口(>此值用 … 折叠,对齐 vxe 默认) +constexpr int kJumpStep = 5; // 点击 … 的跳页步长 + +// 分页器样式:常态不设字色(跟随主题/全局样式表);边框用半透明灰(两主题通用); +// hover/选中页用强调蓝 #409EFF(与 DataTableView 开关同色)。 +const char* kPagerQss = R"( +QToolButton { + border: 1px solid rgba(128,128,128,0.35); + border-radius: 3px; + padding: 1px 6px; + min-width: 16px; + min-height: 20px; + background: transparent; +} +QToolButton:hover:enabled { color: #409EFF; border-color: #409EFF; } +QToolButton[active="true"] { color: #409EFF; border-color: #409EFF; font-weight: bold; } +QToolButton:disabled { color: rgba(128,128,128,0.55); } +)"; +} // namespace + +TablePager::TablePager(QWidget* parent) : QWidget(parent) { + setStyleSheet(QString::fromUtf8(kPagerQss)); + + auto* lay = new QHBoxLayout(this); + lay->setContentsMargins(8, 6, 8, 6); + lay->setSpacing(6); + lay->addStretch(1); // 右对齐 + + prevBtn_ = new QToolButton(this); + prevBtn_->setText(QStringLiteral("‹")); + prevBtn_->setCursor(Qt::PointingHandCursor); + connect(prevBtn_, &QToolButton::clicked, this, + [this] { emit pageRequested(pageNo_ - 1, pageSize_); }); + lay->addWidget(prevBtn_); + + numHost_ = new QWidget(this); + numLay_ = new QHBoxLayout(numHost_); + numLay_->setContentsMargins(0, 0, 0, 0); + numLay_->setSpacing(6); + lay->addWidget(numHost_); + + nextBtn_ = new QToolButton(this); + nextBtn_->setText(QStringLiteral("›")); + nextBtn_->setCursor(Qt::PointingHandCursor); + connect(nextBtn_, &QToolButton::clicked, this, + [this] { emit pageRequested(pageNo_ + 1, pageSize_); }); + lay->addWidget(nextBtn_); + + auto* gotoLabel = new QLabel(QStringLiteral("前往"), this); + lay->addWidget(gotoLabel); + + jumpEdit_ = new QLineEdit(this); + jumpEdit_->setFixedWidth(40); + jumpEdit_->setAlignment(Qt::AlignCenter); + jumpValidator_ = new QIntValidator(1, 1, jumpEdit_); + jumpEdit_->setValidator(jumpValidator_); + connect(jumpEdit_, &QLineEdit::returnPressed, this, [this] { + bool ok = false; + int p = jumpEdit_->text().toInt(&ok); + const int pc = pageCount(); + if (!ok) { + jumpEdit_->setText(QString::number(pageNo_)); + return; + } + p = std::min(std::max(1, p), std::max(1, pc)); + if (p == pageNo_) { + jumpEdit_->setText(QString::number(pageNo_)); + return; + } + emit pageRequested(p, pageSize_); + }); + lay->addWidget(jumpEdit_); + + lay->addWidget(new QLabel(QStringLiteral("页"), this)); + + sizeCombo_ = new QComboBox(this); + for (int s : kPageSizes) + sizeCombo_->addItem(QStringLiteral("%1条/页").arg(s), s); + connect(sizeCombo_, &QComboBox::activated, this, [this](int i) { + emit pageRequested(1, sizeCombo_->itemData(i).toInt()); // 改每页条数 → 回第 1 页 + }); + lay->addWidget(sizeCombo_); + + totalLabel_ = new QLabel(this); + lay->addWidget(totalLabel_); + + setState(0, 1, pageSize_); +} + +int TablePager::pageCount() const { + if (pageSize_ <= 0) return 1; + return std::max(1, (total_ + pageSize_ - 1) / pageSize_); +} + +void TablePager::setState(int total, int pageNo, int pageSize) { + total_ = std::max(0, total); + if (pageSize > 0) pageSize_ = pageSize; + const int pc = pageCount(); + pageNo_ = std::min(std::max(1, pageNo), pc); + + prevBtn_->setEnabled(pageNo_ > 1); + nextBtn_->setEnabled(pageNo_ < pc); + + jumpValidator_->setTop(pc); + { + QSignalBlocker b(jumpEdit_); + jumpEdit_->setText(QString::number(pageNo_)); + } + { + QSignalBlocker b(sizeCombo_); + int idx = sizeCombo_->findData(pageSize_); + if (idx < 0) { // 非预设条数(兜底):补一项再选中 + sizeCombo_->addItem(QStringLiteral("%1条/页").arg(pageSize_), pageSize_); + idx = sizeCombo_->findData(pageSize_); + } + sizeCombo_->setCurrentIndex(idx); + } + totalLabel_->setText(QStringLiteral("共 %1 条记录").arg(total_)); + + rebuildNumbers(); +} + +void TablePager::rebuildNumbers() { + // 清空旧页码按钮。 + while (QLayoutItem* it = numLay_->takeAt(0)) { + if (QWidget* w = it->widget()) w->deleteLater(); + delete it; + } + + const int pc = pageCount(); + auto addNum = [this](int p, bool active) { + auto* b = new QToolButton(numHost_); + b->setText(QString::number(p)); + b->setCursor(Qt::PointingHandCursor); + b->setProperty("active", active); + b->setEnabled(!active); // 当前页不可再点 + connect(b, &QToolButton::clicked, this, [this, p] { emit pageRequested(p, pageSize_); }); + numLay_->addWidget(b); + }; + auto addDots = [this](int target) { + auto* b = new QToolButton(numHost_); + b->setText(QStringLiteral("...")); + b->setCursor(Qt::PointingHandCursor); + connect(b, &QToolButton::clicked, this, + [this, target] { emit pageRequested(target, pageSize_); }); + numLay_->addWidget(b); + }; + + if (pc <= kPagerCount) { + for (int p = 1; p <= pc; ++p) addNum(p, p == pageNo_); + return; + } + // 折叠窗口:首页 [ … ] 中段(当前±2) [ … ] 末页。 + addNum(1, pageNo_ == 1); + int left = pageNo_ - 2; + int right = pageNo_ + 2; + if (left < 2) { + left = 2; + right = 5; + } + if (right > pc - 1) { + right = pc - 1; + left = pc - 4; + } + if (left > 2) addDots(std::max(1, pageNo_ - kJumpStep)); + for (int p = left; p <= right; ++p) addNum(p, p == pageNo_); + if (right < pc - 1) addDots(std::min(pc, pageNo_ + kJumpStep)); + addNum(pc, pageNo_ == pc); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/TablePager.hpp b/src/app/panels/chart/TablePager.hpp new file mode 100644 index 0000000..7509f9f --- /dev/null +++ b/src/app/panels/chart/TablePager.hpp @@ -0,0 +1,48 @@ +#pragma once +#include + +class QToolButton; +class QLineEdit; +class QComboBox; +class QLabel; +class QHBoxLayout; +class QIntValidator; + +namespace geopro::app { + +// 分页器(对齐原版 vxe-pager,size--mini):上一页 / 页码(多页带 … 省略跳页)/ 下一页 + +// 「前往 [n] 页」跳页框 + 每页条数下拉 [50/100/500/1000] + 「共 N 条记录」。右对齐。 +// 数据驱动:setState(total,pageNo,pageSize) 重建页码并同步各控件。任何翻页/改每页条数都发 +// pageRequested(改每页条数时回到第 1 页,镜像原版)。仅展示与请求,不持有数据。 +// 配色:常态随主题(不显式设色,跟随全局样式表);hover/选中页用强调蓝 #409EFF(与表格开关同色)。 +class TablePager : public QWidget { + Q_OBJECT +public: + explicit TablePager(QWidget* parent = nullptr); + + // 用总数/当前页/每页条数刷新分页器(重建页码按钮、同步跳页框/下拉/总数文案)。 + void setState(int total, int pageNo, int pageSize); + +signals: + // 请求加载某页(翻页/跳页/改每页条数)。改每页条数时 pageNo=1。 + void pageRequested(int pageNo, int pageSize); + +private: + int pageCount() const; // ceil(total/pageSize),至少 1 + void rebuildNumbers(); // 按当前页重建页码按钮(含 … 跳页) + + int total_ = 0; + int pageNo_ = 1; + int pageSize_ = 50; + + QToolButton* prevBtn_ = nullptr; + QToolButton* nextBtn_ = nullptr; + QWidget* numHost_ = nullptr; + QHBoxLayout* numLay_ = nullptr; + QLineEdit* jumpEdit_ = nullptr; + QIntValidator* jumpValidator_ = nullptr; + QComboBox* sizeCombo_ = nullptr; + QLabel* totalLabel_ = nullptr; +}; + +} // namespace geopro::app diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp index a496f17..5706aa3 100644 --- a/src/controller/DatasetDetailController.cpp +++ b/src/controller/DatasetDetailController.cpp @@ -35,6 +35,16 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd } void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode, int tabIndex) { + loadTabImpl(dsId, ddCode, tabIndex, /*pageNo*/ 1, /*pageSize*/ 0); +} + +void DatasetDetailController::loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, + int pageNo, int pageSize) { + loadTabImpl(dsId, ddCode, tabIndex, pageNo, pageSize); +} + +void DatasetDetailController::loadTabImpl(const QString& dsId, const QString& ddCode, int tabIndex, + int pageNo, int pageSize) { auto* s = registry_.find(ddCode.toStdString()); if (!s) return; // 策略消失(不应发生):静默不加载 const std::vector tabs = s->tabs(); @@ -45,7 +55,7 @@ void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode // 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed,且不留半注册的在飞句柄。 data::DetailLoad* load = nullptr; try { - load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString()); + load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString(), pageNo, pageSize); } catch (const std::exception& e) { qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex, qUtf8Printable(spec.loaderKey), e.what()); diff --git a/src/controller/DatasetDetailController.hpp b/src/controller/DatasetDetailController.hpp index 20b5260..c3f6aaf 100644 --- a/src/controller/DatasetDetailController.hpp +++ b/src/controller/DatasetDetailController.hpp @@ -23,7 +23,11 @@ public slots: // 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。 void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString()); // 加载某页签(lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。 + // 分页型页签(如 dd_grid 列表)首载用默认页(pageNo=1/pageSize=0 → 仓储解析默认每页条数)。 void loadTab(const QString& dsId, const QString& ddCode, int tabIndex); + // 分页加载某页签(分页器翻页/改每页条数时由壳触发)。pageSize=0 → 仓储用该类型默认值。 + void loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); void focusDataset(const QString& dsId); signals: void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, @@ -33,6 +37,10 @@ signals: void loadFailed(const QString& dsId, const QString& message); void focusRequested(const QString& dsId); private: + // loadTab/loadTabPaged 共用实现:按 (dsId,ddCode,tabIndex) 查 loaderKey,带分页参数异步加载。 + void loadTabImpl(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo, + int pageSize); + data::IAsyncDatasetRepository& repo_; ChartStrategyRegistry& registry_; QMap> inflight_; // 按页签槽位的在飞句柄(§5.0 身份比对) diff --git a/src/core/model/detail/DetailPayloads.hpp b/src/core/model/detail/DetailPayloads.hpp index 28518ae..762aba3 100644 --- a/src/core/model/detail/DetailPayloads.hpp +++ b/src/core/model/detail/DetailPayloads.hpp @@ -61,10 +61,14 @@ struct TableColumn { }; // 通用表格载荷:列定义 + 预格式化的行(每格 QString)+ 总数(分页用)。 +// 分页(dd_grid 列表,服务端分页 vxe-pager):pageSize>0 时视图渲染分页器,pageNo 为当前页(1 基); +// pageSize=0(默认)= 不分页(measurement/trajectory 全量列表,一次性返回所有行)。 struct TablePayload { std::vector columns; std::vector> rows; int total = 0; + int pageNo = 1; // 当前页(1 基);分页用 + int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页 }; // 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。 diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index 5161dde..a192760 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(geopro_data STATIC dto/MeasurementDto.cpp dto/GrMeasurementDto.cpp dto/TrajectoryDto.cpp + dto/GridDto.cpp api/ApiProjectRepository.cpp api/ApiDatasetRepository.cpp api/DatasetLoadHandles.cpp diff --git a/src/data/api/ApiDatasetRepository.cpp b/src/data/api/ApiDatasetRepository.cpp index a4863f6..ee4472d 100644 --- a/src/data/api/ApiDatasetRepository.cpp +++ b/src/data/api/ApiDatasetRepository.cpp @@ -10,6 +10,7 @@ #include "api/DatasetLoadHandles.hpp" #include "dto/DatasetChartDto.hpp" #include "dto/GrMeasurementDto.hpp" +#include "dto/GridDto.hpp" #include "dto/MeasurementDto.hpp" #include "dto/TrajectoryDto.hpp" #include "model/detail/DetailPayloads.hpp" @@ -132,11 +133,24 @@ net::ApiBatch* trajectoryLineBatch(net::ApiClient& api, const std::string& dsId) return new net::ApiBatch(calls, &isFailure); } +// dd_grid 白化数据列表(服务端分页):单请求 grid/rows(GET,query:dsObjectId/pageNo/pageSize)。 +// 响应 data = {rowList[{x,y,id}], gridHeaderDisplay[x,y], total}。 +net::ApiBatch* gridRowsBatch(net::ApiClient& api, const std::string& dsId, int pageNo, int pageSize) { + QList calls{ + api.getAsync(QStringLiteral("/business/dd/ert/grid/rows?dsObjectId=%1&pageNo=%2&pageSize=%3") + .arg(enc(dsId)) + .arg(pageNo) + .arg(pageSize)), + }; + return new net::ApiBatch(calls, &isFailure); +} + } // namespace ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} -DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const std::string& dsId) { +DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo, int pageSize) { if (loaderKey == "inversion.scatter") return makeInversionScatter(dsId); if (loaderKey == "inversion.grid") return makeInversionGrid(dsId); if (loaderKey == "ert_measurement.scatter") return makeMeasurementScatter(dsId); @@ -146,6 +160,7 @@ DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const if (loaderKey == "traj.rows") return makeTrajectoryRows(dsId); if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId); if (loaderKey == "traj.map") return makeTrajectoryMap(dsId); + if (loaderKey == "grid.rows") return makeGridRows(dsId, pageNo, pageSize); throw std::runtime_error("unknown loaderKey: " + loaderKey); } @@ -207,4 +222,15 @@ DetailLoad* ApiDatasetRepository::makeTrajectoryMap(const std::string& dsId) { }); } +DetailLoad* ApiDatasetRepository::makeGridRows(const std::string& dsId, int pageNo, int pageSize) { + // 解析默认值:pageNo<=0→1,pageSize<=0→50(原版默认每页 50 条),再传入 URL 与解析器 + // (保证 URL 参数与「序号」列偏移一致)。 + const int pn = pageNo > 0 ? pageNo : 1; + const int ps = pageSize > 0 ? pageSize : 50; + return new ApiDetailLoad(gridRowsBatch(api_, dsId, pn, ps), + [pn, ps](const QList& r) { + return QVariant::fromValue(dto::parseGridTable(r[0].data, pn, ps)); + }); +} + } // namespace geopro::data diff --git a/src/data/api/ApiDatasetRepository.hpp b/src/data/api/ApiDatasetRepository.hpp index 7ecb997..d31cd5d 100644 --- a/src/data/api/ApiDatasetRepository.hpp +++ b/src/data/api/ApiDatasetRepository.hpp @@ -7,7 +7,8 @@ namespace geopro::data { class ApiDatasetRepository : public IAsyncDatasetRepository { public: explicit ApiDatasetRepository(net::ApiClient& api); - DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId) override; + DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo = 1, int pageSize = 0) override; private: DetailLoad* makeInversionScatter(const std::string& dsId); DetailLoad* makeInversionGrid(const std::string& dsId); @@ -18,6 +19,7 @@ private: DetailLoad* makeTrajectoryRows(const std::string& dsId); DetailLoad* makeTrajectoryElevation(const std::string& dsId); DetailLoad* makeTrajectoryMap(const std::string& dsId); + DetailLoad* makeGridRows(const std::string& dsId, int pageNo, int pageSize); net::ApiClient& api_; }; } // namespace geopro::data diff --git a/src/data/dto/GridDto.cpp b/src/data/dto/GridDto.cpp new file mode 100644 index 0000000..1187a12 --- /dev/null +++ b/src/data/dto/GridDto.cpp @@ -0,0 +1,34 @@ +#include "dto/GridDto.hpp" + +#include + +#include "dto/TrajectoryDto.hpp" // parseGridHeaderTable(通用 gridHeaderDisplay+rowList 解析器)复用 + +namespace geopro::data::dto { +using namespace geopro::core; + +TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize) { + const int pn = pageNo > 0 ? pageNo : 1; + const int ps = pageSize > 0 ? pageSize : 0; + + TablePayload t = parseGridHeaderTable(data); // x / y 列 + 各行(按 columnSort) + + // 前插「序号」列(vxe seq 列:居中、按页偏移自增)。 + TableColumn seq; + seq.code = QStringLiteral("__seq"); + seq.title = QStringLiteral("序号"); + seq.sort = 0; + t.columns.insert(t.columns.begin(), seq); + + const int base = (pn - 1) * ps; // 本页首行的全局序号偏移(ps=0 → 偏移 0,从 1 起) + for (size_t i = 0; i < t.rows.size(); ++i) + t.rows[i].insert(t.rows[i].begin(), QString::number(base + static_cast(i) + 1)); + + // 总数:dd_grid 用 data.total(parseGridHeaderTable 默认回退本批行数,这里以服务端总数覆盖)。 + t.total = data.value(QStringLiteral("total")).toInt(t.total); + t.pageNo = pn; + t.pageSize = ps; + return t; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/GridDto.hpp b/src/data/dto/GridDto.hpp new file mode 100644 index 0000000..0728d6d --- /dev/null +++ b/src/data/dto/GridDto.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_grid(白化数据)列表。端点 dd/ert/grid/rows?dsObjectId=&pageNo=&pageSize= +// (服务端分页;data 形如 {rowList[{x,y,id}], gridHeaderDisplay[x,y], total})。 +// +// 渲染(实测 vxe-table 确认):序号 / x / y 三列,全部居中、同色灰字(无特殊着色); +// 底部 vxe-pager(上一页/页码/下一页 + 前往 N 页 + 每页条数选择 [50,100,500,1000] + 共 N 条记录)。 +// +// 复用通用 parseGridHeaderTable(gridHeaderDisplay→x/y 列 + rowList→行),再前插「序号」列 +// (vxe seq 列:按页偏移自增 = (pageNo-1)*pageSize + 行内序号);total 取 data.total(非 __rowTotal); +// 回填 pageNo/pageSize 供视图渲染分页器。pageNo/pageSize 为本次请求参数(仓储已解析默认值后传入)。 +geopro::core::TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize); + +} // namespace geopro::data::dto diff --git a/src/data/repo/IAsyncDatasetRepository.hpp b/src/data/repo/IAsyncDatasetRepository.hpp index 1bf0629..0f8c6bd 100644 --- a/src/data/repo/IAsyncDatasetRepository.hpp +++ b/src/data/repo/IAsyncDatasetRepository.hpp @@ -10,7 +10,10 @@ class IAsyncDatasetRepository { public: virtual ~IAsyncDatasetRepository() = default; // 通用页签加载(tab 引擎):按 loaderKey 分派,载荷经 QVariant 类型擦除。 - virtual DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId) = 0; + // pageNo/pageSize 仅分页型 loaderKey(如 grid.rows)使用;其余 loaderKey 忽略。 + // 默认 pageNo=1/pageSize=0 → 非分页加载(分页 loaderKey 自行解析默认每页条数)。 + virtual DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId, + int pageNo = 1, int pageSize = 0) = 0; }; } // namespace geopro::data diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c522864..cf0a2d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,7 @@ target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) target_sources(geopro_tests PRIVATE data/test_measurement_dto.cpp) target_sources(geopro_tests PRIVATE data/test_gr_dto.cpp) target_sources(geopro_tests PRIVATE data/test_trajectory_dto.cpp) +target_sources(geopro_tests PRIVATE data/test_grid_dto.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) # 通用仓储分派离线单测(loadAsync 分派 + QVariant payload round-trip)。 target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp) diff --git a/tests/app/test_chart_strategy_registry.cpp b/tests/app/test_chart_strategy_registry.cpp index de779d1..9a508b6 100644 --- a/tests/app/test_chart_strategy_registry.cpp +++ b/tests/app/test_chart_strategy_registry.cpp @@ -4,6 +4,7 @@ #include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/GrMeasurementStrategy.hpp" #include "panels/chart/TrajectoryStrategy.hpp" +#include "panels/chart/GridStrategy.hpp" using namespace geopro::controller; namespace { struct Fake : IDatasetChartStrategy { @@ -93,3 +94,16 @@ TEST(TrajectoryStrategy, DrivesMapTableElevationTabs) { EXPECT_FALSE(tabs[2].lazy); EXPECT_EQ(tabs[2].loaderKey.toStdString(), "traj.elev"); } + +TEST(GridStrategy, DrivesSinglePaginatedListTab) { + geopro::app::GridStrategy s; + EXPECT_EQ(s.ddCode(), "dd_grid"); + const auto tabs = s.tabs(); + ASSERT_EQ(tabs.size(), 1u); + // 列表:Table,非 lazy,分页(paginated)。loaderKey grid.rows → TablePayload。 + EXPECT_EQ(tabs[0].title.toStdString(), std::string("列表")); + EXPECT_EQ(tabs[0].kind, ViewKind::Table); + EXPECT_FALSE(tabs[0].lazy); + EXPECT_TRUE(tabs[0].paginated); + EXPECT_EQ(tabs[0].loaderKey.toStdString(), "grid.rows"); +} diff --git a/tests/controller/test_dataset_detail_controller.cpp b/tests/controller/test_dataset_detail_controller.cpp index be8c8a4..b175c96 100644 --- a/tests/controller/test_dataset_detail_controller.cpp +++ b/tests/controller/test_dataset_detail_controller.cpp @@ -46,7 +46,11 @@ struct StubDetailLoad : data::DetailLoad { // 桩仓储:每个 loaderKey 都造一个新句柄,记录最近一个用于 fire。 struct StubAsyncRepo : data::IAsyncDatasetRepository { StubDetailLoad* last = nullptr; - data::DetailLoad* loadAsync(const std::string&, const std::string&) override { + int lastPageNo = 0, lastPageSize = 0; // 记录最近一次分页参数(验证 loadTabPaged 透传) + data::DetailLoad* loadAsync(const std::string&, const std::string&, int pageNo, + int pageSize) override { + lastPageNo = pageNo; + lastPageSize = pageSize; last = new StubDetailLoad; return last; } @@ -141,6 +145,26 @@ TEST(DatasetDetailController, LoadTabLazyTabStartsLoad) { EXPECT_EQ(ready.takeFirst().at(1).toInt(), 1); } +// loadTab(非分页)用默认页参(pageNo=1/pageSize=0);loadTabPaged 透传分页参数到仓储。 +TEST(DatasetDetailController, LoadTabUsesDefaultPageParams) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTab("ds1", "dd_inversion_data", 0); + EXPECT_EQ(repo.lastPageNo, 1); + EXPECT_EQ(repo.lastPageSize, 0); +} + +TEST(DatasetDetailController, LoadTabPagedThreadsPageParams) { + StubAsyncRepo repo; + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); + c.loadTabPaged("ds1", "dd_inversion_data", 0, 3, 100); + ASSERT_NE(repo.last, nullptr); + EXPECT_EQ(repo.lastPageNo, 3); + EXPECT_EQ(repo.lastPageSize, 100); +} + TEST(DatasetDetailController, AbortsPreviousOnSameSlotReload) { StubAsyncRepo repo; auto reg = makeInversionRegistry(); diff --git a/tests/data/test_async_repo_dispatch.cpp b/tests/data/test_async_repo_dispatch.cpp index 6fcd3d8..62d4d9a 100644 --- a/tests/data/test_async_repo_dispatch.cpp +++ b/tests/data/test_async_repo_dispatch.cpp @@ -65,6 +65,10 @@ TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) { DetailLoad* trajMap = repo.loadAsync("traj.map", "ds1"); ASSERT_NE(trajMap, nullptr); trajMap->abort(); + + DetailLoad* gridRows = repo.loadAsync("grid.rows", "ds1", 1, 50); // 分页型:带页参 + ASSERT_NE(gridRows, nullptr); + gridRows->abort(); } // 未知 loaderKey 抛 std::runtime_error。 diff --git a/tests/data/test_grid_dto.cpp b/tests/data/test_grid_dto.cpp new file mode 100644 index 0000000..68c0e61 --- /dev/null +++ b/tests/data/test_grid_dto.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include "dto/GridDto.hpp" + +using namespace geopro::data::dto; +using geopro::core::TableColumnKind; + +namespace { + +// 取自真实夹具 tests/fixtures/dd/ert-grid-rows.json 的 data(rowList 5 行,gridHeaderDisplay +// 两列 x/y,total=62,逐字一致;端点 dd/ert/grid/rows)。内联避免引入 fixture 路径编译定义。 +const char* kGridData = R"({ + "gridHeaderDisplay": [ + { "columnCode": "x", "columnNameChn": "x", "columnNameEng": "x", "columnWidth": 10, "columnSort": 1 }, + { "columnCode": "y", "columnNameChn": "y", "columnNameEng": "y", "columnWidth": 10, "columnSort": 2 } + ], + "functionList": [], + "total": 62, + "rowList": [ + { "x": 2.904, "y": 25.126, "id": "1438944148742144" }, + { "x": 4.897, "y": 25.161, "id": "1438944148742145" }, + { "x": 6.892, "y": 25.247, "id": "1438944148742146" }, + { "x": 8.892, "y": 25.251, "id": "1438944148742147" }, + { "x": 10.891, "y": 25.226, "id": "1438944148742148" } + ] +})"; + +QJsonObject gridData() { return QJsonDocument::fromJson(kGridData).object(); } + +} // namespace + +TEST(GridDto, FirstPagePrependsSeqColumnAndReadsTotal) { + auto t = parseGridTable(gridData(), /*pageNo*/ 1, /*pageSize*/ 50); + + // 三列:序号 / x / y(序号前插,x/y 来自 gridHeaderDisplay 按 columnSort)。 + ASSERT_EQ(t.columns.size(), 3u); + EXPECT_EQ(t.columns[0].title.toStdString(), std::string("序号")); + EXPECT_EQ(t.columns[0].code.toStdString(), "__seq"); + EXPECT_EQ(t.columns[1].title.toStdString(), "x"); + EXPECT_EQ(t.columns[2].title.toStdString(), "y"); + for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text); // 无特殊列 + + // 5 行;首行 序号=1 / x=2.904 / y=25.126。 + ASSERT_EQ(t.rows.size(), 5u); + ASSERT_EQ(t.rows[0].size(), 3u); + EXPECT_EQ(t.rows[0][0].toStdString(), "1"); + EXPECT_EQ(t.rows[0][1].toStdString(), "2.904"); + EXPECT_EQ(t.rows[0][2].toStdString(), "25.126"); + EXPECT_EQ(t.rows[4][0].toStdString(), "5"); // 第 5 行序号=5 + + // 总数取 data.total=62(非本批 5 行);分页态回填。 + EXPECT_EQ(t.total, 62); + EXPECT_EQ(t.pageNo, 1); + EXPECT_EQ(t.pageSize, 50); +} + +TEST(GridDto, SeqColumnOffsetsByPage) { + // 第 2 页、每页 50:本页首行全局序号 = (2-1)*50 + 1 = 51。 + auto t = parseGridTable(gridData(), /*pageNo*/ 2, /*pageSize*/ 50); + ASSERT_EQ(t.rows.size(), 5u); + EXPECT_EQ(t.rows[0][0].toStdString(), "51"); + EXPECT_EQ(t.rows[4][0].toStdString(), "55"); + EXPECT_EQ(t.pageNo, 2); + EXPECT_EQ(t.pageSize, 50); +} + +TEST(GridDto, EmptyDataYieldsSeqOnlyColumnNoRows) { + const QJsonObject empty; + auto t = parseGridTable(empty, 1, 50); + // 空数据:仅有前插的「序号」列,无行;total=0。 + ASSERT_EQ(t.columns.size(), 1u); + EXPECT_EQ(t.columns[0].code.toStdString(), "__seq"); + EXPECT_EQ(t.rows.size(), 0u); + EXPECT_EQ(t.total, 0); +} diff --git a/tests/fixtures/dd/ert-grid-rows.json b/tests/fixtures/dd/ert-grid-rows.json new file mode 100644 index 0000000..8cc07ad --- /dev/null +++ b/tests/fixtures/dd/ert-grid-rows.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "成功", + "data": { + "gridHeaderDisplay": [ + { "columnCode": "x", "columnNameChn": "x", "columnNameEng": "x", "columnWidth": 10, "columnSort": 1 }, + { "columnCode": "y", "columnNameChn": "y", "columnNameEng": "y", "columnWidth": 10, "columnSort": 2 } + ], + "functionList": [], + "total": 62, + "rowList": [ + { "x": 2.904, "y": 25.126, "id": "1438944148742144" }, + { "x": 4.897, "y": 25.161, "id": "1438944148742145" }, + { "x": 6.892, "y": 25.247, "id": "1438944148742146" }, + { "x": 8.892, "y": 25.251, "id": "1438944148742147" }, + { "x": 10.891, "y": 25.226, "id": "1438944148742148" } + ] + } +}