diff --git a/src/app/main.cpp b/src/app/main.cpp index 7f51553..33d9bec 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -486,30 +486,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const auto slices = scene3dRepo->sliceRows(); const auto anomalies = scene3dRepo->anomalyRows(); // 电阻率/视/瞬变段=对象树反演 ds(splitByCategory 按 dsTypeCode 分;voxel 段下方单独喂完整树)。 + // 各段据结构建「从项目根的容器层级树」(项目根/GS/TM → ds,CategorySection 自剪枝)。 + drawer->analysisTab()->setStructure(*lastStructNodes); drawer->analysisTab()->setBuckets(geopro::app::splitByCategory(*lastSourceRows)); - // 三维体段=从项目根的完整层级树:项目根/GS/TM 容器节点 → 体(挂生成位置) → 切片(挂体) → 异常(挂体/切片)。 - // populateDatasetList 按 parentId 自动建树(spec §7/§8)。 + // 三维体段=体 + 切片 + 异常;容器层级由 CategorySection 据 structure + 各 ds 的 structParentId 自建。 std::vector voxelTree; - // 剪枝:只展示有三维体挂载的容器路径(从体的生成位置向上追溯到根),不带出无关 GS/TM。 - std::map byId; - for (const auto& n : *lastStructNodes) byId[n.id] = &n; - std::set keep; - for (const auto& v : vols) { - std::string p = v.parentId; // 体的生成位置(structParentId) - while (!p.empty() && byId.count(p) && !keep.count(p)) { - keep.insert(p); - p = byId[p]->parentId; // 向上到祖先(GS/项目根) - } - } - for (const auto& n : *lastStructNodes) { // 仅保留有体路径的容器节点(ddCode="container",无复选框) - if (!keep.count(n.id)) continue; - geopro::data::DsRow c; - c.id = n.id; - c.dsName = n.name; - c.ddCode = "container"; - c.parentId = n.parentId; - voxelTree.push_back(std::move(c)); - } for (const auto& v : vols) voxelTree.push_back(v); for (const auto& s : slices) voxelTree.push_back(s); for (const auto& a : anomalies) voxelTree.push_back(a); diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 9b8edbe..24ea496 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -39,7 +39,10 @@ public: QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override { const bool special = idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable); - return QSize(0, special ? 34 : 52); + if (special) return QSize(0, 34); + // 无副标题(容器节点 项目/GS/TM)用紧凑矮卡,避免标题下大片留白。 + const bool hasMeta = idx.data(Qt::DisplayRole).toString().contains(QLatin1Char('\n')); + return QSize(0, hasMeta ? 52 : 30); } void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override { @@ -81,8 +84,8 @@ public: geopro::app::tokenColor("accent/primary")); } - // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。 - int textLeftPad = 14; + // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。无复选框项(容器节点)左留白小。 + int textLeftPad = 6; const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable); if (checkable) { const int box = 16; @@ -105,17 +108,19 @@ public: meta = disp.mid(nl + 1); } - const QRect textR = r.adjusted(textLeftPad, 6, -12, -6); - // 标题 + const QRect textR = r.adjusted(textLeftPad, 4, -12, -4); QFont tf = opt.font; tf.setPixelSize(geopro::app::scaledPx(13)); p->setFont(tf); p->setPen(geopro::app::tokenColor("text/primary")); - const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2); - p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter, - p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width())); - // 元信息 - if (!meta.isEmpty()) { + if (meta.isEmpty()) { + // 无副标题(容器节点):标题垂直居中整卡,不留下半空白。 + p->drawText(textR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(title, Qt::ElideRight, textR.width())); + } else { + const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2); + p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width())); QFont mf = opt.font; mf.setPixelSize(geopro::app::scaledPx(11)); p->setFont(mf); diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index 6f8bd0e..fe4773b 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "panels/columns/DateRangeEdit.hpp" #include #include @@ -61,8 +63,11 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd); body->setSpacing(space::kSm); - // 筛选行:装置类型(仅 ERT 类)+ 采集时间段(起止;最小日期显示"不限",不再露 1752)。 + // 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 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()); @@ -70,16 +75,13 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset [this](int) { rebuildList(); }); filterRow->addWidget(arrayCombo_); } - filterRow->addWidget(new QLabel(QStringLiteral("采集"), body_)); - dateRange_ = new DateRangeEdit(body_); - connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); }); - filterRow->addWidget(dateRange_, 1); 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) { @@ -168,9 +170,39 @@ void CategorySection::rebuildList() { 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_, filtered, /*append=*/false); + populateDatasetList(list_, display, /*append=*/false); for (QTreeWidgetItemIterator it(list_); *it; ++it) { // 容器节点(项目根/GS/TM)只作层级骨架——明确去掉复选框、不可勾选。 if ((*it)->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) { diff --git a/src/data/api/Api3dRepository.cpp b/src/data/api/Api3dRepository.cpp index 7ea6777..c8a390b 100644 --- a/src/data/api/Api3dRepository.cpp +++ b/src/data/api/Api3dRepository.cpp @@ -1,5 +1,6 @@ #include "api/Api3dRepository.hpp" +#include #include #include #include @@ -103,7 +104,12 @@ bool Api3dRepository::isVolumeDataset(const std::string& dsId) const { std::string Api3dRepository::createVolume(VolumeBuildParams params, const std::string& name) { const std::string id = "vol-" + std::to_string(++volumeCounter_); - volumes_[id] = StoredVolume{std::move(params), name, std::nullopt}; + StoredVolume sv; + sv.params = std::move(params); + sv.name = name; + sv.createTime = + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); + volumes_[id] = std::move(sv); return id; } @@ -129,7 +135,8 @@ std::vector Api3dRepository::volumeRows() const { r.dsName = sv.name; r.ddCode = "dd_voxel"; r.typeName = "三维体"; - r.parentId = sv.request ? sv.request->structParentId : std::string(); // 挂生成位置(项目根/GS/TM) + r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置) + r.createTime = sv.createTime; rows.push_back(std::move(r)); } return rows; @@ -313,6 +320,7 @@ std::vector Api3dRepository::sliceRows() const { r.ddCode = "dd_slice"; r.typeName = "切片"; r.parentId = ss.spec.volumeDsId; // 树中挂在所属三维体下 + r.createTime = ss.createTime; rows.push_back(std::move(r)); } return rows; @@ -343,7 +351,9 @@ bool Api3dRepository::sliceSpec(const std::string& dsId, SliceSpec& out) const { void Api3dRepository::createSlice(const SliceSpec& spec, const std::string& name, std::function onOk, OnError /*onErr*/) { const std::string id = "slice-" + std::to_string(++sliceCounter_); - slices_[id] = StoredSlice{spec, name}; + slices_[id] = StoredSlice{ + spec, name, + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString()}; onOk(id); } diff --git a/src/data/api/Api3dRepository.hpp b/src/data/api/Api3dRepository.hpp index fee79d6..4b27033 100644 --- a/src/data/api/Api3dRepository.hpp +++ b/src/data/api/Api3dRepository.hpp @@ -129,6 +129,7 @@ private: core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶) std::optional pointCount; // 聚合散点数(finalizeVolume 时持久化,详情统计用) std::optional request; // 组装的真实请求体(createVolume(req) 路径填充) + std::string createTime; // 创建时刻(mock,列表副标题/详情用) }; std::map volumes_; // dsId → 体 int volumeCounter_ = 0; @@ -137,6 +138,7 @@ private: struct StoredSlice { SliceSpec spec; std::string name; + std::string createTime; // 创建时刻 }; std::map slices_; // dsId → 切片 int sliceCounter_ = 0;