feat(dataset-detail): dd_ert_measurement_gr_data 接地电阻柱状图详情

在按类型渲染引擎上新增 Bar 视图 kind。

- 柱状图(BarChartView:QwtPlotBarChart 单系列 / QwtPlotMultiBarChart 分组):
  X=电极点 #1..#40(electrodeId),Y=电阻(欧姆)=p1Rg,单系列 P1 实心 #5470c6
  (任一行 p2Rg 非空时加分组系列 P2);Y 轴标题「电阻(单位:欧姆)」横排左上、
  X 轴标题「电极点」、图例 P1;主题跟随;析构 clearSeries 防悬垂
- 列表:复用通用 DataTableView,固定 7 列(ID/日期/时间/P1 Rg(Ω)/P1状态/
  P2 Rg(Ω)/P2状态)
- BarPayload(DetailPayloads.hpp 加性);GrMeasurementDto(parseGrBar/parseGrTable,
  响应为 JSON 数组,p1/p2 isDouble 守卫);GrMeasurementStrategy(柱状图/Bar+列表/Table);
  ApiDatasetRepository loaderKey gr.bar/gr.rows(同一端点不同解析);
  DetailViewFactory Bar case;main 注册

测试 138→143 全绿。夹具 tests/fixtures/dd/ert-gr-rows.json。
This commit is contained in:
gaozheng 2026-06-13 11:46:47 +08:00
parent a00aeb9a56
commit bc5613f0d2
17 changed files with 613 additions and 1 deletions

View File

@ -30,6 +30,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/BarChartView.cpp
panels/chart/DetailViewFactory.cpp panels/chart/DetailViewFactory.cpp
panels/chart/ChartTheme.cpp panels/chart/ChartTheme.cpp
panels/chart/ColorMapService.cpp panels/chart/ColorMapService.cpp

View File

@ -86,6 +86,7 @@
#include "DatasetDetailController.hpp" #include "DatasetDetailController.hpp"
#include "panels/chart/ErtInversionStrategy.hpp" #include "panels/chart/ErtInversionStrategy.hpp"
#include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.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"
@ -912,6 +913,7 @@ int main(int argc, char* argv[])
geopro::controller::ChartStrategyRegistry chartRegistry; geopro::controller::ChartStrategyRegistry chartRegistry;
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>()); chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>()); chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其 // ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其

View File

@ -0,0 +1,215 @@
#include "panels/chart/BarChartView.hpp"
#include <QHBoxLayout>
#include <QLabel>
#include <QPalette>
#include <QVBoxLayout>
#include <qwt_column_symbol.h>
#include <qwt_legend.h>
#include <qwt_plot.h>
#include <qwt_plot_barchart.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_multi_barchart.h>
#include <qwt_samples.h>
#include <qwt_scale_draw.h>
#include <qwt_text.h>
#include <algorithm>
#include "Theme.hpp"
#include "panels/chart/ChartTheme.hpp"
namespace geopro::app {
namespace {
// 类目轴刻度把整数刻度位0,1,2,…)映射为类目标签 "#1","#2",…(来自 categories
// 非整数/越界刻度返回空标签(避免次刻度污染)。
class CategoryScaleDraw : public QwtScaleDraw {
public:
explicit CategoryScaleDraw(std::vector<QString> labels) : labels_(std::move(labels)) {
enableComponent(QwtScaleDraw::Backbone, true);
enableComponent(QwtScaleDraw::Ticks, true);
}
QwtText label(double v) const override {
const double r = std::round(v);
if (std::abs(v - r) > 1e-6) return QwtText(); // 仅整数刻度出标签
const int i = static_cast<int>(r);
if (i < 0 || i >= static_cast<int>(labels_.size())) return QwtText();
return labels_[static_cast<size_t>(i)];
}
private:
std::vector<QString> labels_;
};
QColor barColor(const QString& hex) {
QColor c(hex);
return c.isValid() ? c : QColor(0x54, 0x70, 0xc6); // 回退 ECharts 蓝
}
// 造一个 Box 样式柱符号(实心填充、无边框)。
QwtColumnSymbol* makeBarSymbol(const QColor& fill) {
auto* sym = new QwtColumnSymbol(QwtColumnSymbol::Box);
sym->setLineWidth(0);
sym->setFrameStyle(QwtColumnSymbol::NoFrame);
QPalette pal(fill);
pal.setColor(QPalette::Window, fill);
pal.setColor(QPalette::Dark, fill);
sym->setPalette(pal);
return sym;
}
} // namespace
BarChartView::BarChartView(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// 左上水平 y 轴标题ECharts 风格,置于图例行左侧)。
yTitle_ = new QLabel(this);
auto* titleRow = new QWidget(this);
auto* titleLay = new QHBoxLayout(titleRow);
titleLay->setContentsMargins(48, 6, 8, 0); // 左缩进对齐 y 轴上方
titleLay->setSpacing(0);
titleLay->addWidget(yTitle_);
titleLay->addStretch();
lay->addWidget(titleRow);
plot_ = new QwtPlot(this);
plot_->setObjectName(QStringLiteral("grBarPlotArea"));
plot_->enableAxis(QwtPlot::xBottom, true);
plot_->enableAxis(QwtPlot::yLeft, true);
// x 轴标题「电极点」底部居中setData 设文本)。
plot_->setAxisTitle(QwtPlot::xBottom, QwtText());
// 顶部图例P1/P2
plot_->insertLegend(new QwtLegend(), QwtPlot::TopLegend);
// 仅横向y网格弱化与原版 ECharts 一致:仅水平刻度线)。
auto* grid = new QwtPlotGrid();
grid->enableX(false);
grid->enableY(true);
grid->enableXMin(false);
grid->enableYMin(false);
grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine);
grid->attach(plot_);
plot_->setMinimumSize(0, 0);
lay->addWidget(plot_, 1);
// 主题:底色/轴字/网格按当前主题套一次 + 热切换。
applyChartPlotTheme(plot_);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
[this]() {
applyChartPlotTheme(plot_);
// y 轴标题文字色随主题。
QPalette pal = yTitle_->palette();
pal.setColor(QPalette::WindowText,
isDarkTheme() ? tokenColor("text/secondary")
: QColor(90, 90, 90));
yTitle_->setPalette(pal);
});
}
BarChartView::~BarChartView() {
// 卸载并删除已挂的柱状项,先于 QwtPlot autoDelete 触发(与 RawDataChartView/
// GridDataChartView 析构对称,避免悬垂/双删)。
clearSeries();
}
void BarChartView::clearSeries() {
for (QwtPlotItem* it : barItems_) {
it->detach();
delete it;
}
barItems_.clear();
}
void BarChartView::setPayload(const QVariant& payload) {
if (!payload.canConvert<geopro::core::BarPayload>()) return; // 坏/空 → 空态
setData(payload.value<geopro::core::BarPayload>());
}
void BarChartView::setData(const geopro::core::BarPayload& p) {
data_ = p;
clearSeries();
const int n = static_cast<int>(p.categories.size());
// y 轴标题(左上水平 QLabel
yTitle_->setText(p.yTitle);
QPalette tpal = yTitle_->palette();
tpal.setColor(QPalette::WindowText,
isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90));
yTitle_->setPalette(tpal);
// x 轴标题「电极点」+ 类目刻度 "#1".."#40"。
plot_->setAxisTitle(QwtPlot::xBottom, p.xTitle);
plot_->setAxisScaleDraw(QwtPlot::xBottom, new CategoryScaleDraw(p.categories));
plot_->setAxisScale(QwtPlot::xBottom, -0.5, n - 0.5, 1.0); // 每类目一刻度
plot_->setAxisMaxMinor(QwtPlot::xBottom, 0);
// y 范围 0..max自动从数据取上界留 5% 余量)。
double yMax = 0.0;
for (const auto& s : p.series)
for (double v : s.values) yMax = std::max(yMax, v);
plot_->setAxisScale(QwtPlot::yLeft, 0.0, yMax > 0 ? yMax * 1.05 : 1.0);
if (p.series.empty()) {
plot_->replot();
return;
}
if (p.series.size() == 1) {
// 单系列 → QwtPlotBarChart。
const auto& s = p.series.front();
auto* chart = new QwtPlotBarChart(s.name);
QVector<QPointF> samples;
samples.reserve(n);
for (int i = 0; i < n && i < static_cast<int>(s.values.size()); ++i)
samples.append(QPointF(i, s.values[static_cast<size_t>(i)]));
chart->setSamples(samples);
chart->setSymbol(makeBarSymbol(barColor(s.color)));
chart->setLegendMode(QwtPlotBarChart::LegendChartTitle);
chart->setLayoutPolicy(QwtPlotBarChart::AutoAdjustSamples);
chart->setSpacing(10); // 柱间留白(默认柱状观感)
chart->setMargin(4);
chart->attach(plot_);
barItems_.push_back(chart);
} else {
// 多系列P1+P2→ QwtPlotMultiBarChart分组柱
auto* chart = new QwtPlotMultiBarChart();
QList<QwtText> titles;
for (size_t k = 0; k < p.series.size(); ++k) {
chart->setSymbol(static_cast<int>(k), makeBarSymbol(barColor(p.series[k].color)));
titles.append(QwtText(p.series[k].name));
}
chart->setBarTitles(titles);
chart->setStyle(QwtPlotMultiBarChart::Grouped);
chart->setLayoutPolicy(QwtPlotMultiBarChart::AutoAdjustSamples);
chart->setSpacing(10);
chart->setMargin(4);
QVector<QwtSetSample> samples;
samples.reserve(n);
for (int i = 0; i < n; ++i) {
QVector<double> set;
set.reserve(static_cast<int>(p.series.size()));
for (const auto& s : p.series)
set.append(i < static_cast<int>(s.values.size()) ? s.values[static_cast<size_t>(i)]
: 0.0);
samples.append(QwtSetSample(i, set));
}
chart->setSamples(samples);
chart->attach(plot_);
barItems_.push_back(chart);
}
plot_->replot();
}
} // namespace geopro::app

View File

@ -0,0 +1,40 @@
#pragma once
#include <QWidget>
#include <vector>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/IDetailView.hpp"
class QLabel;
class QwtPlot;
class QwtPlotItem;
namespace geopro::app {
// 柱状图视图dd_ert_measurement_gr_data 接地电阻 柱状图页签):
// QwtPlot + QwtPlotBarChart单系列 P1或 QwtPlotMultiBarChart分组 P1+P2
// x 轴类目标签 "#1".."#40"(自定义 QwtScaleDrawx 轴标题「电极点」(底部居中);
// y 轴标题「电阻(单位:欧姆)」用左上水平 QLabelECharts 风格,对齐原版);
// 顶部图例P1/P2柱填充 #5470c6P1数据色两主题一致
// 背景/轴字/网格随主题ChartTheme / ThemeManager
class BarChartView : public QWidget, public IDetailView {
Q_OBJECT
public:
explicit BarChartView(QWidget* parent = nullptr);
~BarChartView() override;
void setData(const geopro::core::BarPayload& p);
QWidget* widget() override { return this; }
void setPayload(const QVariant& payload) override; // 坏/空 variant → 空态不崩
private:
void clearSeries(); // 卸载并删除已挂的柱状项(避免 QwtPlot autoDelete 双删)
geopro::core::BarPayload data_;
QwtPlot* plot_;
QLabel* yTitle_; // 左上水平 y 轴标题ECharts 风格)
std::vector<QwtPlotItem*> barItems_; // 当前挂载的柱状项(已 attach卸载时 detach+delete
};
} // namespace geopro::app

View File

@ -2,6 +2,7 @@
#include <stdexcept> #include <stdexcept>
#include "panels/chart/BarChartView.hpp"
#include "panels/chart/DataTableView.hpp" #include "panels/chart/DataTableView.hpp"
#include "panels/chart/GridDataChartView.hpp" #include "panels/chart/GridDataChartView.hpp"
#include "panels/chart/RawDataChartView.hpp" #include "panels/chart/RawDataChartView.hpp"
@ -17,9 +18,10 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
case controller::ViewKind::Table: case controller::ViewKind::Table:
return std::unique_ptr<IDetailView>(new DataTableView(parent)); return std::unique_ptr<IDetailView>(new DataTableView(parent));
case controller::ViewKind::Bar: case controller::ViewKind::Bar:
return std::unique_ptr<IDetailView>(new BarChartView(parent));
case controller::ViewKind::LineProfile: case controller::ViewKind::LineProfile:
case controller::ViewKind::PolylineMap: case controller::ViewKind::PolylineMap:
// 后续阶段补:Bar(gr_data)/LineProfile,PolylineMap(trajectory)。 // 后续阶段补:LineProfile,PolylineMap(trajectory)。
throw std::runtime_error("makeDetailView: ViewKind not yet implemented"); throw std::runtime_error("makeDetailView: ViewKind not yet implemented");
} }
throw std::runtime_error("makeDetailView: unknown ViewKind"); throw std::runtime_error("makeDetailView: unknown ViewKind");

View File

@ -0,0 +1,19 @@
#pragma once
#include <vector>
#include "IDatasetChartStrategy.hpp" // geopro::controller
namespace geopro::app {
// ERT 接地电阻measurement gr_data策略柱状图同步+ 列表(懒加载)两页签。
// 两页签同一端点 measurement/gr/rowsloaderKey 不同gr.bar 产 BarPayload / gr.rows 产 TablePayload
struct GrMeasurementStrategy : controller::IDatasetChartStrategy {
std::string ddCode() const override { return "dd_ert_measurement_gr_data"; }
std::vector<controller::TabSpec> tabs() const override {
return {
{QStringLiteral("柱状图"), controller::ViewKind::Bar,
QStringLiteral("gr.bar"), /*lazy*/ false, /*paginated*/ false},
{QStringLiteral("列表"), controller::ViewKind::Table,
QStringLiteral("gr.rows"), /*lazy*/ true, /*paginated*/ false},
};
}
};
} // namespace geopro::app

View File

@ -67,8 +67,25 @@ struct TablePayload {
int total = 0; int total = 0;
}; };
// 柱状图系列:名称(图例/legend+ 各类目的 y 值 + 填充色hex如 #5470c6数据色两主题一致
struct BarSeries {
QString name;
std::vector<double> values;
QString color;
};
// 柱状图载荷dd_ert_measurement_gr_data 接地电阻类目x 轴标签,如 "#1".."#40"+
// 一个或多个系列P1/P2每系列一组按类目对齐的 y 值)+ 轴标题。
struct BarPayload {
std::vector<QString> categories;
std::vector<BarSeries> series;
QString xTitle;
QString yTitle;
};
} // namespace geopro::core } // namespace geopro::core
Q_DECLARE_METATYPE(geopro::core::ScatterPayload) Q_DECLARE_METATYPE(geopro::core::ScatterPayload)
Q_DECLARE_METATYPE(geopro::core::ContourPayload) Q_DECLARE_METATYPE(geopro::core::ContourPayload)
Q_DECLARE_METATYPE(geopro::core::TablePayload) Q_DECLARE_METATYPE(geopro::core::TablePayload)
Q_DECLARE_METATYPE(geopro::core::BarPayload)

View File

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

View File

@ -1,5 +1,6 @@
#include "api/ApiDatasetRepository.hpp" #include "api/ApiDatasetRepository.hpp"
#include <stdexcept> #include <stdexcept>
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
@ -8,6 +9,7 @@
#include "ApiBatch.hpp" #include "ApiBatch.hpp"
#include "api/DatasetLoadHandles.hpp" #include "api/DatasetLoadHandles.hpp"
#include "dto/DatasetChartDto.hpp" #include "dto/DatasetChartDto.hpp"
#include "dto/GrMeasurementDto.hpp"
#include "dto/MeasurementDto.hpp" #include "dto/MeasurementDto.hpp"
#include "model/detail/DetailPayloads.hpp" #include "model/detail/DetailPayloads.hpp"
@ -96,6 +98,20 @@ net::ApiBatch* measurementRowsBatch(net::ApiClient& api, const std::string& dsId
return new net::ApiBatch(calls, &isFailure); return new net::ApiBatch(calls, &isFailure);
} }
// 接地电阻gr柱状图与列表同一端点 measurement/gr/rows(GETquery 参数)。
// 响应 data 是 JSON 数组 → ApiResponseParse 包成 {value:[...]},取 r[0].data.value("value").toArray()。
net::ApiBatch* grRowsBatch(net::ApiClient& api, const std::string& dsId) {
QList<net::IApiCall*> calls{
api.getAsync(QStringLiteral("/business/dd/ert/measurement/gr/rows?dsObjectId=%1").arg(enc(dsId))),
};
return new net::ApiBatch(calls, &isFailure);
}
// gr 响应数组(包在 data.value 里)。
QJsonArray grDataArray(const QList<net::ApiResponse>& r) {
return r[0].data.value(QStringLiteral("value")).toArray();
}
} // namespace } // namespace
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {} ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
@ -105,6 +121,8 @@ DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const
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);
if (loaderKey == "ert_measurement.rows") return makeMeasurementRows(dsId); if (loaderKey == "ert_measurement.rows") return makeMeasurementRows(dsId);
if (loaderKey == "gr.bar") return makeGrBar(dsId);
if (loaderKey == "gr.rows") return makeGrRows(dsId);
throw std::runtime_error("unknown loaderKey: " + loaderKey); throw std::runtime_error("unknown loaderKey: " + loaderKey);
} }
@ -136,4 +154,16 @@ DetailLoad* ApiDatasetRepository::makeMeasurementRows(const std::string& dsId) {
}); });
} }
DetailLoad* ApiDatasetRepository::makeGrBar(const std::string& dsId) {
return new ApiDetailLoad(grRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseGrBar(grDataArray(r)));
});
}
DetailLoad* ApiDatasetRepository::makeGrRows(const std::string& dsId) {
return new ApiDetailLoad(grRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseGrTable(grDataArray(r)));
});
}
} // namespace geopro::data } // namespace geopro::data

View File

@ -13,6 +13,8 @@ private:
DetailLoad* makeInversionGrid(const std::string& dsId); DetailLoad* makeInversionGrid(const std::string& dsId);
DetailLoad* makeMeasurementScatter(const std::string& dsId); DetailLoad* makeMeasurementScatter(const std::string& dsId);
DetailLoad* makeMeasurementRows(const std::string& dsId); DetailLoad* makeMeasurementRows(const std::string& dsId);
DetailLoad* makeGrBar(const std::string& dsId);
DetailLoad* makeGrRows(const std::string& dsId);
net::ApiClient& api_; net::ApiClient& api_;
}; };
} // namespace geopro::data } // namespace geopro::data

View File

@ -0,0 +1,87 @@
#include "dto/GrMeasurementDto.hpp"
#include <QJsonObject>
#include <QJsonValue>
namespace geopro::data::dto {
using namespace geopro::core;
namespace {
// ECharts 默认蓝(柱填充,数据色——浅/暗主题一致)。
const QString kP1Color = QStringLiteral("#5470c6");
const QString kP2Color = QStringLiteral("#91cc75"); // ECharts 默认绿(备用 P2 系列)
// 把 JSON 数值/字符串预格式化为单元格 QString整数不带小数点null/缺省 → 空串)。
QString cellText(const QJsonValue& v) {
if (v.isDouble()) {
const double d = v.toDouble();
if (d == static_cast<double>(static_cast<long long>(d)))
return QString::number(static_cast<long long>(d));
return QString::number(d, 'g', 10);
}
if (v.isString()) return v.toString();
return QString(); // null/undefined
}
} // namespace
BarPayload parseGrBar(const QJsonArray& rows) {
BarPayload p;
p.xTitle = QStringLiteral("电极点");
p.yTitle = QStringLiteral("电阻(单位:欧姆)");
BarSeries p1{QStringLiteral("P1"), {}, kP1Color};
BarSeries p2{QStringLiteral("P2"), {}, kP2Color};
bool hasP2 = false; // 任一行 p2Rg 非空 → 建 P2 系列(分组柱)。
for (const auto& e : rows) {
const QJsonObject o = e.toObject();
p.categories.push_back(
QStringLiteral("#") + QString::number(o.value(QStringLiteral("electrodeId")).toInt()));
const QJsonValue p1v = o.value(QStringLiteral("p1Rg"));
p1.values.push_back(p1v.isDouble() ? p1v.toDouble() : 0.0); // null/缺省 → 0与 P2 对称防脏数据)
const QJsonValue p2v = o.value(QStringLiteral("p2Rg"));
if (p2v.isDouble()) hasP2 = true;
p2.values.push_back(p2v.isDouble() ? p2v.toDouble() : 0.0);
}
p.series.push_back(std::move(p1)); // P1 恒有
if (hasP2) p.series.push_back(std::move(p2));
return p;
}
TablePayload parseGrTable(const QJsonArray& rows) {
TablePayload t;
// 7 固定列硬编码gr 响应无列定义。code = JSON 字段名title = 原版列标题。
const struct { const char* code; const char* title; } kCols[] = {
{"electrodeId", "ID"},
{"testDate", "日期"},
{"testTime", "时间"},
{"p1Rg", "P1 Rg(Ω)"},
{"p1RgStatus", "P1状态"},
{"p2Rg", "P2 Rg(Ω)"},
{"p2RgStatus", "P2状态"},
};
for (const auto& c : kCols) {
TableColumn col;
col.code = QString::fromUtf8(c.code);
col.title = QString::fromUtf8(c.title);
t.columns.push_back(col);
}
for (const auto& e : rows) {
const QJsonObject o = e.toObject();
std::vector<QString> cells;
cells.reserve(t.columns.size());
for (const auto& col : t.columns) cells.push_back(cellText(o.value(col.code)));
t.rows.push_back(std::move(cells));
}
t.total = static_cast<int>(t.rows.size());
return t;
}
} // namespace geopro::data::dto

View File

@ -0,0 +1,19 @@
#pragma once
#include <QJsonArray>
#include "model/detail/DetailPayloads.hpp"
namespace geopro::data::dto {
// dd_ert_measurement_gr_data 接地电阻:两页签同一端点 measurement/gr/rowsdata 是 JSON 数组
// (每元素 {electrodeId,testDate,testTime,p1Rg,p1RgStatus,p2Rg,p2RgStatus},无 gridHeaderDisplay
//
// 柱状图:类目 = "#"+electrodeId"#1".."#40");系列 P1 = p1Rg恒有色 #5470c6
// 系列 P2 = p2Rg仅当任一行 p2Rg 非空才建(本样本全 null → 只有 P1
// x 轴标题「电极点」y 轴标题「电阻(单位:欧姆)」。
geopro::core::BarPayload parseGrBar(const QJsonArray& rows);
// 列表7 固定列gr 无 gridHeaderDisplay故硬编码列标题/列码):
// ID | 日期 | 时间 | P1 Rg(Ω) | P1状态 | P2 Rg(Ω) | P2状态。
geopro::core::TablePayload parseGrTable(const QJsonArray& rows);
} // namespace geopro::data::dto

View File

@ -39,6 +39,7 @@ target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp) target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) 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_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

@ -2,6 +2,7 @@
#include "DatasetDetailTab.hpp" #include "DatasetDetailTab.hpp"
#include "IDatasetChartStrategy.hpp" // geopro::controller控制器层 #include "IDatasetChartStrategy.hpp" // geopro::controller控制器层
#include "panels/chart/MeasurementStrategy.hpp" #include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp"
using namespace geopro::controller; using namespace geopro::controller;
namespace { namespace {
struct Fake : IDatasetChartStrategy { struct Fake : IDatasetChartStrategy {
@ -52,3 +53,20 @@ TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) {
EXPECT_TRUE(tabs[1].lazy); EXPECT_TRUE(tabs[1].lazy);
EXPECT_EQ(tabs[1].loaderKey.toStdString(), "ert_measurement.rows"); EXPECT_EQ(tabs[1].loaderKey.toStdString(), "ert_measurement.rows");
} }
TEST(GrMeasurementStrategy, DrivesBarAndTableTabs) {
geopro::app::GrMeasurementStrategy s;
EXPECT_EQ(s.ddCode(), "dd_ert_measurement_gr_data");
const auto tabs = s.tabs();
ASSERT_EQ(tabs.size(), 2u);
// 柱状图Bar非 lazy。
EXPECT_EQ(tabs[0].title.toStdString(), std::string("柱状图"));
EXPECT_EQ(tabs[0].kind, ViewKind::Bar);
EXPECT_FALSE(tabs[0].lazy);
EXPECT_EQ(tabs[0].loaderKey.toStdString(), "gr.bar");
// 列表Tablelazy。
EXPECT_EQ(tabs[1].title.toStdString(), std::string("列表"));
EXPECT_EQ(tabs[1].kind, ViewKind::Table);
EXPECT_TRUE(tabs[1].lazy);
EXPECT_EQ(tabs[1].loaderKey.toStdString(), "gr.rows");
}

View File

@ -45,6 +45,14 @@ TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) {
DetailLoad* measRows = repo.loadAsync("ert_measurement.rows", "ds1"); DetailLoad* measRows = repo.loadAsync("ert_measurement.rows", "ds1");
ASSERT_NE(measRows, nullptr); ASSERT_NE(measRows, nullptr);
measRows->abort(); measRows->abort();
DetailLoad* grBar = repo.loadAsync("gr.bar", "ds1");
ASSERT_NE(grBar, nullptr);
grBar->abort();
DetailLoad* grRows = repo.loadAsync("gr.rows", "ds1");
ASSERT_NE(grRows, nullptr);
grRows->abort();
} }
// 未知 loaderKey 抛 std::runtime_error。 // 未知 loaderKey 抛 std::runtime_error。

123
tests/data/test_gr_dto.cpp Normal file
View File

@ -0,0 +1,123 @@
#include <gtest/gtest.h>
#include <QJsonArray>
#include <QJsonDocument>
#include "dto/GrMeasurementDto.hpp"
using namespace geopro::data::dto;
using geopro::core::TableColumnKind;
namespace {
// 取自真实夹具 tests/fixtures/dd/ert-gr-rows.json 的 data 数组(裁剪至 20 行,逐字一致)。
// 与 test_measurement_dto.cpp 同法:内联夹具,避免引入 fixture 路径编译定义。
const char* kGrRows = R"([
{ "electrodeId": 1, "testDate": "5/26/12", "testTime": "18:25:15", "p1Rg": 1385, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 2, "testDate": "5/26/12", "testTime": "18:25:16", "p1Rg": 3444, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 3, "testDate": "5/26/12", "testTime": "18:25:17", "p1Rg": 737, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 4, "testDate": "5/26/12", "testTime": "18:25:18", "p1Rg": 813, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 5, "testDate": "5/26/12", "testTime": "18:25:19", "p1Rg": 577, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 6, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 652, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 7, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 696, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 8, "testDate": "5/26/12", "testTime": "18:25:21", "p1Rg": 523, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 9, "testDate": "5/26/12", "testTime": "18:25:22", "p1Rg": 587, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 10, "testDate": "5/26/12", "testTime": "18:25:23", "p1Rg": 968, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 11, "testDate": "5/26/12", "testTime": "18:25:24", "p1Rg": 1701, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 12, "testDate": "5/26/12", "testTime": "18:25:25", "p1Rg": 1951, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 13, "testDate": "5/26/12", "testTime": "18:25:26", "p1Rg": 1405, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 14, "testDate": "5/26/12", "testTime": "18:25:27", "p1Rg": 1166, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 15, "testDate": "5/26/12", "testTime": "18:25:28", "p1Rg": 1222, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 16, "testDate": "5/26/12", "testTime": "18:25:29", "p1Rg": 1043, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 17, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 1272, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 18, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 2150, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 19, "testDate": "5/26/12", "testTime": "18:25:31", "p1Rg": 1806, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 20, "testDate": "5/26/12", "testTime": "18:25:32", "p1Rg": 971, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }
])";
QJsonArray grRows() { return QJsonDocument::fromJson(kGrRows).array(); }
} // namespace
TEST(GrDto, ParsesBarSingleP1Series) {
const QJsonArray rows = grRows();
ASSERT_EQ(rows.size(), 20);
auto bar = parseGrBar(rows);
// 类目 "#1".."#20"。
ASSERT_EQ(bar.categories.size(), 20u);
EXPECT_EQ(bar.categories.front().toStdString(), "#1");
EXPECT_EQ(bar.categories.back().toStdString(), "#20");
// 轴标题。
EXPECT_EQ(bar.xTitle.toStdString(), std::string("电极点"));
EXPECT_EQ(bar.yTitle.toStdString(), std::string("电阻(单位:欧姆)"));
// 单系列 P1p2Rg 全 null → 无 P2色 #5470c6。
ASSERT_EQ(bar.series.size(), 1u);
const auto& s = bar.series.front();
EXPECT_EQ(s.name.toStdString(), "P1");
EXPECT_EQ(s.color.toStdString(), "#5470c6");
ASSERT_EQ(s.values.size(), 20u);
EXPECT_DOUBLE_EQ(s.values.front(), 1385.0); // 首行 p1Rg
EXPECT_DOUBLE_EQ(s.values[1], 3444.0);
EXPECT_DOUBLE_EQ(s.values.back(), 971.0); // 第 20 行 p1Rg
}
TEST(GrDto, AddsP2SeriesWhenAnyNonNull) {
// 合成:含一行 p2Rg 非空 → 应建 P2 系列(分组柱)。
const char* json = R"([
{"electrodeId":1,"p1Rg":100,"p1RgStatus":"正常","p2Rg":null,"p2RgStatus":""},
{"electrodeId":2,"p1Rg":200,"p1RgStatus":"正常","p2Rg":55,"p2RgStatus":"正常"}
])";
const QJsonArray rows = QJsonDocument::fromJson(json).array();
auto bar = parseGrBar(rows);
ASSERT_EQ(bar.series.size(), 2u);
EXPECT_EQ(bar.series[0].name.toStdString(), "P1");
EXPECT_EQ(bar.series[1].name.toStdString(), "P2");
ASSERT_EQ(bar.series[1].values.size(), 2u);
EXPECT_DOUBLE_EQ(bar.series[1].values[1], 55.0);
}
TEST(GrDto, ParsesEmptyArrayToValidEmptyPayloads) {
// 空/畸形响应:空 QJsonArray → 有效但空的载荷,不崩。
const QJsonArray empty;
auto bar = parseGrBar(empty);
EXPECT_EQ(bar.categories.size(), 0u);
EXPECT_EQ(bar.series.size(), 1u); // P1 恒有(系列存在但无值)
EXPECT_EQ(bar.series.front().values.size(), 0u);
EXPECT_EQ(bar.xTitle.toStdString(), std::string("电极点"));
auto t = parseGrTable(empty);
EXPECT_EQ(t.columns.size(), 7u); // 7 列硬编码,恒有
EXPECT_EQ(t.rows.size(), 0u);
EXPECT_EQ(t.total, 0);
}
TEST(GrDto, ParsesTableSevenFixedColumns) {
const QJsonArray rows = grRows();
auto t = parseGrTable(rows);
// 7 固定列,无 Toggle 列gr 不复用 measurement 的开关列)。
ASSERT_EQ(t.columns.size(), 7u);
EXPECT_EQ(t.columns[0].title.toStdString(), "ID");
EXPECT_EQ(t.columns[1].title.toStdString(), std::string("日期"));
EXPECT_EQ(t.columns[2].title.toStdString(), std::string("时间"));
EXPECT_EQ(t.columns[3].title.toStdString(), "P1 Rg(Ω)");
EXPECT_EQ(t.columns[4].title.toStdString(), std::string("P1状态"));
EXPECT_EQ(t.columns[5].title.toStdString(), "P2 Rg(Ω)");
EXPECT_EQ(t.columns[6].title.toStdString(), std::string("P2状态"));
for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text);
// 20 行;首行 ID=1, p1Rg=1385, P1状态=正常p2Rg null → 空串。
ASSERT_EQ(t.rows.size(), 20u);
ASSERT_EQ(t.rows[0].size(), 7u);
EXPECT_EQ(t.rows[0][0].toStdString(), "1");
EXPECT_EQ(t.rows[0][3].toStdString(), "1385");
EXPECT_EQ(t.rows[0][4].toStdString(), std::string("正常"));
EXPECT_TRUE(t.rows[0][5].isEmpty()); // p2Rg null
EXPECT_EQ(t.total, 20);
}

27
tests/fixtures/dd/ert-gr-rows.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"code": 200,
"msg": "成功",
"__total": 40,
"data": [
{ "electrodeId": 1, "testDate": "5/26/12", "testTime": "18:25:15", "p1Rg": 1385, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 2, "testDate": "5/26/12", "testTime": "18:25:16", "p1Rg": 3444, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 3, "testDate": "5/26/12", "testTime": "18:25:17", "p1Rg": 737, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 4, "testDate": "5/26/12", "testTime": "18:25:18", "p1Rg": 813, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 5, "testDate": "5/26/12", "testTime": "18:25:19", "p1Rg": 577, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 6, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 652, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 7, "testDate": "5/26/12", "testTime": "18:25:20", "p1Rg": 696, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 8, "testDate": "5/26/12", "testTime": "18:25:21", "p1Rg": 523, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 9, "testDate": "5/26/12", "testTime": "18:25:22", "p1Rg": 587, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 10, "testDate": "5/26/12", "testTime": "18:25:23", "p1Rg": 968, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 11, "testDate": "5/26/12", "testTime": "18:25:24", "p1Rg": 1701, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 12, "testDate": "5/26/12", "testTime": "18:25:25", "p1Rg": 1951, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 13, "testDate": "5/26/12", "testTime": "18:25:26", "p1Rg": 1405, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 14, "testDate": "5/26/12", "testTime": "18:25:27", "p1Rg": 1166, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 15, "testDate": "5/26/12", "testTime": "18:25:28", "p1Rg": 1222, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 16, "testDate": "5/26/12", "testTime": "18:25:29", "p1Rg": 1043, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 17, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 1272, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 18, "testDate": "5/26/12", "testTime": "18:25:30", "p1Rg": 2150, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 19, "testDate": "5/26/12", "testTime": "18:25:31", "p1Rg": 1806, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" },
{ "electrodeId": 20, "testDate": "5/26/12", "testTime": "18:25:32", "p1Rg": 971, "p1RgStatus": "正常", "p2Rg": null, "p2RgStatus": "" }
]
}