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
7 changed files with 92 additions and 23 deletions
Showing only changes of commit 0cb0ed8aa0 - Show all commits

View File

@ -81,6 +81,7 @@
#include "ProjectListDialog.hpp" #include "ProjectListDialog.hpp"
#include "WorkbenchNavController.hpp" #include "WorkbenchNavController.hpp"
#include "DatasetDetailController.hpp" #include "DatasetDetailController.hpp"
#include "panels/chart/ErtInversionStrategy.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"
@ -860,7 +861,10 @@ int main(int argc, char* argv[])
// 数据详情仓储 + 控制器(接真实反演 API同一共享会话 ApiClient。 // 数据详情仓储 + 控制器(接真实反演 API同一共享会话 ApiClient。
geopro::data::ApiDatasetRepository datasetRepo(api); geopro::data::ApiDatasetRepository datasetRepo(api);
geopro::controller::DatasetDetailController detailCtrl(datasetRepo); // 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
geopro::controller::ChartStrategyRegistry chartRegistry;
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其 // ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其
// setCentralWidget/setMenuWidget/statusBar 承载工作台。 // setCentralWidget/setMenuWidget/statusBar 承载工作台。

View File

@ -1,7 +1,9 @@
#pragma once #pragma once
#include "panels/chart/IDatasetChartStrategy.hpp" #include "IDatasetChartStrategy.hpp" // geopro::controllergeopro_controller PUBLIC include
namespace geopro::app { namespace geopro::app {
struct ErtInversionStrategy : IDatasetChartStrategy { // ERT 反演策略:散点(chart) + 网格等值面(grid) 两阶段。
struct ErtInversionStrategy : controller::IDatasetChartStrategy {
std::string ddCode() const override { return "dd_inversion_data"; } std::string ddCode() const override { return "dd_inversion_data"; }
bool hasGridPhase() const override { return true; } // 反演有网格数据阶段
}; };
} // namespace geopro::app } // namespace geopro::app

View File

@ -3,8 +3,9 @@
#include "api/DatasetLoadHandles.hpp" #include "api/DatasetLoadHandles.hpp"
namespace geopro::controller { namespace geopro::controller {
DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent) DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo,
: QObject(parent), repo_(repo) {} ChartStrategyRegistry& registry, QObject* parent)
: QObject(parent), repo_(repo), registry_(registry) {}
DatasetDetailController::~DatasetDetailController() { DatasetDetailController::~DatasetDetailController() {
if (chartLoad_) chartLoad_->abort(); // 退出契约abort 在飞句柄,不依赖外部析构顺序兜底 if (chartLoad_) chartLoad_->abort(); // 退出契约abort 在飞句柄,不依赖外部析构顺序兜底
@ -12,7 +13,7 @@ DatasetDetailController::~DatasetDetailController() {
} }
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) { 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("暂不支持该数据类型的预览")); emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
return; return;
} }
@ -40,7 +41,8 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
} }
void DatasetDetailController::loadGridData(const QString& dsId, const QString& ddCode) { 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 if (gridLoad_) gridLoad_->abort(); // abort-and-replace
data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString()); data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString());
gridLoad_ = load; gridLoad_ = load;

View File

@ -6,6 +6,7 @@
#include "model/Field.hpp" #include "model/Field.hpp"
#include "model/ColorScale.hpp" #include "model/ColorScale.hpp"
#include "model/Anomaly.hpp" #include "model/Anomaly.hpp"
#include "IDatasetChartStrategy.hpp"
namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; } namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; }
namespace geopro::controller { namespace geopro::controller {
@ -32,7 +33,8 @@ public:
std::vector<geopro::core::Anomaly> anomalies; std::vector<geopro::core::Anomaly> anomalies;
}; };
explicit DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent = nullptr); DatasetDetailController(data::IAsyncDatasetRepository& repo,
ChartStrategyRegistry& registry, QObject* parent = nullptr);
~DatasetDetailController() override; // 退出契约(spec §7)abort 在飞句柄,避免迟到信号打到已析构 this ~DatasetDetailController() override; // 退出契约(spec §7)abort 在飞句柄,避免迟到信号打到已析构 this
public slots: public slots:
void openDataset(const QString& dsId, const QString& ddCode); void openDataset(const QString& dsId, const QString& ddCode);
@ -46,6 +48,7 @@ signals:
void loadFailed(const QString& dsId, const QString& message); void loadFailed(const QString& dsId, const QString& message);
private: private:
data::IAsyncDatasetRepository& repo_; data::IAsyncDatasetRepository& repo_;
ChartStrategyRegistry& registry_;
QPointer<data::ChartLoad> chartLoad_; QPointer<data::ChartLoad> chartLoad_;
QPointer<data::GridLoad> gridLoad_; QPointer<data::GridLoad> gridLoad_;
}; };

View File

@ -2,14 +2,15 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <string> #include <string>
namespace geopro::app { namespace geopro::controller {
class DatasetDetailPage; // 前置
// dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。 // dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。
struct IDatasetChartStrategy { struct IDatasetChartStrategy {
virtual ~IDatasetChartStrategy() = default; virtual ~IDatasetChartStrategy() = default;
virtual std::string ddCode() const = 0; virtual std::string ddCode() const = 0;
// 该类型是否有「网格数据」加载阶段ERT 反演=true纯散点/折线/图像类=false
// 控制器据此决定是否允许 loadGridData替代硬编码 ddCode 判断。
virtual bool hasGridPhase() const = 0;
}; };
class ChartStrategyRegistry { class ChartStrategyRegistry {
@ -26,4 +27,4 @@ public:
private: private:
std::map<std::string, std::unique_ptr<IDatasetChartStrategy>> map_; std::map<std::string, std::unique_ptr<IDatasetChartStrategy>> map_;
}; };
} // namespace geopro::app } // namespace geopro::controller

View File

@ -1,8 +1,11 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "panels/chart/IDatasetChartStrategy.hpp" #include "IDatasetChartStrategy.hpp" // geopro::controller控制器层
using namespace geopro::app; using namespace geopro::controller;
namespace { 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) { TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) {
ChartStrategyRegistry reg; ChartStrategyRegistry reg;
@ -12,3 +15,10 @@ TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) {
EXPECT_FALSE(reg.supports("dd_unknown")); EXPECT_FALSE(reg.supports("dd_unknown"));
EXPECT_EQ(reg.find("dd_unknown"), nullptr); EXPECT_EQ(reg.find("dd_unknown"), nullptr);
} }
TEST(ChartStrategyRegistry, ReportsHasGridPhase) {
ChartStrategyRegistry reg;
reg.add(std::make_unique<Fake>());
auto* s = reg.find("dd_inversion_data");
ASSERT_NE(s, nullptr);
EXPECT_TRUE(s->hasGridPhase());
}

View File

@ -1,11 +1,29 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <QSignalSpy> #include <QSignalSpy>
#include "DatasetDetailController.hpp" #include "DatasetDetailController.hpp"
#include "IDatasetChartStrategy.hpp"
#include "repo/IAsyncDatasetRepository.hpp" #include "repo/IAsyncDatasetRepository.hpp"
#include "api/DatasetLoadHandles.hpp" #include "api/DatasetLoadHandles.hpp"
using namespace geopro; using namespace geopro;
namespace { 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<InversionStrategy>());
return reg;
}
// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。 // 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。
struct StubChartLoad : data::ChartLoad { struct StubChartLoad : data::ChartLoad {
bool aborted = false; bool aborted = false;
@ -33,7 +51,8 @@ struct StubAsyncRepo : data::IAsyncDatasetRepository {
TEST(DatasetDetailController, EmitsChartReadyOnDone) { TEST(DatasetDetailController, EmitsChartReadyOnDone) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
c.openDataset("ds1", "dd_inversion_data"); c.openDataset("ds1", "dd_inversion_data");
repo.lastChart->fireDone(); repo.lastChart->fireDone();
@ -42,7 +61,8 @@ TEST(DatasetDetailController, EmitsChartReadyOnDone) {
TEST(DatasetDetailController, EmitsLoadFailedOnFailed) { TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
c.openDataset("ds1", "dd_inversion_data"); c.openDataset("ds1", "dd_inversion_data");
repo.lastChart->fireFailed(); repo.lastChart->fireFailed();
@ -51,16 +71,39 @@ TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) { TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry(); // 注册了反演,但未注册 dd_other
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
c.openDataset("ds1", "dd_other"); c.openDataset("ds1", "dd_other");
EXPECT_EQ(spy.count(), 1); EXPECT_EQ(spy.count(), 1);
EXPECT_EQ(repo.lastChart, nullptr); // 未发起加载 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<NoGridStrategy>());
controller::DatasetDetailController c(repo, reg);
c.loadGridData("ds1", "dd_scatter_only");
EXPECT_EQ(repo.lastGrid, nullptr); // hasGridPhase()==false → 未发起
}
TEST(DatasetDetailController, AbortsPreviousOnReopen) { TEST(DatasetDetailController, AbortsPreviousOnReopen) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
c.openDataset("dsA", "dd_inversion_data"); c.openDataset("dsA", "dd_inversion_data");
StubChartLoad* a = repo.lastChart; StubChartLoad* a = repo.lastChart;
c.openDataset("dsB", "dd_inversion_data"); // 替换 c.openDataset("dsB", "dd_inversion_data"); // 替换
@ -69,7 +112,8 @@ TEST(DatasetDetailController, AbortsPreviousOnReopen) {
TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) { TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady); QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
c.openDataset("dsA", "dd_inversion_data"); c.openDataset("dsA", "dd_inversion_data");
StubChartLoad* a = repo.lastChart; StubChartLoad* a = repo.lastChart;
@ -83,7 +127,8 @@ TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) {
TEST(DatasetDetailController, EmitsGridReadyOnDone) { TEST(DatasetDetailController, EmitsGridReadyOnDone) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady); QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
c.loadGridData("ds1", "dd_inversion_data"); c.loadGridData("ds1", "dd_inversion_data");
repo.lastGrid->fireDone(); repo.lastGrid->fireDone();
@ -92,7 +137,8 @@ TEST(DatasetDetailController, EmitsGridReadyOnDone) {
TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) { TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed); QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
c.loadGridData("ds1", "dd_inversion_data"); c.loadGridData("ds1", "dd_inversion_data");
repo.lastGrid->fireFailed(); repo.lastGrid->fireFailed();
@ -101,7 +147,8 @@ TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) {
TEST(DatasetDetailController, DropsLateGridSignalFromAbortedLoad) { TEST(DatasetDetailController, DropsLateGridSignalFromAbortedLoad) {
StubAsyncRepo repo; StubAsyncRepo repo;
controller::DatasetDetailController c(repo); auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady); QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
c.loadGridData("dsA", "dd_inversion_data"); c.loadGridData("dsA", "dd_inversion_data");
StubGridLoad* a = repo.lastGrid; StubGridLoad* a = repo.lastGrid;