feat(ela): 数据集/文件/异常列表 → ElaListView + QStandardItemModel

- DatasetListPanel/AnomalyListPanel: populate 签名 QListWidget*→QStandardItemModel*,
  QListWidgetItem→QStandardItem(setData(value,role)/setCheckable/setIcon/setForeground)
- main.cpp: 3 列表 → ElaListView + QStandardItemModel; removeLoadMore/addLoadMore 改 model
  (rowCount/item/removeRow/appendRow); itemClicked→clicked(QModelIndex);
  anomaly itemChanged→model itemChanged; 加载更多/勾选显隐/点击 行为保持
- 注: 列表交互(异常显隐/加载更多/数据集点击)为活逻辑, 需运行验证
This commit is contained in:
gaozheng 2026-06-10 09:48:24 +08:00
parent 389a2da744
commit 8e7563c0f5
5 changed files with 88 additions and 78 deletions

View File

@ -34,8 +34,9 @@
#include <QFrame> #include <QFrame>
#include <QGraphicsOpacityEffect> #include <QGraphicsOpacityEffect>
#include <QLabel> #include <QLabel>
#include <QListWidget> #include <QModelIndex>
#include <QListWidgetItem> #include <QStandardItem>
#include <QStandardItemModel>
#include <QKeySequence> #include <QKeySequence>
#include <QProcess> #include <QProcess>
#include <QSettings> #include <QSettings>
@ -64,6 +65,7 @@
#include <ElaApplication.h> #include <ElaApplication.h>
#include <ElaCheckBox.h> #include <ElaCheckBox.h>
#include <ElaDef.h> #include <ElaDef.h>
#include <ElaListView.h>
#include <ElaTheme.h> #include <ElaTheme.h>
#include <ElaWindow.h> #include <ElaWindow.h>
@ -502,17 +504,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 // 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget(); auto* datasetTabs = new QTabWidget();
auto* datasetList = new QListWidget(); auto* datasetList = new ElaListView(); // Fluent 列表(自绘 hover/选中, 随主题)
// 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。随主题着色。 auto* datasetModel = new QStandardItemModel(datasetList);
const QString kListQss = QStringLiteral( datasetList->setModel(datasetModel);
"QListWidget{ background:#FFFFFF; border:none; outline:none; }"
"QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }"
"QListWidget::item:hover{ background:#EEF3FB; }"
"QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }");
geopro::app::applyThemedStyleSheet(datasetList, kListQss);
datasetTabs->addTab(datasetList, QStringLiteral("数据")); datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget(); auto* fileList = new ElaListView();
geopro::app::applyThemedStyleSheet(fileList, kListQss); // 与数据页签同款简洁分割 auto* fileModel = new QStandardItemModel(fileList);
fileList->setModel(fileModel);
datasetTabs->addTab(fileList, QStringLiteral("文件")); datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
auto* datasetBox = wrapWithHeader( auto* datasetBox = wrapWithHeader(
@ -525,8 +523,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea); dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 // 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* anomalyList = new QListWidget(); auto* anomalyList = new ElaListView();
anomalyList->setAlternatingRowColors(true); auto* anomalyModel = new QStandardItemModel(anomalyList);
anomalyList->setModel(anomalyModel);
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
objAttrLabel->setWordWrap(true); objAttrLabel->setWordWrap(true);
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
@ -686,7 +685,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}; };
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms, auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyModel, hiddenAnoms,
anomalyBadge](const QString& dsId, const QString& name) { anomalyBadge](const QString& dsId, const QString& name) {
if (dsId.isEmpty()) return; if (dsId.isEmpty()) return;
*currentDsId = dsId; *currentDsId = dsId;
@ -695,8 +694,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const auto anomalies = repo.loadAnomalies(dsId.toStdString()); const auto anomalies = repo.loadAnomalies(dsId.toStdString());
hiddenAnoms->clear(); hiddenAnoms->clear();
{ {
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged先屏蔽 const QSignalBlocker block(anomalyModel); // 重填触发 itemChanged先屏蔽
geopro::app::populateAnomalyList(anomalyList, anomalies); geopro::app::populateAnomalyList(anomalyModel, anomalies);
} }
// 异常列表 Tab 数量徽标。 // 异常列表 Tab 数量徽标。
if (anomalyBadge) { if (anomalyBadge) {
@ -722,14 +721,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
// 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。 // 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, QObject::connect(datasetList, &QAbstractItemView::clicked, datasetList,
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) { [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](const QModelIndex& idx) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) {
nav.loadMoreData(); nav.loadMoreData();
return; return;
} }
const QString name = const QString name = idx.data(Qt::DisplayRole).toString().section('\n', 0, 0);
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
detailRendererPtr->RemoveAllViewProps(); detailRendererPtr->RemoveAllViewProps();
detailRenderWindowPtr->Render(); detailRenderWindowPtr->Render();
propLabel->setText(QStringLiteral( propLabel->setText(QStringLiteral(
@ -737,8 +735,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}); });
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ── // ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList, QObject::connect(anomalyModel, &QStandardItemModel::itemChanged, anomalyList,
[hiddenAnoms, rebuildDetail](QListWidgetItem* item) { [hiddenAnoms, rebuildDetail](QStandardItem* item) {
const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt(); const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt();
if (item->checkState() == Qt::Checked) if (item->checkState() == Qt::Checked)
hiddenAnoms->erase(idx); hiddenAnoms->erase(idx);
@ -851,19 +849,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 控制器 ↔ UI 信号接线(导航壳)────────────────────────────────────── // ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
// "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。 // "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。
auto removeLoadMore = [](QListWidget* lw) { auto removeLoadMore = [](QStandardItemModel* mdl) {
if (lw->count() > 0 && const int n = mdl->rowCount();
lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) if (n > 0 && mdl->item(n - 1)->data(geopro::app::kDsLoadMoreRole).toBool())
delete lw->takeItem(lw->count() - 1); mdl->removeRow(n - 1);
}; };
auto addLoadMore = [](QListWidget* lw, int total) { auto addLoadMore = [](QStandardItemModel* mdl, int total) {
const int loaded = lw->count(); const int loaded = mdl->rowCount();
if (loaded < total) { if (loaded < total) {
auto* m = new QListWidgetItem( auto* it = new QStandardItem(QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total));
QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total), lw); it->setData(true, geopro::app::kDsLoadMoreRole);
m->setData(geopro::app::kDsLoadMoreRole, true); it->setTextAlignment(Qt::AlignCenter);
m->setTextAlignment(Qt::AlignCenter); it->setForeground(QColor("#2D6CB5"));
m->setForeground(QColor("#2D6CB5")); it->setEditable(false);
mdl->appendRow(it);
} }
return loaded; return loaded;
}; };
@ -902,42 +901,42 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
topBar->setProjects(list, cur, total > static_cast<int>(list.size())); topBar->setProjects(list, cur, total > static_cast<int>(list.size()));
}); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree, QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
[objectTree, datasetList, fileList, datasetTitle, datasetTabs]( [objectTree, datasetModel, fileModel, datasetTitle, datasetTabs](
const QString& projectName, const QString& projectName,
const std::vector<geopro::data::StructNode>& nodes) { const std::vector<geopro::data::StructNode>& nodes) {
objectTree->setStructure(projectName, nodes); objectTree->setStructure(projectName, nodes);
datasetList->clear(); datasetModel->clear();
fileList->clear(); fileModel->clear();
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(0, QStringLiteral("数据")); datasetTabs->setTabText(0, QStringLiteral("数据"));
datasetTabs->setTabText(1, QStringLiteral("文件")); datasetTabs->setTabText(1, QStringLiteral("文件"));
}); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs]( [removeLoadMore, addLoadMore, datasetModel, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total, const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) { bool append) {
removeLoadMore(datasetList); removeLoadMore(datasetModel);
geopro::app::populateDatasetList(datasetList, rows, append); geopro::app::populateDatasetList(datasetModel, rows, append);
const int loaded = addLoadMore(datasetList, total); const int loaded = addLoadMore(datasetModel, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText( datasetTabs->setTabText(
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("数据")); : QStringLiteral("数据"));
}); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList, QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
[removeLoadMore, addLoadMore, fileList, datasetTabs]( [removeLoadMore, addLoadMore, fileModel, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total, const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) { bool append) {
removeLoadMore(fileList); removeLoadMore(fileModel);
geopro::app::populateFileList(fileList, rows, append); geopro::app::populateFileList(fileModel, rows, append);
const int loaded = addLoadMore(fileList, total); const int loaded = addLoadMore(fileModel, total);
datasetTabs->setTabText( datasetTabs->setTabText(
1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total) 1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("文件")); : QStringLiteral("文件"));
}); });
QObject::connect(fileList, &QListWidget::itemClicked, fileList, QObject::connect(fileList, &QAbstractItemView::clicked, fileList,
[&nav](QListWidgetItem* item) { [&nav](const QModelIndex& idx) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles();
}); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree, QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
[objectTree, &window](const QString& stage, const QString& msg) { [objectTree, &window](const QString& stage, const QString& msg) {

View File

@ -5,9 +5,9 @@
#include <QColor> #include <QColor>
#include <QIcon> #include <QIcon>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPixmap> #include <QPixmap>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QString> #include <QString>
#include "model/ColorScale.hpp" #include "model/ColorScale.hpp"
@ -54,10 +54,10 @@ QPixmap swatch(const std::string& colorStr)
} // namespace } // namespace
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies) void populateAnomalyList(QStandardItemModel* model, const std::vector<geopro::core::Anomaly>& anomalies)
{ {
if (!list) return; if (!model) return;
list->clear(); model->clear();
for (std::size_t i = 0; i < anomalies.size(); ++i) { for (std::size_t i = 0; i < anomalies.size(); ++i) {
const auto& a = anomalies[i]; const auto& a = anomalies[i];
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name); const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
@ -66,10 +66,12 @@ void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anom
if (!type.isEmpty()) text += QStringLiteral("%1").arg(type); if (!type.isEmpty()) text += QStringLiteral("%1").arg(type);
text += QStringLiteral("\n%1").arg(summarize(a)); text += QStringLiteral("\n%1").arg(summarize(a));
auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list); auto* item = new QStandardItem(QIcon(swatch(a.lineColor)), text);
item->setData(kAnomalyIndexRole, static_cast<int>(i)); item->setEditable(false);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setData(static_cast<int>(i), kAnomalyIndexRole);
item->setCheckable(true);
item->setCheckState(Qt::Checked); // 默认显示 item->setCheckState(Qt::Checked); // 默认显示
model->appendRow(item);
} }
} }

View File

@ -3,7 +3,7 @@
#include "model/Anomaly.hpp" #include "model/Anomaly.hpp"
class QListWidget; class QStandardItemModel;
namespace geopro::app { namespace geopro::app {
@ -14,6 +14,6 @@ constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole
// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。 // 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。
// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。 // 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。
// 清空旧条目后重填。 // 清空旧条目后重填。
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies); void populateAnomalyList(QStandardItemModel* model, const std::vector<geopro::core::Anomaly>& anomalies);
} // namespace geopro::app } // namespace geopro::app

View File

@ -1,8 +1,8 @@
#include "panels/DatasetListPanel.hpp" #include "panels/DatasetListPanel.hpp"
#include <QColor> #include <QColor>
#include <QListWidget> #include <QStandardItem>
#include <QListWidgetItem> #include <QStandardItemModel>
#include <QString> #include <QString>
namespace geopro::app { namespace geopro::app {
@ -16,29 +16,34 @@ QString humanSize(long long b) {
} }
} // namespace } // namespace
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) { void populateDatasetList(QStandardItemModel* model, const std::vector<geopro::data::DsRow>& rows,
if (!list) return; bool append) {
if (!append) list->clear(); if (!model) return;
if (!append) model->clear();
for (const auto& d : rows) { for (const auto& d : rows) {
QString text = QString::fromStdString(d.dsName); QString text = QString::fromStdString(d.dsName);
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
if (!d.typeName.empty()) if (!d.typeName.empty())
sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型
if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub);
auto* item = new QListWidgetItem(text, list); auto* item = new QStandardItem(text);
item->setData(kDsIdRole, QString::fromStdString(d.id)); item->setEditable(false);
item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode)); item->setData(QString::fromStdString(d.id), kDsIdRole);
item->setData(QString::fromStdString(d.ddCode), kDsDdTypeRole);
model->appendRow(item);
} }
} }
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) { void populateFileList(QStandardItemModel* model, const std::vector<geopro::data::DsRow>& rows,
if (!list) return; bool append) {
if (!append) list->clear(); if (!model) return;
if (!append) model->clear();
if (!append && rows.empty()) { if (!append && rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); auto* hint = new QStandardItem(QStringLiteral("(暂无文件)"));
hint->setFlags(Qt::NoItemFlags); hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6")); hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter); hint->setTextAlignment(Qt::AlignCenter);
model->appendRow(hint);
return; return;
} }
for (const auto& d : rows) { for (const auto& d : rows) {
@ -47,9 +52,11 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小 sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小
const QString text = fname + QStringLiteral("\n%1").arg(sub); const QString text = fname + QStringLiteral("\n%1").arg(sub);
auto* item = new QListWidgetItem(text, list); auto* item = new QStandardItem(text);
item->setData(kDsIdRole, QString::fromStdString(d.id)); item->setEditable(false);
item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl)); item->setData(QString::fromStdString(d.id), kDsIdRole);
item->setData(QString::fromStdString(d.fileUrl), kDsFileUrlRole);
model->appendRow(item);
} }
} }

View File

@ -3,7 +3,7 @@
#include "repo/RepoTypes.hpp" #include "repo/RepoTypes.hpp"
class QListWidget; class QStandardItemModel;
namespace geopro::app { namespace geopro::app {
@ -13,9 +13,11 @@ constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1
constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2文件下载 url备用 constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2文件下载 url备用
constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行 constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行
// 数据页签:每条 = dsName +类型名UserRole 存 dsId、+1 存 ddCode。 // 数据页签:每条 = dsName +类型名UserRole 存 dsId、+1 存 ddCode。填入标准 model。
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append); void populateDatasetList(QStandardItemModel* model, const std::vector<geopro::data::DsRow>& rows,
bool append);
// 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位。 // 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位。
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append); void populateFileList(QStandardItemModel* model, const std::vector<geopro::data::DsRow>& rows,
bool append);
} // namespace geopro::app } // namespace geopro::app