feat(ui): CategorySection 类型段组件(段头装置/日期筛选+段体可勾选树+生成入口) + DatasetFieldDictionary 缓存类
This commit is contained in:
parent
40646f7d06
commit
30e990d967
|
|
@ -81,6 +81,7 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/columns/Column2DDataset.cpp
|
panels/columns/Column2DDataset.cpp
|
||||||
panels/columns/Column3DDataset.cpp
|
panels/columns/Column3DDataset.cpp
|
||||||
panels/columns/Column3DAnalysis.cpp
|
panels/columns/Column3DAnalysis.cpp
|
||||||
|
panels/columns/CategorySection.cpp
|
||||||
panels/columns/ColumnDrawer.cpp
|
panels/columns/ColumnDrawer.cpp
|
||||||
panels/AnomalyTablePanel.cpp
|
panels/AnomalyTablePanel.cpp
|
||||||
panels/LoadingOverlay.cpp
|
panels/LoadingOverlay.cpp
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -46,4 +46,15 @@ std::string arrayLabel(const DsTypeFields& f, const std::string& value) {
|
||||||
return it != f.arrayTypeLabels.end() ? it->second : value; // 缺失回退原值(spec §11)
|
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
|
} // namespace geopro::data
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,17 @@ std::string collectTimeOf(const DsRow& row, const DsTypeFields& f);
|
||||||
// 装置类型 value→中文:命中 arrayTypeLabels 取中文,否则回退原值(spec §11 字典源待坐实时的安全退路)。
|
// 装置类型 value→中文:命中 arrayTypeLabels 取中文,否则回退原值(spec §11 字典源待坐实时的安全退路)。
|
||||||
std::string arrayLabel(const DsTypeFields& f, const std::string& value);
|
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
|
} // namespace geopro::data
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,15 @@ TEST(DatasetFieldDictionary, ArrayLabelHitsAndFallsBackToRawValue) {
|
||||||
// spec §11:原始值不在 optionsObject 时回退显示原值(如实测 1429468249448449)。
|
// spec §11:原始值不在 optionsObject 时回退显示原值(如实测 1429468249448449)。
|
||||||
EXPECT_EQ(arrayLabel(f, "1429468249448449"), "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");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue