feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
6 changed files with 265 additions and 0 deletions
Showing only changes of commit 30e990d967 - Show all commits

View File

@ -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

View File

@ -0,0 +1,170 @@
#include "panels/columns/CategorySection.hpp"
#include <QComboBox>
#include <QDate>
#include <QDateEdit>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QToolButton>
#include <QTreeWidget>
#include <QTreeWidgetItemIterator>
#include <QVBoxLayout>
#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<int>(&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<geopro::data::StructNode>& nodes) {
structure_ = nodes; // 容器分层(项目根/GS/TM→ds在 Task 12 接入真实结构后据此构建。
}
void CategorySection::setDatasets(const std::vector<DsRow>& 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<QString> 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<DsRow> 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

View File

@ -0,0 +1,58 @@
#pragma once
#include <QStringList>
#include <QWidget>
#include <vector>
#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<geopro::data::StructNode>& nodes);
void setDatasets(const std::vector<geopro::data::DsRow>& 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<geopro::data::DsRow> rows_;
std::vector<geopro::data::StructNode> structure_;
QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头)
QWidget* body_ = nullptr; // 段体容器(折叠时隐藏)
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 hasArrayTypeFilter
QDateEdit* fromDate_ = nullptr; // 采集时间下限
QTreeWidget* list_ = nullptr;
};
} // namespace geopro::app

View File

@ -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

View File

@ -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<std::string, DsTypeFields> byType_;
};
} // namespace geopro::data

View File

@ -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");
}