diff --git a/src/app/panels/columns/CategoryAnalysisTab.cpp b/src/app/panels/columns/CategoryAnalysisTab.cpp index 2680571..a29a8a7 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.cpp +++ b/src/app/panels/columns/CategoryAnalysisTab.cpp @@ -1,6 +1,7 @@ #include "panels/columns/CategoryAnalysisTab.hpp" #include +#include #include #include #include @@ -10,6 +11,7 @@ #include "Theme.hpp" #include "panels/columns/CategorySection.hpp" +#include "repo/CategoryDescriptor.hpp" // categoryCatalog(含 trajectory 段) namespace geopro::app { @@ -44,6 +46,7 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d [this, segId](const QStringList& ids) { checkedBySeg_[segId] = ids; recomputeCheckedUnion(); + updatePlaceholderAndVisibility(); // 勾选/段内容变化后段「有无行」可能变 → 刷新显隐 }); connect(sec, &CategorySection::generateVolumeRequested, this, &CategoryAnalysisTab::generateVolumeRequested); @@ -72,6 +75,18 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d // 尾部弹簧(末项):默认 0;全部段折叠时由 relayoutSections 置 1,吸收余量把段头顶到顶部。 col->addStretch(0); scroll->setWidget(content); + + // 全空占位:所有段都无可渲染数据行时显示,与 scroll 同级、互斥显隐(由 updatePlaceholderAndVisibility 切换)。 + auto* placeholder = new QLabel(QStringLiteral("请在左侧对象树勾选测线 / 数据集"), this); + placeholder->setAlignment(Qt::AlignCenter); + placeholder->setWordWrap(true); + applyTokenizedStyleSheet(placeholder, + QStringLiteral("QLabel{color:{{text/tertiary}};padding:24px;}")); + outer->addWidget(placeholder, 0); + placeholder->hide(); + placeholder_ = placeholder; + + updatePlaceholderAndVisibility(); // 初始无数据 → 直接进入占位态 } void CategoryAnalysisTab::relayoutSections() { @@ -86,14 +101,29 @@ void CategoryAnalysisTab::relayoutSections() { } void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) { - const auto& cfg = categoryConfigs(); - for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) { + // splitByCategory 现按 categoryCatalog() 分桶(含 trajectory 第 5 桶,自然分发到轨迹段)。 + // 注:trajectory 段在 Task C2 才加入构造,当前 section("trajectory") 返回 nullptr → guard 跳过。 + const auto& cat = geopro::data::categoryCatalog(); + for (std::size_t i = 0; i < cat.size() && i < b.segments.size(); ++i) { // voxel(三维体) 段数据来自 mock voxelTree(体/切片/异常),由调用方单独 section("voxel")->setDatasets // 注入;splitByCategory 对它永远是空桶,若在此用空桶覆盖会先清掉其勾选(随后重填但勾选已丢) → // 表现为「创建切片/异常后体/切片选择被清空」(用户 issue 1)。故跳过 voxel,勿覆盖。 - if (cfg[i].id == "voxel") continue; - if (auto* sec = section(cfg[i].id)) sec->setDatasets(b.segments[i]); + if (cat[i].id == "voxel") continue; + if (auto* sec = section(cat[i].id)) sec->setDatasets(b.segments[i]); } + updatePlaceholderAndVisibility(); // 分发后据各段有无数据刷新显隐 + 占位 +} + +void CategoryAnalysisTab::updatePlaceholderAndVisibility() { + bool anyVisible = false; + for (auto* sec : ordered_) { + const bool has = sec->hasRenderableRows(); + sec->setVisible(has); + if (has) anyVisible = true; + } + if (scroll_) scroll_->setVisible(anyVisible); + if (placeholder_) placeholder_->setVisible(!anyVisible); + relayoutSections(); } void CategoryAnalysisTab::setStructure(const std::vector& nodes) { diff --git a/src/app/panels/columns/CategoryAnalysisTab.hpp b/src/app/panels/columns/CategoryAnalysisTab.hpp index eb00c5a..dfcba5c 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.hpp +++ b/src/app/panels/columns/CategoryAnalysisTab.hpp @@ -35,6 +35,7 @@ public: void setItemBusy(const QString& dsId, bool busy); // 复选框↔等待 spinner 切换 void clearAllBusy(); // 撤回所有 spinner(失败兜底) void scrollItemToTop(const QString& dsId); // 把某行尽量滚到面板顶部(新建三维体后定位) + void refreshVisibility() { updatePlaceholderAndVisibility(); } // 外部注入(如 voxel setDatasets)后刷新段显隐/占位 signals: void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选并集 @@ -61,12 +62,15 @@ private: // 据各段折叠态重排 stretch:折叠段=0(仅段头高)、展开段=1(吸收余量);全折叠时尾部弹簧吸收→段头顶到顶部。 // 仅在面板不产生滚动条(内容short于视口)时有可见效果——正是用户反馈的"折叠后停在原位中间"场景。 void relayoutSections(); + // 据各段是否有可渲染数据行 显隐段;全空时显示占位提示、隐藏滚动区。数据变化(分发/勾选/注入)后调用。 + void updatePlaceholderAndVisibility(); std::map sections_; std::vector ordered_; // 按 categoryConfigs 顺序(relayout 遍历用) QScrollArea* scroll_ = nullptr; // 外层滚动区(scrollItemToTop 定位用) QWidget* content_ = nullptr; // 滚动内容容器(坐标映射用) QVBoxLayout* col_ = nullptr; // 段堆叠布局(末项=尾部弹簧) + QWidget* placeholder_ = nullptr; // 全空时显示的占位提示(与 scroll_ 同级,互斥显隐) std::map checkedBySeg_; // 各段当前勾选(合并成并集) }; diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index ef66c92..632b4c3 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -189,6 +189,15 @@ QTreeWidgetItem* CategorySection::itemFor(const QString& dsId) const { return nullptr; } +bool CategorySection::hasRenderableRows() const { + if (!list_) return false; + // 数据行 = 非 container 的可勾选行;只有容器节点(分组)不算「有数据」。 + for (QTreeWidgetItemIterator it(list_); *it; ++it) + if ((*it)->data(0, kDsDdCodeRole).toString() != QStringLiteral("container")) + return true; + return false; +} + void CategorySection::setStructure(const std::vector& nodes) { structure_ = nodes; // 容器分层(项目根/GS/TM→ds)在 Task 12 接入真实结构后据此构建。 } diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp index 1ce1714..79a6744 100644 --- a/src/app/panels/columns/CategorySection.hpp +++ b/src/app/panels/columns/CategorySection.hpp @@ -46,6 +46,7 @@ public: void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见 QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用) QTreeWidgetItem* itemFor(const QString& dsId) const; // 按 dsId 取行(无则 nullptr) + bool hasRenderableRows() const; // 段体是否含可渲染数据行(非 container 容器节点),供单列动态显隐 signals: void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染