feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
26 changed files with 556 additions and 6 deletions
Showing only changes of commit 2cf2b6aaa7 - Show all commits

View File

@ -34,6 +34,7 @@ add_executable(geopro_desktop WIN32
panels/chart/RawDataChartView.cpp panels/chart/RawDataChartView.cpp
panels/chart/GridDataChartView.cpp panels/chart/GridDataChartView.cpp
panels/chart/DataTableView.cpp panels/chart/DataTableView.cpp
panels/chart/TablePager.cpp
panels/chart/BarChartView.cpp panels/chart/BarChartView.cpp
panels/chart/LineChartView.cpp panels/chart/LineChartView.cpp
panels/chart/TrajectoryMapView.cpp panels/chart/TrajectoryMapView.cpp

View File

@ -90,6 +90,7 @@
#include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp" #include "panels/chart/GrMeasurementStrategy.hpp"
#include "panels/chart/TrajectoryStrategy.hpp" #include "panels/chart/TrajectoryStrategy.hpp"
#include "panels/chart/GridStrategy.hpp"
#include "api/ApiProjectRepository.hpp" #include "api/ApiProjectRepository.hpp"
#include "api/ApiDatasetRepository.hpp" #include "api/ApiDatasetRepository.hpp"
#include "panels/ObjectTreePanel.hpp" #include "panels/ObjectTreePanel.hpp"
@ -531,6 +532,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 页签懒加载lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ── // ── 页签懒加载lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl, QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl,
&geopro::controller::DatasetDetailController::loadTab); &geopro::controller::DatasetDetailController::loadTab);
// ── 分页:分页器翻页/改每页条数 → 控制器按页加载 → 回填(同 tabReady 路径,刷新表格+分页器)──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabPageNeeded, &detailCtrl,
&geopro::controller::DatasetDetailController::loadTabPaged);
// context 用 detailPanel析构即自动断连避免野指针。window 比 detailPanel 活得久, // context 用 detailPanel析构即自动断连避免野指针。window 比 detailPanel 活得久,
// 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。 // 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel, QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
@ -924,6 +928,7 @@ int main(int argc, char* argv[])
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>()); chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>()); chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::TrajectoryStrategy>()); chartRegistry.add(std::make_unique<geopro::app::TrajectoryStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GridStrategy>());
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其 // ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其

View File

@ -6,6 +6,7 @@
#include "Glyphs.hpp" #include "Glyphs.hpp"
#include "PanelHeader.hpp" #include "PanelHeader.hpp"
#include "panels/LoadingOverlay.hpp" #include "panels/LoadingOverlay.hpp"
#include "panels/chart/DataTableView.hpp"
#include "panels/chart/DetailViewFactory.hpp" #include "panels/chart/DetailViewFactory.hpp"
#include "panels/chart/IDetailView.hpp" #include "panels/chart/IDetailView.hpp"
@ -39,6 +40,16 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
views_[i] = raw; views_[i] = raw;
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget随其尺寸覆盖图区 // lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget随其尺寸覆盖图区
if (spec.lazy) overlays_[static_cast<int>(i)] = new LoadingOverlay(raw->widget()); if (spec.lazy) overlays_[static_cast<int>(i)] = new LoadingOverlay(raw->widget());
// 分页型页签:把表格视图的分页请求冒泡为页信号(携带 dsId/ddCode/tabIndex + 页参数)。
if (spec.paginated) {
if (auto* table = qobject_cast<DataTableView*>(raw->widget())) {
const int idx = static_cast<int>(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}); panelTabs.append({Glyph::Detail, spec.title, raw->widget(), false});
} }
const QVector<HeaderAction> actions = { const QVector<HeaderAction> actions = {

View File

@ -34,6 +34,9 @@ public:
signals: signals:
// lazy 页签首次激活且未加载 → 请求懒加载。 // lazy 页签首次激活且未加载 → 请求懒加载。
void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); 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: private:
QString dsId_; QString dsId_;

View File

@ -30,6 +30,8 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC
setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名 setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名
// 页内 lazy 页签首次激活 → 冒泡为面板信号(外部接控制器 loadTab // 页内 lazy 页签首次激活 → 冒泡为面板信号(外部接控制器 loadTab
connect(p, &DatasetDetailPage::tabNeeded, this, &DatasetDetailPanel::tabNeeded); connect(p, &DatasetDetailPage::tabNeeded, this, &DatasetDetailPanel::tabNeeded);
// 页内分页器翻页 → 冒泡为面板信号(外部接控制器 loadTabPaged
connect(p, &DatasetDetailPage::tabPageNeeded, this, &DatasetDetailPanel::tabPageNeeded);
} }
setCurrentWidget(p); setCurrentWidget(p);
} }

View File

@ -23,6 +23,9 @@ public:
signals: signals:
void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表 void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表
void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); // lazy 页首激活 → 懒加载 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: private:
DatasetDetailPage* pageFor(const QString& dsId) const; DatasetDetailPage* pageFor(const QString& dsId) const;

View File

@ -5,6 +5,8 @@
#include <QTableView> #include <QTableView>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "panels/chart/TablePager.hpp"
namespace geopro::app { namespace geopro::app {
namespace { namespace {
@ -126,6 +128,12 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_)); table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_));
lay->addWidget(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) { void DataTableView::setPayload(const QVariant& payload) {
@ -146,6 +154,14 @@ void DataTableView::setPayload(const QVariant& payload) {
header->setSectionResizeMode(col, QHeaderView::Stretch); header->setSectionResizeMode(col, QHeaderView::Stretch);
} }
} }
// 分页器分页型载荷pageSize>0dd_grid显示并同步状态否则隐藏全量列表
if (t.pageSize > 0) {
pager_->setState(t.total, t.pageNo, t.pageSize);
pager_->show();
} else {
pager_->hide();
}
} }
} // namespace geopro::app } // namespace geopro::app

View File

@ -43,7 +43,11 @@ private:
const TablePayloadModel* model_; // 不拥有 const TablePayloadModel* model_; // 不拥有
}; };
// 通用数据列表视图IDetailView + QTableView。measurement/grid/trajectory 列表共用。 class TablePager;
// 通用数据列表视图IDetailView + QTableView+ 分页型载荷时底部 TablePager 分页器)。
// measurement/grid/trajectory 列表共用。载荷 pageSize>0dd_grid时显示分页器并转发翻页请求
// 否则隐藏分页器(全量列表)。
class DataTableView : public QWidget, public IDetailView { class DataTableView : public QWidget, public IDetailView {
Q_OBJECT Q_OBJECT
public: public:
@ -52,9 +56,14 @@ public:
QWidget* widget() override { return this; } QWidget* widget() override { return this; }
void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩 void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩
signals:
// 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。
void pageRequested(int pageNo, int pageSize);
private: private:
QTableView* table_; QTableView* table_;
TablePayloadModel* model_; TablePayloadModel* model_;
TablePager* pager_; // 分页器pageSize>0 时显示,否则隐藏)
}; };
} // namespace geopro::app } // namespace geopro::app

View File

@ -0,0 +1,18 @@
#pragma once
#include <vector>
#include "IDatasetChartStrategy.hpp" // geopro::controller
namespace geopro::app {
// dd_grid白化数据策略单「列表」页签服务端分页vxe-pager
// 列表 = Tablepaginatedgrid.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<controller::TabSpec> tabs() const override {
return {
{QStringLiteral("列表"), controller::ViewKind::Table,
QStringLiteral("grid.rows"), /*lazy*/ false, /*paginated*/ true},
};
}
};
} // namespace geopro::app

View File

@ -0,0 +1,188 @@
#include "panels/chart/TablePager.hpp"
#include <algorithm>
#include <QComboBox>
#include <QHBoxLayout>
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QToolButton>
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

View File

@ -0,0 +1,48 @@
#pragma once
#include <QWidget>
class QToolButton;
class QLineEdit;
class QComboBox;
class QLabel;
class QHBoxLayout;
class QIntValidator;
namespace geopro::app {
// 分页器(对齐原版 vxe-pagersize--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

View File

@ -35,6 +35,16 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
} }
void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode, int tabIndex) { 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()); auto* s = registry_.find(ddCode.toStdString());
if (!s) return; // 策略消失(不应发生):静默不加载 if (!s) return; // 策略消失(不应发生):静默不加载
const std::vector<controller::TabSpec> tabs = s->tabs(); const std::vector<controller::TabSpec> tabs = s->tabs();
@ -45,7 +55,7 @@ void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode
// 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed且不留半注册的在飞句柄。 // 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed且不留半注册的在飞句柄。
data::DetailLoad* load = nullptr; data::DetailLoad* load = nullptr;
try { try {
load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString()); load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString(), pageNo, pageSize);
} catch (const std::exception& e) { } catch (const std::exception& e) {
qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex, qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex,
qUtf8Printable(spec.loaderKey), e.what()); qUtf8Printable(spec.loaderKey), e.what());

View File

@ -23,7 +23,11 @@ public slots:
// 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。 // 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString()); void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString());
// 加载某页签lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。 // 加载某页签lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。
// 分页型页签(如 dd_grid 列表首载用默认页pageNo=1/pageSize=0 → 仓储解析默认每页条数)。
void loadTab(const QString& dsId, const QString& ddCode, int tabIndex); 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); void focusDataset(const QString& dsId);
signals: signals:
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, 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 loadFailed(const QString& dsId, const QString& message);
void focusRequested(const QString& dsId); void focusRequested(const QString& dsId);
private: private:
// loadTab/loadTabPaged 共用实现:按 (dsId,ddCode,tabIndex) 查 loaderKey带分页参数异步加载。
void loadTabImpl(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo,
int pageSize);
data::IAsyncDatasetRepository& repo_; data::IAsyncDatasetRepository& repo_;
ChartStrategyRegistry& registry_; ChartStrategyRegistry& registry_;
QMap<int, QPointer<data::DetailLoad>> inflight_; // 按页签槽位的在飞句柄§5.0 身份比对) QMap<int, QPointer<data::DetailLoad>> inflight_; // 按页签槽位的在飞句柄§5.0 身份比对)

View File

@ -61,10 +61,14 @@ struct TableColumn {
}; };
// 通用表格载荷:列定义 + 预格式化的行(每格 QString+ 总数(分页用)。 // 通用表格载荷:列定义 + 预格式化的行(每格 QString+ 总数(分页用)。
// 分页dd_grid 列表,服务端分页 vxe-pagerpageSize>0 时视图渲染分页器pageNo 为当前页(1 基)
// pageSize=0默认= 不分页measurement/trajectory 全量列表,一次性返回所有行)。
struct TablePayload { struct TablePayload {
std::vector<TableColumn> columns; std::vector<TableColumn> columns;
std::vector<std::vector<QString>> rows; std::vector<std::vector<QString>> rows;
int total = 0; int total = 0;
int pageNo = 1; // 当前页(1 基);分页用
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager)0=不分页
}; };
// 柱状图系列:名称(图例/legend+ 各类目的 y 值 + 填充色hex如 #5470c6数据色两主题一致 // 柱状图系列:名称(图例/legend+ 各类目的 y 值 + 填充色hex如 #5470c6数据色两主题一致

View File

@ -8,6 +8,7 @@ add_library(geopro_data STATIC
dto/MeasurementDto.cpp dto/MeasurementDto.cpp
dto/GrMeasurementDto.cpp dto/GrMeasurementDto.cpp
dto/TrajectoryDto.cpp dto/TrajectoryDto.cpp
dto/GridDto.cpp
api/ApiProjectRepository.cpp api/ApiProjectRepository.cpp
api/ApiDatasetRepository.cpp api/ApiDatasetRepository.cpp
api/DatasetLoadHandles.cpp api/DatasetLoadHandles.cpp

View File

@ -10,6 +10,7 @@
#include "api/DatasetLoadHandles.hpp" #include "api/DatasetLoadHandles.hpp"
#include "dto/DatasetChartDto.hpp" #include "dto/DatasetChartDto.hpp"
#include "dto/GrMeasurementDto.hpp" #include "dto/GrMeasurementDto.hpp"
#include "dto/GridDto.hpp"
#include "dto/MeasurementDto.hpp" #include "dto/MeasurementDto.hpp"
#include "dto/TrajectoryDto.hpp" #include "dto/TrajectoryDto.hpp"
#include "model/detail/DetailPayloads.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); return new net::ApiBatch(calls, &isFailure);
} }
// dd_grid 白化数据列表(服务端分页):单请求 grid/rows(GETquerydsObjectId/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<net::IApiCall*> 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 } // namespace
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} 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.scatter") return makeInversionScatter(dsId);
if (loaderKey == "inversion.grid") return makeInversionGrid(dsId); if (loaderKey == "inversion.grid") return makeInversionGrid(dsId);
if (loaderKey == "ert_measurement.scatter") return makeMeasurementScatter(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.rows") return makeTrajectoryRows(dsId);
if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId); if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId);
if (loaderKey == "traj.map") return makeTrajectoryMap(dsId); if (loaderKey == "traj.map") return makeTrajectoryMap(dsId);
if (loaderKey == "grid.rows") return makeGridRows(dsId, pageNo, pageSize);
throw std::runtime_error("unknown loaderKey: " + loaderKey); 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→1pageSize<=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<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseGridTable(r[0].data, pn, ps));
});
}
} // namespace geopro::data } // namespace geopro::data

View File

@ -7,7 +7,8 @@ namespace geopro::data {
class ApiDatasetRepository : public IAsyncDatasetRepository { class ApiDatasetRepository : public IAsyncDatasetRepository {
public: public:
explicit ApiDatasetRepository(net::ApiClient& api); 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: private:
DetailLoad* makeInversionScatter(const std::string& dsId); DetailLoad* makeInversionScatter(const std::string& dsId);
DetailLoad* makeInversionGrid(const std::string& dsId); DetailLoad* makeInversionGrid(const std::string& dsId);
@ -18,6 +19,7 @@ private:
DetailLoad* makeTrajectoryRows(const std::string& dsId); DetailLoad* makeTrajectoryRows(const std::string& dsId);
DetailLoad* makeTrajectoryElevation(const std::string& dsId); DetailLoad* makeTrajectoryElevation(const std::string& dsId);
DetailLoad* makeTrajectoryMap(const std::string& dsId); DetailLoad* makeTrajectoryMap(const std::string& dsId);
DetailLoad* makeGridRows(const std::string& dsId, int pageNo, int pageSize);
net::ApiClient& api_; net::ApiClient& api_;
}; };
} // namespace geopro::data } // namespace geopro::data

34
src/data/dto/GridDto.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "dto/GridDto.hpp"
#include <QJsonValue>
#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<int>(i) + 1));
// 总数dd_grid 用 data.totalparseGridHeaderTable 默认回退本批行数,这里以服务端总数覆盖)。
t.total = data.value(QStringLiteral("total")).toInt(t.total);
t.pageNo = pn;
t.pageSize = ps;
return t;
}
} // namespace geopro::data::dto

18
src/data/dto/GridDto.hpp Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <QJsonObject>
#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 条记录)。
//
// 复用通用 parseGridHeaderTablegridHeaderDisplay→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

View File

@ -10,7 +10,10 @@ class IAsyncDatasetRepository {
public: public:
virtual ~IAsyncDatasetRepository() = default; virtual ~IAsyncDatasetRepository() = default;
// 通用页签加载tab 引擎):按 loaderKey 分派,载荷经 QVariant 类型擦除。 // 通用页签加载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 } // namespace geopro::data

View File

@ -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_measurement_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_gr_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_trajectory_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_grid_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
# 线loadAsync + QVariant payload round-trip # 线loadAsync + QVariant payload round-trip
target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp) target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp)

View File

@ -4,6 +4,7 @@
#include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp" #include "panels/chart/GrMeasurementStrategy.hpp"
#include "panels/chart/TrajectoryStrategy.hpp" #include "panels/chart/TrajectoryStrategy.hpp"
#include "panels/chart/GridStrategy.hpp"
using namespace geopro::controller; using namespace geopro::controller;
namespace { namespace {
struct Fake : IDatasetChartStrategy { struct Fake : IDatasetChartStrategy {
@ -93,3 +94,16 @@ TEST(TrajectoryStrategy, DrivesMapTableElevationTabs) {
EXPECT_FALSE(tabs[2].lazy); EXPECT_FALSE(tabs[2].lazy);
EXPECT_EQ(tabs[2].loaderKey.toStdString(), "traj.elev"); 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");
}

View File

@ -46,7 +46,11 @@ struct StubDetailLoad : data::DetailLoad {
// 桩仓储:每个 loaderKey 都造一个新句柄,记录最近一个用于 fire。 // 桩仓储:每个 loaderKey 都造一个新句柄,记录最近一个用于 fire。
struct StubAsyncRepo : data::IAsyncDatasetRepository { struct StubAsyncRepo : data::IAsyncDatasetRepository {
StubDetailLoad* last = nullptr; 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; last = new StubDetailLoad;
return last; return last;
} }
@ -141,6 +145,26 @@ TEST(DatasetDetailController, LoadTabLazyTabStartsLoad) {
EXPECT_EQ(ready.takeFirst().at(1).toInt(), 1); EXPECT_EQ(ready.takeFirst().at(1).toInt(), 1);
} }
// loadTab非分页用默认页参pageNo=1/pageSize=0loadTabPaged 透传分页参数到仓储。
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) { TEST(DatasetDetailController, AbortsPreviousOnSameSlotReload) {
StubAsyncRepo repo; StubAsyncRepo repo;
auto reg = makeInversionRegistry(); auto reg = makeInversionRegistry();

View File

@ -65,6 +65,10 @@ TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) {
DetailLoad* trajMap = repo.loadAsync("traj.map", "ds1"); DetailLoad* trajMap = repo.loadAsync("traj.map", "ds1");
ASSERT_NE(trajMap, nullptr); ASSERT_NE(trajMap, nullptr);
trajMap->abort(); trajMap->abort();
DetailLoad* gridRows = repo.loadAsync("grid.rows", "ds1", 1, 50); // 分页型:带页参
ASSERT_NE(gridRows, nullptr);
gridRows->abort();
} }
// 未知 loaderKey 抛 std::runtime_error。 // 未知 loaderKey 抛 std::runtime_error。

View File

@ -0,0 +1,78 @@
#include <gtest/gtest.h>
#include <QJsonDocument>
#include <QJsonObject>
#include "dto/GridDto.hpp"
using namespace geopro::data::dto;
using geopro::core::TableColumnKind;
namespace {
// 取自真实夹具 tests/fixtures/dd/ert-grid-rows.json 的 datarowList 5 行gridHeaderDisplay
// 两列 x/ytotal=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);
}

19
tests/fixtures/dd/ert-grid-rows.json vendored Normal file
View File

@ -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" }
]
}
}