diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 4f09f65..89f92ab 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(geopro_desktop WIN32 panels/chart/RawDataChartView.cpp panels/chart/GridDataChartView.cpp panels/chart/DataTableView.cpp + panels/chart/BarChartView.cpp panels/chart/DetailViewFactory.cpp panels/chart/ChartTheme.cpp panels/chart/ColorMapService.cpp diff --git a/src/app/main.cpp b/src/app/main.cpp index 2b09d7f..646b4f8 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -86,6 +86,7 @@ #include "DatasetDetailController.hpp" #include "panels/chart/ErtInversionStrategy.hpp" #include "panels/chart/MeasurementStrategy.hpp" +#include "panels/chart/GrMeasurementStrategy.hpp" #include "api/ApiProjectRepository.hpp" #include "api/ApiDatasetRepository.hpp" #include "panels/ObjectTreePanel.hpp" @@ -912,6 +913,7 @@ int main(int argc, char* argv[]) geopro::controller::ChartStrategyRegistry chartRegistry; 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/chart/BarChartView.cpp b/src/app/panels/chart/BarChartView.cpp new file mode 100644 index 0000000..2e872a3 --- /dev/null +++ b/src/app/panels/chart/BarChartView.cpp @@ -0,0 +1,215 @@ +#include "panels/chart/BarChartView.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 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(r); + if (i < 0 || i >= static_cast(labels_.size())) return QwtText(); + return labels_[static_cast(i)]; + } + +private: + std::vector 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()) return; // 坏/空 → 空态 + setData(payload.value()); +} + +void BarChartView::setData(const geopro::core::BarPayload& p) { + data_ = p; + clearSeries(); + + const int n = static_cast(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 samples; + samples.reserve(n); + for (int i = 0; i < n && i < static_cast(s.values.size()); ++i) + samples.append(QPointF(i, s.values[static_cast(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 titles; + for (size_t k = 0; k < p.series.size(); ++k) { + chart->setSymbol(static_cast(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 samples; + samples.reserve(n); + for (int i = 0; i < n; ++i) { + QVector set; + set.reserve(static_cast(p.series.size())); + for (const auto& s : p.series) + set.append(i < static_cast(s.values.size()) ? s.values[static_cast(i)] + : 0.0); + samples.append(QwtSetSample(i, set)); + } + chart->setSamples(samples); + chart->attach(plot_); + barItems_.push_back(chart); + } + + plot_->replot(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/BarChartView.hpp b/src/app/panels/chart/BarChartView.hpp new file mode 100644 index 0000000..c889eee --- /dev/null +++ b/src/app/panels/chart/BarChartView.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include + +#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"(自定义 QwtScaleDraw);x 轴标题「电极点」(底部居中); +// y 轴标题「电阻(单位:欧姆)」用左上水平 QLabel(ECharts 风格,对齐原版); +// 顶部图例(P1/P2);柱填充 #5470c6(P1,数据色,两主题一致)。 +// 背景/轴字/网格随主题(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 barItems_; // 当前挂载的柱状项(已 attach;卸载时 detach+delete) +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/DetailViewFactory.cpp b/src/app/panels/chart/DetailViewFactory.cpp index e5114af..7908b64 100644 --- a/src/app/panels/chart/DetailViewFactory.cpp +++ b/src/app/panels/chart/DetailViewFactory.cpp @@ -2,6 +2,7 @@ #include +#include "panels/chart/BarChartView.hpp" #include "panels/chart/DataTableView.hpp" #include "panels/chart/GridDataChartView.hpp" #include "panels/chart/RawDataChartView.hpp" @@ -17,9 +18,10 @@ std::unique_ptr makeDetailView(controller::ViewKind kind, QWidget* case controller::ViewKind::Table: return std::unique_ptr(new DataTableView(parent)); case controller::ViewKind::Bar: + return std::unique_ptr(new BarChartView(parent)); case controller::ViewKind::LineProfile: 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: unknown ViewKind"); diff --git a/src/app/panels/chart/GrMeasurementStrategy.hpp b/src/app/panels/chart/GrMeasurementStrategy.hpp new file mode 100644 index 0000000..0d21849 --- /dev/null +++ b/src/app/panels/chart/GrMeasurementStrategy.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include "IDatasetChartStrategy.hpp" // geopro::controller +namespace geopro::app { + +// ERT 接地电阻(measurement gr_data)策略:柱状图(同步)+ 列表(懒加载)两页签。 +// 两页签同一端点 measurement/gr/rows,loaderKey 不同(gr.bar 产 BarPayload / gr.rows 产 TablePayload)。 +struct GrMeasurementStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_ert_measurement_gr_data"; } + std::vector 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 diff --git a/src/core/model/detail/DetailPayloads.hpp b/src/core/model/detail/DetailPayloads.hpp index 616c81b..d769005 100644 --- a/src/core/model/detail/DetailPayloads.hpp +++ b/src/core/model/detail/DetailPayloads.hpp @@ -67,8 +67,25 @@ struct TablePayload { int total = 0; }; +// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。 +struct BarSeries { + QString name; + std::vector values; + QString color; +}; + +// 柱状图载荷(dd_ert_measurement_gr_data 接地电阻):类目(x 轴标签,如 "#1".."#40")+ +// 一个或多个系列(P1/P2,每系列一组按类目对齐的 y 值)+ 轴标题。 +struct BarPayload { + std::vector categories; + std::vector series; + QString xTitle; + QString yTitle; +}; + } // namespace geopro::core Q_DECLARE_METATYPE(geopro::core::ScatterPayload) Q_DECLARE_METATYPE(geopro::core::ContourPayload) Q_DECLARE_METATYPE(geopro::core::TablePayload) +Q_DECLARE_METATYPE(geopro::core::BarPayload) diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index 45594ec..0dcde7c 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(geopro_data STATIC dto/NavDto.cpp dto/DatasetChartDto.cpp dto/MeasurementDto.cpp + dto/GrMeasurementDto.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 ba7e71d..b6114c0 100644 --- a/src/data/api/ApiDatasetRepository.cpp +++ b/src/data/api/ApiDatasetRepository.cpp @@ -1,5 +1,6 @@ #include "api/ApiDatasetRepository.hpp" #include +#include #include #include #include @@ -8,6 +9,7 @@ #include "ApiBatch.hpp" #include "api/DatasetLoadHandles.hpp" #include "dto/DatasetChartDto.hpp" +#include "dto/GrMeasurementDto.hpp" #include "dto/MeasurementDto.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); } +// 接地电阻(gr):柱状图与列表同一端点 measurement/gr/rows(GET,query 参数)。 +// 响应 data 是 JSON 数组 → ApiResponseParse 包成 {value:[...]},取 r[0].data.value("value").toArray()。 +net::ApiBatch* grRowsBatch(net::ApiClient& api, const std::string& dsId) { + QList 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& r) { + return r[0].data.value(QStringLiteral("value")).toArray(); +} + } // namespace 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 == "ert_measurement.scatter") return makeMeasurementScatter(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); } @@ -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& r) { + return QVariant::fromValue(dto::parseGrBar(grDataArray(r))); + }); +} + +DetailLoad* ApiDatasetRepository::makeGrRows(const std::string& dsId) { + return new ApiDetailLoad(grRowsBatch(api_, dsId), [](const QList& r) { + return QVariant::fromValue(dto::parseGrTable(grDataArray(r))); + }); +} + } // namespace geopro::data diff --git a/src/data/api/ApiDatasetRepository.hpp b/src/data/api/ApiDatasetRepository.hpp index cb76a91..7dd3897 100644 --- a/src/data/api/ApiDatasetRepository.hpp +++ b/src/data/api/ApiDatasetRepository.hpp @@ -13,6 +13,8 @@ private: DetailLoad* makeInversionGrid(const std::string& dsId); DetailLoad* makeMeasurementScatter(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_; }; } // namespace geopro::data diff --git a/src/data/dto/GrMeasurementDto.cpp b/src/data/dto/GrMeasurementDto.cpp new file mode 100644 index 0000000..962b591 --- /dev/null +++ b/src/data/dto/GrMeasurementDto.cpp @@ -0,0 +1,87 @@ +#include "dto/GrMeasurementDto.hpp" + +#include +#include + +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(static_cast(d))) + return QString::number(static_cast(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 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(t.rows.size()); + return t; +} + +} // namespace geopro::data::dto diff --git a/src/data/dto/GrMeasurementDto.hpp b/src/data/dto/GrMeasurementDto.hpp new file mode 100644 index 0000000..61d3483 --- /dev/null +++ b/src/data/dto/GrMeasurementDto.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include "model/detail/DetailPayloads.hpp" + +namespace geopro::data::dto { + +// dd_ert_measurement_gr_data 接地电阻:两页签同一端点 measurement/gr/rows,data 是 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b5e9669..891a555 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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_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_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 0de0772..cebeef6 100644 --- a/tests/app/test_chart_strategy_registry.cpp +++ b/tests/app/test_chart_strategy_registry.cpp @@ -2,6 +2,7 @@ #include "DatasetDetailTab.hpp" #include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层) #include "panels/chart/MeasurementStrategy.hpp" +#include "panels/chart/GrMeasurementStrategy.hpp" using namespace geopro::controller; namespace { struct Fake : IDatasetChartStrategy { @@ -52,3 +53,20 @@ TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) { EXPECT_TRUE(tabs[1].lazy); 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"); + // 列表:Table,lazy。 + 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"); +} diff --git a/tests/data/test_async_repo_dispatch.cpp b/tests/data/test_async_repo_dispatch.cpp index 2da68e6..e9ab843 100644 --- a/tests/data/test_async_repo_dispatch.cpp +++ b/tests/data/test_async_repo_dispatch.cpp @@ -45,6 +45,14 @@ TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) { DetailLoad* measRows = repo.loadAsync("ert_measurement.rows", "ds1"); ASSERT_NE(measRows, nullptr); 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。 diff --git a/tests/data/test_gr_dto.cpp b/tests/data/test_gr_dto.cpp new file mode 100644 index 0000000..190099f --- /dev/null +++ b/tests/data/test_gr_dto.cpp @@ -0,0 +1,123 @@ +#include + +#include +#include + +#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("电阻(单位:欧姆)")); + + // 单系列 P1(p2Rg 全 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); +} diff --git a/tests/fixtures/dd/ert-gr-rows.json b/tests/fixtures/dd/ert-gr-rows.json new file mode 100644 index 0000000..cff1c41 --- /dev/null +++ b/tests/fixtures/dd/ert-gr-rows.json @@ -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": "" } + ] +}