feat(vtk): 段构造迁描述符;段头图标条由operations/filters驱动+筛选折叠

This commit is contained in:
gaozheng 2026-06-30 22:32:33 +08:00
parent b0da3c12fb
commit 11d7dd53b9
3 changed files with 84 additions and 84 deletions

View File

@ -35,13 +35,13 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶 col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
col->setSpacing(space::kSm); col->setSpacing(space::kSm);
for (const CategorySpec& spec : categoryConfigs()) { for (const auto& desc : geopro::data::categoryCatalog()) {
auto* sec = new CategorySection(spec, dict, content); auto* sec = new CategorySection(desc, dict, content);
sections_[spec.id] = sec; sections_[desc.id] = sec;
ordered_.push_back(sec); ordered_.push_back(sec);
connect(sec, &CategorySection::collapsedChanged, this, connect(sec, &CategorySection::collapsedChanged, this,
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch向上收 &CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch向上收
const std::string segId = spec.id; const std::string segId = desc.id;
connect(sec, &CategorySection::checkedDatasetsChanged, this, connect(sec, &CategorySection::checkedDatasetsChanged, this,
[this, segId](const QStringList& ids) { [this, segId](const QStringList& ids) {
checkedBySeg_[segId] = ids; checkedBySeg_[segId] = ids;
@ -50,8 +50,8 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
}); });
connect(sec, &CategorySection::generateVolumeRequested, this, connect(sec, &CategorySection::generateVolumeRequested, this,
&CategoryAnalysisTab::generateVolumeRequested); &CategoryAnalysisTab::generateVolumeRequested);
connect(sec, &CategorySection::radarImportRequested, this, // 注CategorySection 的「+导入雷达测线」入口已移除Task D1 迁至 TopBar。CategoryAnalysisTab
&CategoryAnalysisTab::radarImportRequested); // 自身仍保留 radarImportRequested 信号(main.cpp 的连接保持有效),待 D1 由 TopBar 新发射方接入。
connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested); connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested);
connect(sec, &CategorySection::deleteDatasetRequested, this, connect(sec, &CategorySection::deleteDatasetRequested, this,
&CategoryAnalysisTab::deleteDatasetRequested); &CategoryAnalysisTab::deleteDatasetRequested);

View File

@ -20,6 +20,7 @@
#include "Theme.hpp" #include "Theme.hpp"
#include "panels/DatasetListPanel.hpp" #include "panels/DatasetListPanel.hpp"
#include "panels/columns/SectionIconBar.hpp"
#include "repo/DatasetFieldDictionary.hpp" #include "repo/DatasetFieldDictionary.hpp"
namespace geopro::app { namespace geopro::app {
@ -27,14 +28,14 @@ namespace geopro::app {
using geopro::data::DsRow; using geopro::data::DsRow;
using geopro::data::DsTypeFields; using geopro::data::DsTypeFields;
CategorySection::CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict, CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc,
QWidget* parent) geopro::data::DatasetFieldDictionary* dict, QWidget* parent)
: QWidget(parent), spec_(spec), dict_(dict) { : QWidget(parent), desc_(desc), dict_(dict) {
auto* root = new QVBoxLayout(this); auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0); root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0); root->setSpacing(0);
// 数据类型段头可折叠规范§4.3/§6chevron + 标题(title 字号·半粗) 「+ 新增三维体」(右,仅反演类)。 // 数据类型段头可折叠规范§4.3/§6chevron + 标题(title 字号·半粗) 右侧响应式图标条(SectionIconBar)。
// 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。 // 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。
auto* headerRow = new QWidget(this); auto* headerRow = new QWidget(this);
headerRow->setObjectName(QStringLiteral("secHeader")); headerRow->setObjectName(QStringLiteral("secHeader"));
@ -58,57 +59,44 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
.arg(type::kWeightSemibold)); .arg(type::kWeightSemibold));
auto syncHeader = [this] { auto syncHeader = [this] {
header_->setText((header_->isChecked() ? QStringLiteral("") : QStringLiteral("")) header_->setText((header_->isChecked() ? QStringLiteral("") : QStringLiteral(""))
+ QString::fromStdString(spec_.title)); + QString::fromStdString(desc_.title));
}; };
syncHeader(); syncHeader();
hl->addWidget(header_); hl->addWidget(header_);
hl->addStretch(1); hl->addStretch(1);
if (spec_.canGenerateVolume) { // 段头图标条:遍历 desc_.operations经一处 OpKind→IconAction 映射装配spec §6
auto* gen = new QToolButton(headerRow); // glyph 键须命中 SectionIconBar::glyphFromKey 已识别集z 值无专用键,复用 collapse(竖向双箭头)。
gen->setText(QStringLiteral("+ 新增三维体")); iconBar_ = new SectionIconBar(headerRow);
gen->setCursor(Qt::PointingHandCursor); std::vector<IconAction> acts;
// 次级强调按钮(规范§6.7):描边 accent + accent 文字hover 浅强调底;非裸文字。 for (geopro::data::OpKind op : desc_.operations) {
applyTokenizedStyleSheet( switch (op) {
gen, QStringLiteral( case geopro::data::OpKind::GenerateVolume:
"QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;" // dsTypeCode 不再由段配置带(描述符无此字段)→ 发空串,接收方按 sourceIds 解析类型。
"color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}" acts.push_back({QStringLiteral("plus"), QStringLiteral("新增三维体"),
"QToolButton:hover{background:{{bg/selected}};}" [this] { emit generateVolumeRequested(QString(), checkedDsIds()); }, {}});
"QToolButton:pressed{background:{{bg/hover}};}") break;
.arg(radius::kSm) case geopro::data::OpKind::Filter:
.arg(scaledPx(space::kXxs)) acts.push_back({QStringLiteral("filter"), QStringLiteral("筛选"),
.arg(scaledPx(space::kMd)) [this] { if (filterRow_) filterRow_->setVisible(!filterRow_->isVisible()); },
.arg(scaledPx(type::kCaption))); {}});
connect(gen, &QToolButton::clicked, this, [this] { break;
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds()); case geopro::data::OpKind::PlaneZ:
}); acts.push_back({QStringLiteral("collapse"), QStringLiteral("z 值"), {},
hl->addWidget(gen); [this](QToolButton*) { // Task E3 建滑块 popup当前先发请求信号
} emit zSliderRequested(QString::fromStdString(desc_.id));
// 三维体段头「+ 导入雷达测线」(后端未就绪的本地过渡入口):弹出菜单选 规范化/Impulse。 }});
// 次级强调按钮样式同「+新增三维体」;点击发 radarImportRequested(impulse) → 上层走导入流程。 break;
if (spec_.id == "voxel") { case geopro::data::OpKind::Basemap:
auto* imp = new QToolButton(headerRow); acts.push_back({QStringLiteral("map"), QStringLiteral("底图"), {},
imp->setText(QStringLiteral("+ 导入雷达测线")); [this](QToolButton*) { // Task F2 建底图 popup当前先发请求信号
imp->setCursor(Qt::PointingHandCursor); emit basemapPopupRequested(QString::fromStdString(desc_.id));
imp->setPopupMode(QToolButton::InstantPopup); }});
applyTokenizedStyleSheet( break;
imp, QStringLiteral( }
"QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;"
"color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}"
"QToolButton::menu-indicator{image:none;width:0;}"
"QToolButton:hover{background:{{bg/selected}};}"
"QToolButton:pressed{background:{{bg/hover}};}")
.arg(radius::kSm)
.arg(scaledPx(space::kXxs))
.arg(scaledPx(space::kMd))
.arg(scaledPx(type::kCaption)));
auto* menu = new QMenu(imp);
menu->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this,
[this] { emit radarImportRequested(false); });
menu->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this,
[this] { emit radarImportRequested(true); });
imp->setMenu(menu);
hl->addWidget(imp);
} }
iconBar_->setMaxIcons(static_cast<int>(acts.size())); // 段头操作数即可见上限(够宽全显,不够收进「…」)
iconBar_->setActions(acts);
hl->addWidget(iconBar_);
root->addWidget(headerRow); root->addWidget(headerRow);
body_ = new QWidget(this); body_ = new QWidget(this);
@ -116,20 +104,27 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd); body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
body->setSpacing(space::kSm); body->setSpacing(space::kSm);
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。 // 筛选行(默认折叠,由段头 Filter 图标 toggle按 desc_.filters 装配 —— DateRange→采集时间范围(在前)
auto* filterRow = new QHBoxLayout(); // ArrayType→装置类型下拉(在后)。未列出的维度不建控件passesFilters/rebuildList 视该控件缺席=不筛该维。
filterRow->setSpacing(space::kSm); filterRow_ = new QWidget(body_);
dateRange_ = new DateRangeEdit(body_); auto* filterLay = new QHBoxLayout(filterRow_);
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); }); filterLay->setContentsMargins(0, 0, 0, 0);
filterRow->addWidget(dateRange_, 1); filterLay->setSpacing(space::kSm);
if (spec_.hasArrayTypeFilter) { for (geopro::data::FilterKind fk : desc_.filters) {
arrayCombo_ = new QComboBox(body_); if (fk == geopro::data::FilterKind::DateRange) {
arrayCombo_->addItem(QStringLiteral("全部装置"), QString()); dateRange_ = new DateRangeEdit(filterRow_);
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this, connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
[this](int) { rebuildList(); }); filterLay->addWidget(dateRange_, 1);
filterRow->addWidget(arrayCombo_); } else if (fk == geopro::data::FilterKind::ArrayType) {
arrayCombo_ = new QComboBox(filterRow_);
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int) { rebuildList(); });
filterLay->addWidget(arrayCombo_);
}
} }
body->addLayout(filterRow); filterRow_->hide(); // 默认折叠,点段头筛选图标展开
body->addWidget(filterRow_);
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。 // 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
list_ = new QTreeWidget(body_); list_ = new QTreeWidget(body_);
@ -152,7 +147,7 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
if (id.isEmpty()) return; if (id.isEmpty()) return;
emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString()); emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
}); });
if (spec_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常) if (desc_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
list_->setContextMenuPolicy(Qt::CustomContextMenu); list_->setContextMenuPolicy(Qt::CustomContextMenu);
connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu); connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu);
// 树选中切片/异常 → VTK 高亮联动(正向 list→VTK // 树选中切片/异常 → VTK 高亮联动(正向 list→VTK
@ -273,7 +268,7 @@ void CategorySection::clearAllBusy() {
} }
void CategorySection::refreshArrayCombo() { void CategorySection::refreshArrayCombo() {
if (!spec_.hasArrayTypeFilter || !arrayCombo_) return; if (!arrayCombo_) return; // 该段无装置筛选控件desc_.filters 未含 ArrayType→ 不刷
const QString prev = arrayCombo_->currentData().toString(); const QString prev = arrayCombo_->currentData().toString();
const QSignalBlocker block(arrayCombo_); const QSignalBlocker block(arrayCombo_);
arrayCombo_->clear(); arrayCombo_->clear();
@ -303,8 +298,8 @@ void CategorySection::refreshArrayCombo() {
} }
bool CategorySection::passesFilters(const DsRow& row) const { bool CategorySection::passesFilters(const DsRow& row) const {
// 类型筛选("全部"=空不筛):按 ds 自身类型值typeName回退 dsTypeCode命中选中项。 // 类型筛选("全部"=空不筛;无 arrayCombo_=该段不筛装置维度):按 ds 自身类型值typeName回退 dsTypeCode命中选中项。
if (spec_.hasArrayTypeFilter && arrayCombo_) { if (arrayCombo_) {
const QString sel = arrayCombo_->currentData().toString(); const QString sel = arrayCombo_->currentData().toString();
if (!sel.isEmpty()) { if (!sel.isEmpty()) {
const QString t = !row.typeName.empty() ? QString::fromStdString(row.typeName) const QString t = !row.typeName.empty() ? QString::fromStdString(row.typeName)
@ -316,7 +311,8 @@ bool CategorySection::passesFilters(const DsRow& row) const {
const QDate from = dateRange_ ? dateRange_->from() : QDate(); const QDate from = dateRange_ ? dateRange_->from() : QDate();
const QDate to = dateRange_ ? dateRange_->to() : QDate(); const QDate to = dateRange_ ? dateRange_->to() : QDate();
if (from.isValid() || to.isValid()) { if (from.isValid() || to.isValid()) {
const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr; // 采集时间字段定义按 ds 自身类型取(描述符无段级 dsTypeCode逐行查更准缺定义则回退 createTime
const DsTypeFields* f = dict_ ? dict_->fields(row.dsTypeCode) : nullptr;
std::string ts = f ? collectTimeOf(row, *f) : std::string(); std::string ts = f ? collectTimeOf(row, *f) : std::string();
if (ts.empty()) ts = row.createTime; if (ts.empty()) ts = row.createTime;
const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd")); const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd"));

View File

@ -3,7 +3,7 @@
#include <QWidget> #include <QWidget>
#include <vector> #include <vector>
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis #include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
#include "repo/CategoryConfig.hpp" #include "repo/CategoryDescriptor.hpp" // geopro::data::CategoryDescriptor / OpKind / FilterKind
#include "repo/RepoTypes.hpp" #include "repo/RepoTypes.hpp"
class QTreeWidget; class QTreeWidget;
@ -22,14 +22,16 @@ class DatasetFieldDictionary;
namespace geopro::app { namespace geopro::app {
class DateRangeEdit; class DateRangeEdit;
class SectionIconBar;
// 单个数据类型大类段spec §7段头标题/折叠 + 装置类型/日期筛选 + 「+新增三维体」)+ 段体(可勾选数据树)。 // 单个数据类型大类段spec §7段头标题/折叠 + 段头图标条)+ 段体(折叠筛选行 + 可勾选数据树)。
// 勾选数据行 = 渲染(帘面/体素/切片);段头生成按钮据当前勾选源发 generateVolumeRequested。 // 段头图标条由 descriptor.operations(OpKind) 驱动;筛选行由 descriptor.filters(FilterKind) 驱动、默认折叠。
// 勾选数据行 = 渲染(帘面/体素/切片/二维面GenerateVolume 图标据当前勾选源发 generateVolumeRequested。
class CategorySection : public QWidget { class CategorySection : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict, CategorySection(const geopro::data::CategoryDescriptor& desc,
QWidget* parent = nullptr); geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
// 对象树同源的扁平 GS/TM 节点段体容器分层用Task 12 接入真实结构,当前仅存储)。 // 对象树同源的扁平 GS/TM 节点段体容器分层用Task 12 接入真实结构,当前仅存储)。
void setStructure(const std::vector<geopro::data::StructNode>& nodes); void setStructure(const std::vector<geopro::data::StructNode>& nodes);
@ -41,7 +43,6 @@ public:
void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中 void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds异常显隐同步用 QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds异常显隐同步用
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉 void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
const CategorySpec& spec() const { return spec_; }
bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch实现"折叠向上收" bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch实现"折叠向上收"
void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见 void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见
QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用) QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用)
@ -51,8 +52,9 @@ public:
signals: signals:
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染 void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」 void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」(接收方按 sourceIds 解析类型)
void radarImportRequested(bool impulse); // 三维体段头「+导入雷达测线」(false=规范化 .head/.data, true=Impulse .iprb) void zSliderRequested(const QString& typeId); // PlaneZ 图标:弹 z 值滑块 popupTask E3 实现)
void basemapPopupRequested(const QString& typeId); // Basemap 图标:弹底图选择 popupTask F2 实现)
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情 void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常) void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常)
// ── 三维体段右键操作(迁自旧 Column3DAnalysis全接── // ── 三维体段右键操作(迁自旧 Column3DAnalysis全接──
@ -74,14 +76,16 @@ private:
QStringList checkedDsIds() const; QStringList checkedDsIds() const;
bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围 bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围
CategorySpec spec_; geopro::data::CategoryDescriptor desc_;
geopro::data::DatasetFieldDictionary* dict_ = nullptr; geopro::data::DatasetFieldDictionary* dict_ = nullptr;
std::vector<geopro::data::DsRow> rows_; std::vector<geopro::data::DsRow> rows_;
std::vector<geopro::data::StructNode> structure_; std::vector<geopro::data::StructNode> structure_;
QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头) QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头)
SectionIconBar* iconBar_ = nullptr; // 段头响应式图标条(由 desc_.operations 装配)
QWidget* body_ = nullptr; // 段体容器(折叠时隐藏) QWidget* body_ = nullptr; // 段体容器(折叠时隐藏)
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 hasArrayTypeFilter QWidget* filterRow_ = nullptr; // 筛选行容器(默认折叠,由 Filter 图标 toggle
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 desc_.filters 含 ArrayType
DateRangeEdit* dateRange_ = nullptr; // 采集时间范围筛选(起止双日历,可清空) DateRangeEdit* dateRange_ = nullptr; // 采集时间范围筛选(起止双日历,可清空)
QTreeWidget* list_ = nullptr; QTreeWidget* list_ = nullptr;
QTimer* spinTimer_ = nullptr; // 驱动 busy 行 spinner 旋转(有 busy 行时运行) QTimer* spinTimer_ = nullptr; // 驱动 busy 行 spinner 旋转(有 busy 行时运行)