diff --git a/src/app/main.cpp b/src/app/main.cpp index fc89bf7..a7b0c30 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1061,7 +1061,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QString dsId = item->data(0, geopro::app::kDsIdRole).toString(); const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString(); const QString dsName = item->data(0, geopro::app::kDsNameRole).toString(); - if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName); + // tmObjectId(白化 structParentId)从行读出透传,使白化模板列表非空。 + const QString tmObjectId = + item->data(0, geopro::app::kDsTmObjectIdRole).toString(); + if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId); }); // ── 控制器信号 → 详情面板(tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ── @@ -1468,10 +1471,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (dsId.isEmpty()) return; const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString(); const QString dsName = item->data(0, geopro::app::kDsNameRole).toString(); + // tmObjectId(白化 structParentId)从行读出透传,使白化模板列表非空。 + const QString tmObjectId = item->data(0, geopro::app::kDsTmObjectIdRole).toString(); QMenu menu(datasetList); menu.addAction(QStringLiteral("数据集详情"), datasetList, - [&detailCtrl, dsId, ddCode, dsName]() { - detailCtrl.openDataset(dsId, ddCode, dsName); + [&detailCtrl, dsId, ddCode, dsName, tmObjectId]() { + detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId); }); menu.addAction(QStringLiteral("属性"), datasetList, [&nav, dsId]() { nav.selectDataset(dsId); // 只读元字段 @@ -1636,10 +1641,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, [removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs]( - const QString&, const std::vector& rows, int total, - bool append) { + const QString& tmObjectId, const std::vector& rows, + int total, bool append) { removeTreeLoadMore(datasetList); - geopro::app::populateDatasetList(datasetList, rows, append); + // tmObjectId(本批所属 TM 对象 id)存入每项 → 白化对话框透传用(structParentId)。 + geopro::app::populateDatasetList(datasetList, rows, append, tmObjectId); const int loaded = addTreeLoadMore(datasetList, total); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集")); datasetTabs->setTabText( diff --git a/src/app/panels/DatasetDetailPage.cpp b/src/app/panels/DatasetDetailPage.cpp index f3388f4..0cc7f9b 100644 --- a/src/app/panels/DatasetDetailPage.cpp +++ b/src/app/panels/DatasetDetailPage.cpp @@ -50,7 +50,8 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const // 仓储与 projectId 回调透传给工厂(FilledContour 用色阶模板仓储;Scatter 用反演命令仓储)。 // dsIdGetter 用本页 dsId_(此处已赋值),随项目/数据集稳定。 auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_, - [this] { return dsId_; }); // 抛出由调用栈兜底(GuardedApplication) + [this] { return dsId_; }, + [this] { return tmObjectId_; }); // 抛出由调用栈兜底(GuardedApplication) IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期 views_[i] = raw; // lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。 diff --git a/src/app/panels/DatasetDetailPage.hpp b/src/app/panels/DatasetDetailPage.hpp index 1e9680a..a80564e 100644 --- a/src/app/panels/DatasetDetailPage.hpp +++ b/src/app/panels/DatasetDetailPage.hpp @@ -32,6 +32,9 @@ public: // dsId 用本页 dsId_(build 内构造 dsIdGetter,此时 dsId_ 已赋值);projectId 复用上面的 getter。 void setCommandRepo(geopro::data::IDatasetCommandRepository* repo); + // 所属 TM 对象 id(=白化 structParentId)注入(须在 build 前设置 → tmObjectIdGetter 透传给视图)。 + void setTmObjectId(const QString& tmObjectId) { tmObjectId_ = tmObjectId; } + // 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。 void build(const QString& dsId, const QString& ddCode, const QString& dsName, const std::vector& tabs); @@ -57,6 +60,7 @@ private: QString dsId_; QString ddCode_; QString dsName_; + QString tmObjectId_; // 所属 TM 对象 id(白化 structParentId),经 tmObjectIdGetter 透传给视图 std::vector tabs_; // 与 tabs_ 同序。每个 IDetailView 持有的 QWidget 经 build() 以 this 为父接管, // 生命周期由 Qt 父子树清理(不在此 delete);build() 仅调用一次(见其断言)。 diff --git a/src/app/panels/DatasetDetailPanel.cpp b/src/app/panels/DatasetDetailPanel.cpp index e2796c0..8eb2009 100644 --- a/src/app/panels/DatasetDetailPanel.cpp +++ b/src/app/panels/DatasetDetailPanel.cpp @@ -32,7 +32,7 @@ DatasetDetailPage* DatasetDetailPanel::pageFor(const QString& dsId) const { } void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddCode, - const QString& dsName, + const QString& dsName, const QString& tmObjectId, const std::vector& tabs) { auto* p = pageFor(dsId); if (!p) { @@ -40,6 +40,7 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC // 注入须在 build 前(build 内造视图时即透传给工厂)。 p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_); p->setCommandRepo(cmdRepo_); + p->setTmObjectId(tmObjectId); // 白化 structParentId(build 前设置 → 透传给视图) p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带 const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id) const int idx = addTab(p, title); diff --git a/src/app/panels/DatasetDetailPanel.hpp b/src/app/panels/DatasetDetailPanel.hpp index 66450c8..8babdd4 100644 --- a/src/app/panels/DatasetDetailPanel.hpp +++ b/src/app/panels/DatasetDetailPanel.hpp @@ -26,7 +26,9 @@ public: void setCommandRepo(geopro::data::IDatasetCommandRepository* repo); // 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。 + // tmObjectId:所属 TM 对象 id(白化 structParentId),build 前交给页 → 视图。 void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, + const QString& tmObjectId, const std::vector& tabs); void onTabReady(const QString& dsId, int tabIndex, const QVariant& payload); void onTabLoadStarted(const QString& dsId, int tabIndex); diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 1e44022..9b8edbe 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -160,7 +160,7 @@ public: namespace { // 建一条数据集树项(不挂载):列0 文本 = dsName +「创建时间 · 类型名」,data 存各角色。 -QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) { +QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d, const QString& tmObjectId) { QString text = QString::fromStdString(d.dsName); QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 if (!d.typeName.empty()) @@ -174,6 +174,7 @@ QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) { item->setData(0, kDsNameRole, QString::fromStdString(d.dsName)); item->setData(0, kDsTypeNameRole, QString::fromStdString(d.typeName)); item->setData(0, kDsCreateTimeRole, QString::fromStdString(d.createTime)); + item->setData(0, kDsTmObjectIdRole, tmObjectId); // 所属 TM 对象 id(白化 structParentId) // 单击 tip:显示数据集主要属性(名称 / 类型 / 创建时间),对齐菜单文档「tip显示ds的主要属性」。 QString tip = QStringLiteral("名称:%1").arg(QString::fromStdString(d.dsName)); if (!d.typeName.empty()) tip += QStringLiteral("\n类型:%1").arg(QString::fromStdString(d.typeName)); @@ -184,7 +185,8 @@ QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) { } } // namespace -void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append) { +void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append, + const QString& tmObjectId) { if (!tree) return; if (!append) tree->clear(); @@ -199,7 +201,7 @@ void populateDatasetList(QTreeWidget* tree, const std::vector batch; batch.reserve(rows.size()); for (const auto& d : rows) { - auto* item = makeDatasetItem(d); + auto* item = makeDatasetItem(d, tmObjectId); byId.insert(QString::fromStdString(d.id), item); batch.push_back(item); } diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index 1f437b5..2e3f8d6 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -22,11 +22,14 @@ constexpr int kDsDdCodeRole = 0x0104; // Qt::UserRole + 4(ddCode,双击详 constexpr int kDsNameRole = 0x0105; // Qt::UserRole + 5(dsName,详情页签标题用) constexpr int kDsTypeNameRole = 0x0106; // Qt::UserRole + 6(类型名,快速筛选用) constexpr int kDsCreateTimeRole = 0x0107; // Qt::UserRole + 7(创建时间,按日期筛选用) +constexpr int kDsTmObjectIdRole = 0x0108; // Qt::UserRole + 8(所属 TM 对象 id=白化 structParentId) // 数据页签:树形(按 DsRow.parentId 嵌套,源数据为根、派生数据挂其下,对齐原版 el-table 树)。 // 每项列0:文本 = dsName +「创建时间 · 类型名」;data(0,角色) 存 dsId/ddCode/dsName。 // append=true 时把新行挂到已加载的父节点下(分页)。 -void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append); +// tmObjectId:本批数据所属 TM 对象 id(=白化 structParentId),存入每项 kDsTmObjectIdRole;可空。 +void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append, + const QString& tmObjectId = QString()); // 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。 void populateFileList(QListWidget* list, const std::vector& rows, bool append); diff --git a/src/app/panels/chart/DetailViewFactory.cpp b/src/app/panels/chart/DetailViewFactory.cpp index f22e065..f110754 100644 --- a/src/app/panels/chart/DetailViewFactory.cpp +++ b/src/app/panels/chart/DetailViewFactory.cpp @@ -16,7 +16,8 @@ std::unique_ptr makeDetailView(controller::ViewKind kind, QWidget* geopro::data::IColorTemplateRepository* colorTplRepo, std::function projectIdGetter, geopro::data::IDatasetCommandRepository* cmdRepo, - std::function dsIdGetter) { + std::function dsIdGetter, + std::function tmObjectIdGetter) { switch (kind) { case controller::ViewKind::Scatter: { auto* raw = new RawDataChartView(parent); @@ -30,6 +31,8 @@ std::unique_ptr makeDetailView(controller::ViewKind kind, QWidget* grid->setColorTemplateRepo(colorTplRepo, projectIdGetter); // 注入反演命令仓储 + dsId/projectId 取值回调(网格化/白化/滤波/另存为按钮)。 grid->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter)); + // 注入 tmObjectId 取值回调(白化对话框模板列表用,= 数据集 structParentId)。 + grid->setTmObjectIdGetter(std::move(tmObjectIdGetter)); return std::unique_ptr(grid); } case controller::ViewKind::Table: { diff --git a/src/app/panels/chart/DetailViewFactory.hpp b/src/app/panels/chart/DetailViewFactory.hpp index aa11617..da9435e 100644 --- a/src/app/panels/chart/DetailViewFactory.hpp +++ b/src/app/panels/chart/DetailViewFactory.hpp @@ -22,11 +22,13 @@ class IDetailView; // 现阶段命中会抛 std::runtime_error(明确失败,而非静默空指针)。 // colorTplRepo/projectIdGetter:FilledContour 视图的色阶模板仓储注入(可空 → 编辑器后端按钮禁用)。 // cmdRepo/dsIdGetter:Scatter 视图(measurement)反演运算/生成视电阻率命令仓储注入(可空 → 按钮占位提示)。 +// tmObjectIdGetter:FilledContour 视图白化对话框所需 tmObjectId(=数据集 structParentId)取值回调(可空 → 模板列表空)。 std::unique_ptr makeDetailView( controller::ViewKind kind, QWidget* parent, geopro::data::IColorTemplateRepository* colorTplRepo = nullptr, std::function projectIdGetter = {}, geopro::data::IDatasetCommandRepository* cmdRepo = nullptr, - std::function dsIdGetter = {}); + std::function dsIdGetter = {}, + std::function tmObjectIdGetter = {}); } // namespace geopro::app diff --git a/src/app/panels/chart/GridDataChartView.cpp b/src/app/panels/chart/GridDataChartView.cpp index 55185bf..0fd677e 100644 --- a/src/app/panels/chart/GridDataChartView.cpp +++ b/src/app/panels/chart/GridDataChartView.cpp @@ -381,16 +381,10 @@ void GridDataChartView::openWhitening() { const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString(); if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; } // tmObjectId(白化模板列表用)= 当前数据集的 structParentId(对照原版 dsFileRow.structParentId)。 - // 客户端 open 链路未透传该字段 → 方案 A:懒拉 getDsObjectDetail(dsId) 取 structParentId, - // 再打开白化对话框。即便取不到(空串)也照常打开(与原版兜底一致,仅模板列表为空)。 - QPointer self(this); - cmdRepo_->getDsObjectDetail(dsId, [self, dsId, projectId](bool ok, QJsonObject data, QString) { - if (!self) return; - const QString tmObjectId = - ok ? data.value(QStringLiteral("structParentId")).toString() : QString(); - WhiteningDialog dlg(self->cmdRepo_, dsId, projectId, tmObjectId, self); - if (dlg.exec() == QDialog::Accepted) self->reloadGrid(); - }); + // 经 open 链路从数据集列表行透传至此(注入的 tmObjectIdGetter_)。空串也照常打开(仅模板列表为空)。 + const QString tmObjectId = tmObjectIdGetter_ ? tmObjectIdGetter_() : QString(); + WhiteningDialog dlg(cmdRepo_, dsId, projectId, tmObjectId, this); + if (dlg.exec() == QDialog::Accepted) reloadGrid(); } void GridDataChartView::openFilter() { diff --git a/src/app/panels/chart/GridDataChartView.hpp b/src/app/panels/chart/GridDataChartView.hpp index c8fae10..98b5ad3 100644 --- a/src/app/panels/chart/GridDataChartView.hpp +++ b/src/app/panels/chart/GridDataChartView.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -64,6 +65,11 @@ public: std::function dsIdGetter, std::function projectIdGetter); + // 注入 tmObjectId 取值回调(= 数据集 structParentId)。白化对话框模板列表用;空 → 模板列表为空。 + void setTmObjectIdGetter(std::function tmObjectIdGetter) { + tmObjectIdGetter_ = std::move(tmObjectIdGetter); + } + private: void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙) @@ -118,6 +124,9 @@ private: // 反演命令仓储 + dsId 取值回调(注入;空则处理类按钮占位提示)。 geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr; std::function dsIdGetter_; + + // tmObjectId 取值回调(= 数据集 structParentId)。白化对话框模板列表用;空 → 模板列表为空。 + std::function tmObjectIdGetter_; }; } // namespace geopro::app diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp index 5706aa3..347da44 100644 --- a/src/controller/DatasetDetailController.cpp +++ b/src/controller/DatasetDetailController.cpp @@ -19,9 +19,9 @@ DatasetDetailController::~DatasetDetailController() { } void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode, - const QString& dsName) { - qInfo("[detail] openDataset id=%s ddCode=%s name=%s", qUtf8Printable(dsId), - qUtf8Printable(ddCode), qUtf8Printable(dsName)); + const QString& dsName, const QString& tmObjectId) { + qInfo("[detail] openDataset id=%s ddCode=%s name=%s tm=%s", qUtf8Printable(dsId), + qUtf8Printable(ddCode), qUtf8Printable(dsName), qUtf8Printable(tmObjectId)); auto* s = registry_.find(ddCode.toStdString()); if (!s) { // 未注册策略 → 优雅降级 qWarning("[detail] 未注册策略 ddCode=%s → 降级提示", qUtf8Printable(ddCode)); @@ -29,7 +29,7 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd return; } const std::vector tabs = s->tabs(); - emit datasetOpened(dsId, ddCode, dsName, tabs); + emit datasetOpened(dsId, ddCode, dsName, tmObjectId, tabs); for (int i = 0; i < static_cast(tabs.size()); ++i) if (!tabs[static_cast(i)].lazy) loadTab(dsId, ddCode, i); } diff --git a/src/controller/DatasetDetailController.hpp b/src/controller/DatasetDetailController.hpp index c3f6aaf..ea7b06b 100644 --- a/src/controller/DatasetDetailController.hpp +++ b/src/controller/DatasetDetailController.hpp @@ -21,7 +21,9 @@ public: ~DatasetDetailController() override; // 退出契约(spec §7):abort 全部在飞句柄,避免迟到信号打到已析构 this public slots: // 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。 - void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString()); + // tmObjectId:数据集所属 TM 对象 id(=白化 structParentId),透传给详情页给白化对话框用;可空。 + void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString(), + const QString& tmObjectId = QString()); // 加载某页签(lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。 // 分页型页签(如 dd_grid 列表)首载用默认页(pageNo=1/pageSize=0 → 仓储解析默认每页条数)。 void loadTab(const QString& dsId, const QString& ddCode, int tabIndex); @@ -31,7 +33,7 @@ public slots: void focusDataset(const QString& dsId); signals: void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName, - const std::vector& tabs); + const QString& tmObjectId, const std::vector& tabs); void tabLoadStarted(const QString& dsId, int tabIndex); void tabReady(const QString& dsId, int tabIndex, const QVariant& payload); void loadFailed(const QString& dsId, const QString& message); diff --git a/tests/controller/test_dataset_detail_controller.cpp b/tests/controller/test_dataset_detail_controller.cpp index b175c96..cd68f5d 100644 --- a/tests/controller/test_dataset_detail_controller.cpp +++ b/tests/controller/test_dataset_detail_controller.cpp @@ -62,12 +62,13 @@ TEST(DatasetDetailController, OpenEmitsDatasetOpenedWithTabsAndDdCode) { auto reg = makeInversionRegistry(); controller::DatasetDetailController c(repo, reg); QSignalSpy spy(&c, &controller::DatasetDetailController::datasetOpened); - c.openDataset("ds1", "dd_inversion_data", "名称"); + c.openDataset("ds1", "dd_inversion_data", "名称", "tm1"); ASSERT_EQ(spy.count(), 1); const auto args = spy.takeFirst(); EXPECT_EQ(args.at(0).toString(), QStringLiteral("ds1")); EXPECT_EQ(args.at(1).toString(), QStringLiteral("dd_inversion_data")); // ddCode 透传 EXPECT_EQ(args.at(2).toString(), QStringLiteral("名称")); + EXPECT_EQ(args.at(3).toString(), QStringLiteral("tm1")); // tmObjectId 透传(白化 structParentId) } TEST(DatasetDetailController, OpenLoadsNonLazyTabsOnly) {