diff --git a/src/app/main.cpp b/src/app/main.cpp index 139c7b4..6e989a7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -81,6 +81,7 @@ #include "ProjectListDialog.hpp" #include "WorkbenchNavController.hpp" #include "DatasetDetailController.hpp" +#include "panels/chart/ErtInversionStrategy.hpp" #include "api/ApiProjectRepository.hpp" #include "api/ApiDatasetRepository.hpp" #include "panels/ObjectTreePanel.hpp" @@ -860,7 +861,10 @@ int main(int argc, char* argv[]) // 数据详情仓储 + 控制器(接真实反演 API):同一共享会话 ApiClient。 geopro::data::ApiDatasetRepository datasetRepo(api); - geopro::controller::DatasetDetailController detailCtrl(datasetRepo); + // 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。 + geopro::controller::ChartStrategyRegistry chartRegistry; + chartRegistry.add(std::make_unique()); + geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); // ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其 // setCentralWidget/setMenuWidget/statusBar 承载工作台。 diff --git a/src/app/panels/chart/ErtInversionStrategy.hpp b/src/app/panels/chart/ErtInversionStrategy.hpp index 01d7f0d..1fde3ae 100644 --- a/src/app/panels/chart/ErtInversionStrategy.hpp +++ b/src/app/panels/chart/ErtInversionStrategy.hpp @@ -1,7 +1,9 @@ #pragma once -#include "panels/chart/IDatasetChartStrategy.hpp" +#include "IDatasetChartStrategy.hpp" // geopro::controller(geopro_controller PUBLIC include) namespace geopro::app { -struct ErtInversionStrategy : IDatasetChartStrategy { +// ERT 反演策略:散点(chart) + 网格等值面(grid) 两阶段。 +struct ErtInversionStrategy : controller::IDatasetChartStrategy { std::string ddCode() const override { return "dd_inversion_data"; } + bool hasGridPhase() const override { return true; } // 反演有网格数据阶段 }; } // namespace geopro::app diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp index 94355cb..c753e86 100644 --- a/src/controller/DatasetDetailController.cpp +++ b/src/controller/DatasetDetailController.cpp @@ -3,8 +3,9 @@ #include "api/DatasetLoadHandles.hpp" namespace geopro::controller { -DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent) - : QObject(parent), repo_(repo) {} +DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, + ChartStrategyRegistry& registry, QObject* parent) + : QObject(parent), repo_(repo), registry_(registry) {} DatasetDetailController::~DatasetDetailController() { if (chartLoad_) chartLoad_->abort(); // 退出契约:abort 在飞句柄,不依赖外部析构顺序兜底 @@ -12,7 +13,7 @@ DatasetDetailController::~DatasetDetailController() { } void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) { - if (ddCode != QLatin1String("dd_inversion_data")) { // 首版仅支持 ERT 反演 + if (!registry_.supports(ddCode.toStdString())) { // 未注册策略 → 优雅降级 emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览")); return; } @@ -40,7 +41,8 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd } void DatasetDetailController::loadGridData(const QString& dsId, const QString& ddCode) { - if (ddCode != QLatin1String("dd_inversion_data")) return; // 仅 ERT 反演有网格数据 + auto* strategy = registry_.find(ddCode.toStdString()); + if (!strategy || !strategy->hasGridPhase()) return; // 仅有网格阶段的类型加载网格数据 if (gridLoad_) gridLoad_->abort(); // abort-and-replace data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString()); gridLoad_ = load; diff --git a/src/controller/DatasetDetailController.hpp b/src/controller/DatasetDetailController.hpp index 5bb39af..d103e1d 100644 --- a/src/controller/DatasetDetailController.hpp +++ b/src/controller/DatasetDetailController.hpp @@ -6,6 +6,7 @@ #include "model/Field.hpp" #include "model/ColorScale.hpp" #include "model/Anomaly.hpp" +#include "IDatasetChartStrategy.hpp" namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; } namespace geopro::controller { @@ -32,7 +33,8 @@ public: std::vector anomalies; }; - explicit DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent = nullptr); + DatasetDetailController(data::IAsyncDatasetRepository& repo, + ChartStrategyRegistry& registry, QObject* parent = nullptr); ~DatasetDetailController() override; // 退出契约(spec §7):abort 在飞句柄,避免迟到信号打到已析构 this public slots: void openDataset(const QString& dsId, const QString& ddCode); @@ -46,6 +48,7 @@ signals: void loadFailed(const QString& dsId, const QString& message); private: data::IAsyncDatasetRepository& repo_; + ChartStrategyRegistry& registry_; QPointer chartLoad_; QPointer gridLoad_; }; diff --git a/src/app/panels/chart/IDatasetChartStrategy.hpp b/src/controller/IDatasetChartStrategy.hpp similarity index 71% rename from src/app/panels/chart/IDatasetChartStrategy.hpp rename to src/controller/IDatasetChartStrategy.hpp index 7fdc240..115d791 100644 --- a/src/app/panels/chart/IDatasetChartStrategy.hpp +++ b/src/controller/IDatasetChartStrategy.hpp @@ -2,14 +2,15 @@ #include #include #include -namespace geopro::app { - -class DatasetDetailPage; // 前置 +namespace geopro::controller { // dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。 struct IDatasetChartStrategy { virtual ~IDatasetChartStrategy() = default; virtual std::string ddCode() const = 0; + // 该类型是否有「网格数据」加载阶段(ERT 反演=true;纯散点/折线/图像类=false)。 + // 控制器据此决定是否允许 loadGridData,替代硬编码 ddCode 判断。 + virtual bool hasGridPhase() const = 0; }; class ChartStrategyRegistry { @@ -26,4 +27,4 @@ public: private: std::map> map_; }; -} // namespace geopro::app +} // namespace geopro::controller diff --git a/tests/app/test_chart_strategy_registry.cpp b/tests/app/test_chart_strategy_registry.cpp index ad6da01..d54c7ba 100644 --- a/tests/app/test_chart_strategy_registry.cpp +++ b/tests/app/test_chart_strategy_registry.cpp @@ -1,8 +1,11 @@ #include -#include "panels/chart/IDatasetChartStrategy.hpp" -using namespace geopro::app; +#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层) +using namespace geopro::controller; namespace { -struct Fake : IDatasetChartStrategy { std::string ddCode() const override { return "dd_inversion_data"; } }; +struct Fake : IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } + bool hasGridPhase() const override { return true; } +}; } TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) { ChartStrategyRegistry reg; @@ -12,3 +15,10 @@ TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) { EXPECT_FALSE(reg.supports("dd_unknown")); EXPECT_EQ(reg.find("dd_unknown"), nullptr); } +TEST(ChartStrategyRegistry, ReportsHasGridPhase) { + ChartStrategyRegistry reg; + reg.add(std::make_unique()); + auto* s = reg.find("dd_inversion_data"); + ASSERT_NE(s, nullptr); + EXPECT_TRUE(s->hasGridPhase()); +} diff --git a/tests/controller/test_dataset_detail_controller.cpp b/tests/controller/test_dataset_detail_controller.cpp index 4e21148..e996ddc 100644 --- a/tests/controller/test_dataset_detail_controller.cpp +++ b/tests/controller/test_dataset_detail_controller.cpp @@ -1,11 +1,29 @@ #include #include #include "DatasetDetailController.hpp" +#include "IDatasetChartStrategy.hpp" #include "repo/IAsyncDatasetRepository.hpp" #include "api/DatasetLoadHandles.hpp" using namespace geopro; namespace { +// 反演策略桩:散点 + 网格两阶段。 +struct InversionStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_inversion_data"; } + bool hasGridPhase() const override { return true; } +}; +// 无网格阶段策略桩:仅散点(如纯散点类型)。 +struct NoGridStrategy : controller::IDatasetChartStrategy { + std::string ddCode() const override { return "dd_scatter_only"; } + bool hasGridPhase() const override { return false; } +}; +// 注册了反演策略的注册表(多数用例复用)。 +controller::ChartStrategyRegistry makeInversionRegistry() { + controller::ChartStrategyRegistry reg; + reg.add(std::make_unique()); + return reg; +} + // 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。 struct StubChartLoad : data::ChartLoad { bool aborted = false; @@ -33,7 +51,8 @@ struct StubAsyncRepo : data::IAsyncDatasetRepository { TEST(DatasetDetailController, EmitsChartReadyOnDone) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); c.openDataset("ds1", "dd_inversion_data"); repo.lastChart->fireDone(); @@ -42,7 +61,8 @@ TEST(DatasetDetailController, EmitsChartReadyOnDone) { TEST(DatasetDetailController, EmitsLoadFailedOnFailed) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); c.openDataset("ds1", "dd_inversion_data"); repo.lastChart->fireFailed(); @@ -51,16 +71,39 @@ TEST(DatasetDetailController, EmitsLoadFailedOnFailed) { TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); // 注册了反演,但未注册 dd_other + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); c.openDataset("ds1", "dd_other"); EXPECT_EQ(spy.count(), 1); EXPECT_EQ(repo.lastChart, nullptr); // 未发起加载 } +// 空注册表 → 任意 ddCode 都不支持 → loadFailed,不发起加载。 +TEST(DatasetDetailController, EmptyRegistryFailsAnyType) { + StubAsyncRepo repo; + controller::ChartStrategyRegistry reg; // 空 + controller::DatasetDetailController c(repo, reg); + QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); + c.openDataset("ds1", "dd_inversion_data"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(repo.lastChart, nullptr); +} + +// 无网格阶段的策略 → loadGridData 不发起网格加载。 +TEST(DatasetDetailController, NoGridPhaseStrategySkipsGridLoad) { + StubAsyncRepo repo; + controller::ChartStrategyRegistry reg; + reg.add(std::make_unique()); + controller::DatasetDetailController c(repo, reg); + c.loadGridData("ds1", "dd_scatter_only"); + EXPECT_EQ(repo.lastGrid, nullptr); // hasGridPhase()==false → 未发起 +} + TEST(DatasetDetailController, AbortsPreviousOnReopen) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); c.openDataset("dsA", "dd_inversion_data"); StubChartLoad* a = repo.lastChart; c.openDataset("dsB", "dd_inversion_data"); // 替换 @@ -69,7 +112,8 @@ TEST(DatasetDetailController, AbortsPreviousOnReopen) { TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); c.openDataset("dsA", "dd_inversion_data"); StubChartLoad* a = repo.lastChart; @@ -83,7 +127,8 @@ TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) { TEST(DatasetDetailController, EmitsGridReadyOnDone) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady); c.loadGridData("ds1", "dd_inversion_data"); repo.lastGrid->fireDone(); @@ -92,7 +137,8 @@ TEST(DatasetDetailController, EmitsGridReadyOnDone) { TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); c.loadGridData("ds1", "dd_inversion_data"); repo.lastGrid->fireFailed(); @@ -101,7 +147,8 @@ TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) { TEST(DatasetDetailController, DropsLateGridSignalFromAbortedLoad) { StubAsyncRepo repo; - controller::DatasetDetailController c(repo); + auto reg = makeInversionRegistry(); + controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady); c.loadGridData("dsA", "dd_inversion_data"); StubGridLoad* a = repo.lastGrid;