From 30e990d9672903a95100433b4b3817199ea212f4 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 24 Jun 2026 19:10:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20CategorySection=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=AE=B5=E7=BB=84=E4=BB=B6(=E6=AE=B5=E5=A4=B4?= =?UTF-8?q?=E8=A3=85=E7=BD=AE/=E6=97=A5=E6=9C=9F=E7=AD=9B=E9=80=89+?= =?UTF-8?q?=E6=AE=B5=E4=BD=93=E5=8F=AF=E5=8B=BE=E9=80=89=E6=A0=91+?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=85=A5=E5=8F=A3)=20+=20DatasetFieldDiction?= =?UTF-8?q?ary=20=E7=BC=93=E5=AD=98=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/CMakeLists.txt | 1 + src/app/panels/columns/CategorySection.cpp | 170 +++++++++++++++++++ src/app/panels/columns/CategorySection.hpp | 58 +++++++ src/data/repo/DatasetFieldDictionary.cpp | 11 ++ src/data/repo/DatasetFieldDictionary.hpp | 13 ++ tests/data/test_dataset_field_dictionary.cpp | 12 ++ 6 files changed, 265 insertions(+) create mode 100644 src/app/panels/columns/CategorySection.cpp create mode 100644 src/app/panels/columns/CategorySection.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 03304cb..4d43f4e 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -81,6 +81,7 @@ add_executable(geopro_desktop WIN32 panels/columns/Column2DDataset.cpp panels/columns/Column3DDataset.cpp panels/columns/Column3DAnalysis.cpp + panels/columns/CategorySection.cpp panels/columns/ColumnDrawer.cpp panels/AnomalyTablePanel.cpp panels/LoadingOverlay.cpp diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp new file mode 100644 index 0000000..bb457b4 --- /dev/null +++ b/src/app/panels/columns/CategorySection.cpp @@ -0,0 +1,170 @@ +#include "panels/columns/CategorySection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Theme.hpp" +#include "panels/DatasetListPanel.hpp" +#include "repo/DatasetFieldDictionary.hpp" + +namespace geopro::app { + +using geopro::data::DsRow; +using geopro::data::DsTypeFields; + +CategorySection::CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict, + QWidget* parent) + : QWidget(parent), spec_(spec), dict_(dict) { + auto* root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // 折叠头:标题 + 箭头(点击展开/收起段体)。 + header_ = new QToolButton(this); + header_->setText(QString::fromStdString(spec_.title)); + header_->setCheckable(true); + header_->setChecked(true); + header_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + header_->setArrowType(Qt::DownArrow); + header_->setAutoRaise(true); + header_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + root->addWidget(header_); + + body_ = new QWidget(this); + auto* body = new QVBoxLayout(body_); + body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd); + body->setSpacing(space::kSm); + + // 段头筛选行:装置类型(仅 ERT 类)+ 采集时间下限 +「+新增三维体」(仅反演类)。 + auto* filterRow = new QHBoxLayout(); + if (spec_.hasArrayTypeFilter) { + arrayCombo_ = new QComboBox(body_); + arrayCombo_->addItem(QStringLiteral("全部装置"), QString()); + connect(arrayCombo_, qOverload(&QComboBox::currentIndexChanged), this, + [this](int) { rebuildList(); }); + filterRow->addWidget(arrayCombo_); + } + fromDate_ = new QDateEdit(body_); + fromDate_->setCalendarPopup(true); + fromDate_->setDisplayFormat(QStringLiteral("yyyy-MM-dd")); + fromDate_->setDate(fromDate_->minimumDate()); // 最小日期=不限(哨兵) + fromDate_->setToolTip(QStringLiteral("采集时间下限(设为最小日期=不限)")); + connect(fromDate_, &QDateEdit::dateChanged, this, [this](const QDate&) { rebuildList(); }); + filterRow->addWidget(new QLabel(QStringLiteral("起始日期"), body_)); + filterRow->addWidget(fromDate_, 1); + if (spec_.canGenerateVolume) { + auto* gen = new QPushButton(QStringLiteral("+ 新增三维体"), body_); + connect(gen, &QPushButton::clicked, this, [this] { + emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds()); + }); + filterRow->addWidget(gen); + } + body->addLayout(filterRow); + + // 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。 + list_ = new QTreeWidget(body_); + list_->setHeaderHidden(true); + list_->setRootIsDecorated(true); + applyDatasetCardDelegate(list_); + connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { emitChecked(); }); + connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) { + const QString id = it->data(0, kDsIdRole).toString(); + if (id.isEmpty()) return; + emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString()); + }); + body->addWidget(list_, 1); + + root->addWidget(body_, 1); + + connect(header_, &QToolButton::toggled, this, [this](bool on) { + body_->setVisible(on); + header_->setArrowType(on ? Qt::DownArrow : Qt::RightArrow); + }); +} + +void CategorySection::setStructure(const std::vector& nodes) { + structure_ = nodes; // 容器分层(项目根/GS/TM→ds)在 Task 12 接入真实结构后据此构建。 +} + +void CategorySection::setDatasets(const std::vector& rows) { + rows_ = rows; + refreshArrayCombo(); + rebuildList(); +} + +void CategorySection::refreshArrayCombo() { + if (!spec_.hasArrayTypeFilter || !arrayCombo_) return; + const QString prev = arrayCombo_->currentData().toString(); + const QSignalBlocker block(arrayCombo_); + arrayCombo_->clear(); + arrayCombo_->addItem(QStringLiteral("全部装置"), QString()); + const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr; + if (f) { + QSet seen; + for (const auto& r : rows_) { + const QString val = QString::fromStdString(arrayValueOf(r, *f)); + if (val.isEmpty() || seen.contains(val)) continue; + seen.insert(val); + arrayCombo_->addItem(QString::fromStdString(arrayLabel(*f, val.toStdString())), val); + } + } + const int idx = arrayCombo_->findData(prev); // 尽量保留上次选择 + arrayCombo_->setCurrentIndex(idx >= 0 ? idx : 0); +} + +bool CategorySection::passesFilters(const DsRow& row) const { + const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr; + // 装置类型筛选(仅 ERT 类;"全部"=空 value 不筛)。 + if (spec_.hasArrayTypeFilter && arrayCombo_) { + const QString sel = arrayCombo_->currentData().toString(); + if (!sel.isEmpty()) { + const QString val = f ? QString::fromStdString(arrayValueOf(row, *f)) : QString(); + if (val != sel) return false; + } + } + // 采集时间下限(collectTime 回退 createTime;最小日期=不限)。 + if (fromDate_ && fromDate_->date() > fromDate_->minimumDate()) { + std::string ts = f ? collectTimeOf(row, *f) : std::string(); + if (ts.empty()) ts = row.createTime; + const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd")); + if (d.isValid() && d < fromDate_->date()) return false; + } + return true; +} + +void CategorySection::rebuildList() { + std::vector filtered; + filtered.reserve(rows_.size()); + for (const auto& r : rows_) + if (passesFilters(r)) filtered.push_back(r); + { + const QSignalBlocker block(list_); + populateDatasetList(list_, filtered, /*append=*/false); + for (QTreeWidgetItemIterator it(list_); *it; ++it) { + (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable); + (*it)->setCheckState(0, Qt::Unchecked); + } + } + emitChecked(); // 重建后必为空选,清掉上次渲染勾选 +} + +QStringList CategorySection::checkedDsIds() const { + QStringList ids; + for (QTreeWidgetItemIterator it(list_); *it; ++it) + if ((*it)->checkState(0) == Qt::Checked) ids << (*it)->data(0, kDsIdRole).toString(); + return ids; +} + +void CategorySection::emitChecked() { emit checkedDatasetsChanged(checkedDsIds()); } + +} // namespace geopro::app diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp new file mode 100644 index 0000000..bf2dbaf --- /dev/null +++ b/src/app/panels/columns/CategorySection.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include "repo/CategoryConfig.hpp" +#include "repo/RepoTypes.hpp" + +class QTreeWidget; +class QComboBox; +class QDateEdit; +class QLabel; +class QToolButton; +class QWidget; + +namespace geopro::data { +class DatasetFieldDictionary; +} + +namespace geopro::app { + +// 单个数据类型大类段(spec §7):段头(标题/折叠 + 装置类型/日期筛选 + 「+新增三维体」)+ 段体(可勾选数据树)。 +// 勾选数据行 = 渲染(帘面/体素/切片);段头生成按钮据当前勾选源发 generateVolumeRequested。 +class CategorySection : public QWidget { + Q_OBJECT +public: + CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict, + QWidget* parent = nullptr); + + // 对象树同源的扁平 GS/TM 节点(段体容器分层用;Task 12 接入真实结构,当前仅存储)。 + void setStructure(const std::vector& nodes); + void setDatasets(const std::vector& rows); + const CategorySpec& spec() const { return spec_; } + +signals: + void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染 + void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」 + void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击=详情 + +private: + void rebuildList(); // 据 rows_(经装置/日期筛选)重建段体树并复原勾选 + void refreshArrayCombo(); // 据当前 rows_ 重填装置类型下拉项(经字典 value→中文) + void emitChecked(); // 收集勾选 → checkedDatasetsChanged + QStringList checkedDsIds() const; + bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围 + + CategorySpec spec_; + geopro::data::DatasetFieldDictionary* dict_ = nullptr; + std::vector rows_; + std::vector structure_; + + QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头) + QWidget* body_ = nullptr; // 段体容器(折叠时隐藏) + QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 hasArrayTypeFilter) + QDateEdit* fromDate_ = nullptr; // 采集时间下限 + QTreeWidget* list_ = nullptr; +}; + +} // namespace geopro::app diff --git a/src/data/repo/DatasetFieldDictionary.cpp b/src/data/repo/DatasetFieldDictionary.cpp index 0f76c52..c215202 100644 --- a/src/data/repo/DatasetFieldDictionary.cpp +++ b/src/data/repo/DatasetFieldDictionary.cpp @@ -46,4 +46,15 @@ std::string arrayLabel(const DsTypeFields& f, const std::string& value) { return it != f.arrayTypeLabels.end() ? it->second : value; // 缺失回退原值(spec §11) } +void DatasetFieldDictionary::setFields(const std::string& dsTypeCode, DsTypeFields fields) { + byType_[dsTypeCode] = std::move(fields); +} +bool DatasetFieldDictionary::has(const std::string& dsTypeCode) const { + return byType_.find(dsTypeCode) != byType_.end(); +} +const DsTypeFields* DatasetFieldDictionary::fields(const std::string& dsTypeCode) const { + const auto it = byType_.find(dsTypeCode); + return it != byType_.end() ? &it->second : nullptr; +} + } // namespace geopro::data diff --git a/src/data/repo/DatasetFieldDictionary.hpp b/src/data/repo/DatasetFieldDictionary.hpp index b484d47..6efde52 100644 --- a/src/data/repo/DatasetFieldDictionary.hpp +++ b/src/data/repo/DatasetFieldDictionary.hpp @@ -24,4 +24,17 @@ std::string collectTimeOf(const DsRow& row, const DsTypeFields& f); // 装置类型 value→中文:命中 arrayTypeLabels 取中文,否则回退原值(spec §11 字典源待坐实时的安全退路)。 std::string arrayLabel(const DsTypeFields& f, const std::string& value); +// 按 dsTypeCode 缓存字段映射的服务(spec §10)。 +// 异步拉取 dynamicForm 的 IO 由 main 接线层负责(loadDatasetFormAsync → parseFieldMapping → setFields), +// 本类只持内存缓存,保持 data 层无网络依赖、可单测。 +class DatasetFieldDictionary { +public: + void setFields(const std::string& dsTypeCode, DsTypeFields fields); + bool has(const std::string& dsTypeCode) const; // 是否已缓存该 dsType 映射 + const DsTypeFields* fields(const std::string& dsTypeCode) const; // 未缓存=nullptr + +private: + std::map byType_; +}; + } // namespace geopro::data diff --git a/tests/data/test_dataset_field_dictionary.cpp b/tests/data/test_dataset_field_dictionary.cpp index fc800d5..f527706 100644 --- a/tests/data/test_dataset_field_dictionary.cpp +++ b/tests/data/test_dataset_field_dictionary.cpp @@ -46,3 +46,15 @@ TEST(DatasetFieldDictionary, ArrayLabelHitsAndFallsBackToRawValue) { // spec §11:原始值不在 optionsObject 时回退显示原值(如实测 1429468249448449)。 EXPECT_EQ(arrayLabel(f, "1429468249448449"), "1429468249448449"); } + +TEST(DatasetFieldDictionary, CacheSetHasFields) { + DatasetFieldDictionary dict; + EXPECT_FALSE(dict.has("ert")); + EXPECT_EQ(dict.fields("ert"), nullptr); + DsTypeFields f; + f.arrayTypeConfFieldId = "f_at"; + dict.setFields("ert", f); + EXPECT_TRUE(dict.has("ert")); + ASSERT_NE(dict.fields("ert"), nullptr); + EXPECT_EQ(dict.fields("ert")->arrayTypeConfFieldId, "f_at"); +}