#include "panels/columns/CategorySection.hpp" #include #include #include #include #include "panels/columns/DateRangeEdit.hpp" #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); // 数据类型标题行(spec §7):折叠箭头+标题(左) | 「+新增三维体」(右,仅反演类,在标题行而非筛选行)。 auto* headerRow = new QWidget(this); auto* hl = new QHBoxLayout(headerRow); hl->setContentsMargins(space::kSm, 0, space::kSm, 0); hl->setSpacing(space::kSm); header_ = new QToolButton(headerRow); header_->setText(QString::fromStdString(spec_.title)); header_->setCheckable(true); header_->setChecked(true); header_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); header_->setArrowType(Qt::DownArrow); header_->setAutoRaise(true); hl->addWidget(header_); hl->addStretch(1); if (spec_.canGenerateVolume) { auto* gen = new QToolButton(headerRow); gen->setText(QStringLiteral("+ 新增三维体")); gen->setAutoRaise(true); connect(gen, &QToolButton::clicked, this, [this] { emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds()); }); hl->addWidget(gen); } root->addWidget(headerRow); 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(); dateRange_ = new DateRangeEdit(body_); connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); }); filterRow->addWidget(dateRange_, 1); if (spec_.hasArrayTypeFilter) { arrayCombo_ = new QComboBox(body_); arrayCombo_->addItem(QStringLiteral("全部装置"), QString()); connect(arrayCombo_, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { rebuildList(); }); filterRow->addWidget(arrayCombo_); } body->addLayout(filterRow); // 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。 list_ = new QTreeWidget(body_); list_->setHeaderHidden(true); list_->setRootIsDecorated(true); list_->setIndentation(14); // 紧凑父子缩进(默认 20 太宽) 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()); }); if (spec_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常) list_->setContextMenuPolicy(Qt::CustomContextMenu); connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu); } 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()); if (dict_) { const auto& en = dict_->arrayTypeEnum(); // 全局装置枚举 itemValue→中文 QSet seen; // 列出当前数据里出现过、且命中枚举的装置值 → 中文名。 for (const auto& r : rows_) { for (const auto& kv : r.properties) { const auto it = en.find(kv.value); if (it == en.end()) continue; // 非装置类型值 const QString val = QString::fromStdString(kv.value); if (seen.contains(val)) continue; seen.insert(val); arrayCombo_->addItem(QString::fromStdString(it->second), val); } } } const int idx = arrayCombo_->findData(prev); // 尽量保留上次选择 arrayCombo_->setCurrentIndex(idx >= 0 ? idx : 0); } bool CategorySection::passesFilters(const DsRow& row) const { // 装置类型筛选(全局枚举;"全部"=空不筛):ds.properties 任一 value 命中选中 itemValue。 if (spec_.hasArrayTypeFilter && arrayCombo_) { const QString sel = arrayCombo_->currentData().toString(); if (!sel.isEmpty()) { bool hit = false; for (const auto& kv : row.properties) if (QString::fromStdString(kv.value) == sel) { hit = true; break; } if (!hit) return false; } } // 采集时间范围(collectTime 经 dict 取;缺则回退 createTime;空范围不约束)。 const QDate from = dateRange_ ? dateRange_->from() : QDate(); const QDate to = dateRange_ ? dateRange_->to() : QDate(); if (from.isValid() || to.isValid()) { const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr; 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()) { if (from.isValid() && d < from) return false; if (to.isValid() && d > to) 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); // 从项目根的层级树:容器节点(结构,剪枝仅留有 ds 的路径)+ ds(挂 parentId 或 structParentId)。 std::vector display; if (!structure_.empty()) { std::map byId; for (const auto& n : structure_) byId[n.id] = &n; std::set keep; // 收集每个 ds 的结构归属向上的祖先链 for (const auto& d : filtered) { std::string p = d.structParentId; while (!p.empty() && byId.count(p) && !keep.count(p)) { keep.insert(p); p = byId[p]->parentId; } } for (const auto& n : structure_) if (keep.count(n.id)) { DsRow c; c.id = n.id; c.dsName = n.name; c.ddCode = "container"; c.parentId = n.parentId; display.push_back(std::move(c)); } } for (const auto& d : filtered) { DsRow x = d; if (x.parentId.empty()) x.parentId = x.structParentId; // 源 ds / 体 → 挂结构容器 display.push_back(std::move(x)); } { const QSignalBlocker block(list_); populateDatasetList(list_, display, /*append=*/false); for (QTreeWidgetItemIterator it(list_); *it; ++it) { // 容器节点(项目根/GS/TM)只作层级骨架——明确去掉复选框、不可勾选。 if ((*it)->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) { (*it)->setFlags((*it)->flags() & ~Qt::ItemIsUserCheckable); continue; } (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable); (*it)->setCheckState(0, Qt::Unchecked); } } list_->expandAll(); // 展开容器层级(项目根/GS/TM),让体/切片/异常可见 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()); } void CategorySection::showContextMenu(const QPoint& pos) { using geopro::render::interact::SliceAxis; QTreeWidgetItem* it = list_->itemAt(pos); if (!it) return; const QString id = it->data(0, kDsIdRole).toString(); const QString ddCode = it->data(0, kDsDdCodeRole).toString(); if (id.isEmpty() || ddCode == QStringLiteral("container")) return; // 容器节点无操作菜单 const QString name = it->data(0, kDsNameRole).toString(); QMenu menu(this); menu.addAction(QStringLiteral("详情"), this, [this, id, ddCode, name] { emit detailRequested(id, ddCode, name); }); if (ddCode == QStringLiteral("dd_voxel")) { // 三维体 QMenu* sl = menu.addMenu(QStringLiteral("生成切片")); sl->addAction(QStringLiteral("上下"), this, [this] { emit sliceRequested(SliceAxis::UpDown); }); sl->addAction(QStringLiteral("前后"), this, [this] { emit sliceRequested(SliceAxis::FrontBack); }); sl->addAction(QStringLiteral("左右"), this, [this] { emit sliceRequested(SliceAxis::LeftRight); }); sl->addAction(QStringLiteral("任意"), this, [this] { emit sliceRequested(SliceAxis::Oblique); }); menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); }); } else if (ddCode == QStringLiteral("dd_slice")) { // 切片 menu.addAction(QStringLiteral("保存位姿"), this, [this, id] { emit sliceSaveRequested(id); }); menu.addAction(QStringLiteral("另存为…"), this, [this, id] { emit sliceSaveAsRequested(id); }); QMenu* ex = menu.addMenu(QStringLiteral("导出")); ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); }); ex->addAction(QStringLiteral("dat"), this, [this, id] { emit sliceExportDatRequested(id); }); menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); }); menu.addSeparator(); menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); }); } else if (ddCode == QStringLiteral("dd_anomaly")) { // 异常 menu.addAction(QStringLiteral("显示"), this, [this, id] { emit anomalyVisibilityChanged(id, true); }); menu.addAction(QStringLiteral("隐藏"), this, [this, id] { emit anomalyVisibilityChanged(id, false); }); menu.addSeparator(); menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); }); } menu.exec(list_->viewport()->mapToGlobal(pos)); } } // namespace geopro::app