feat(nav): 数据/文件页签接 data-page/file-page(按TM+classifyType拉取,文件页签展示名/大小)

This commit is contained in:
gaozheng 2026-06-09 14:54:32 +08:00
parent 5b18dc44ae
commit 839e5c3487
12 changed files with 129 additions and 67 deletions

View File

@ -344,13 +344,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
"QListWidget::item:hover{ background:#F5F8FD; }"
"QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"));
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget(); // M1 文件 tab 占位
{ // 空状态引导M1 暂无文件来源,给出说明而非空白面板(识别优先于回忆)。
auto* hint = new QListWidgetItem(QStringLiteral("M1 暂无关联文件)"), fileList);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter);
}
auto* fileList = new QListWidget();
fileList->setStyleSheet(datasetList->styleSheet()); // 与数据页签同款简洁分割
datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
auto* datasetBox = wrapWithHeader(
@ -637,23 +632,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
[topBar](const std::vector<geopro::data::ProjectSummary>& list,
const QString& cur) { topBar->setProjects(list, cur); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
[objectTree, datasetList, datasetTitle, datasetTabs](
[objectTree, datasetList, fileList, datasetTitle, datasetTabs](
const QString& projectName,
const std::vector<geopro::data::StructNode>& nodes) {
objectTree->setStructure(projectName, nodes);
datasetList->clear(); // 切项目清空 DS 列表
datasetList->clear();
fileList->clear();
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(0, QStringLiteral("数据"));
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsNode>& list) {
const QString&, const std::vector<geopro::data::DsRow>& list) {
geopro::app::populateDatasetList(datasetList, list);
if (datasetTitle)
datasetTitle->setText(QStringLiteral("数据集显示栏"));
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(list.size())));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
[fileList, datasetTabs](const QString&,
const std::vector<geopro::data::DsRow>& list) {
geopro::app::populateFileList(fileList, list);
datasetTabs->setTabText(
1, QStringLiteral("文件 (%1)").arg(static_cast<int>(list.size())));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
[objectTree, &window](const QString& stage, const QString& msg) {
if (stage == QStringLiteral("structure") ||

View File

@ -1,5 +1,6 @@
#include "panels/DatasetListPanel.hpp"
#include <QColor>
#include <QListWidget>
#include <QListWidgetItem>
#include <QString>
@ -7,30 +8,43 @@
namespace geopro::app {
namespace {
// dd 类型 → 中文标注。
QString ddTypeLabel(const std::string& ddType)
{
if (ddType == "dd_section") return QStringLiteral("剖面网格");
if (ddType == "dd_voxel") return QStringLiteral("体素");
return QString::fromStdString(ddType);
QString humanSize(long long b) {
if (b < 1024) return QStringLiteral("%1 B").arg(b);
const double kb = b / 1024.0;
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
}
} // namespace
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss)
{
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows) {
if (!list) return;
list->clear();
for (const auto& ds : dss) {
const QString name = QString::fromStdString(ds.name);
const QString label = ddTypeLabel(ds.ddType);
QString text = name;
if (!label.isEmpty()) text += QStringLiteral("\n%1").arg(label);
for (const auto& d : rows) {
QString text = QString::fromStdString(d.dsName);
if (!d.typeName.empty()) text += QStringLiteral("\n%1").arg(QString::fromStdString(d.typeName));
auto* item = new QListWidgetItem(text, list);
item->setData(kDsIdRole, QString::fromStdString(ds.id));
item->setData(kDsDdTypeRole, QString::fromStdString(ds.ddType));
item->setData(kDsIdRole, QString::fromStdString(d.id));
item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode));
}
}
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows) {
if (!list) return;
list->clear();
if (rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter);
return;
}
for (const auto& d : rows) {
const QString fname =
d.fileName.empty() ? QString::fromStdString(d.dsName) : QString::fromStdString(d.fileName);
const QString text = fname + QStringLiteral("\n%1").arg(humanSize(d.fileSize));
auto* item = new QListWidgetItem(text, list);
item->setData(kDsIdRole, QString::fromStdString(d.id));
item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl));
}
}

View File

@ -8,12 +8,13 @@ class QListWidget;
namespace geopro::app {
// 数据列表条目角色(与 main.cpp 树一致Qt::UserRole=dsId、+1=ddType
constexpr int kDsIdRole = 0x0100; // Qt::UserRole
constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1
constexpr int kDsIdRole = 0x0100; // Qt::UserRole
constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1
constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2文件下载 url备用
// 用某测线(TM)的数据集(采集批次)填充 QListWidget对齐原型左下「数据真实显示栏」
// 每条目 = 名称 +ddType 标注UserRole 存 dsId、+1 存 ddType供单击驱动数据详情
// 清空旧条目后重填
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss);
// 数据页签:每条 = dsName +类型名UserRole 存 dsId、+1 存 ddCode
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows);
// 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows);
} // namespace geopro::app

View File

@ -106,12 +106,20 @@ void WorkbenchNavController::switchProject(const QString& projectId) {
void WorkbenchNavController::selectTm(const QString& tmObjectId) {
if (tmObjectId.isEmpty() || busy_) return;
BusyGuard guard(this, &busy_);
const auto ds = repo_.loadDatasetsOfTm(tmObjectId.toStdString());
if (!ds.ok) {
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(ds.error));
const std::string pid = currentProjectId_;
const std::string tm = tmObjectId.toStdString();
const auto data = repo_.loadTmRows(pid, tm, 3); // 数据
if (!data.ok) {
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(data.error));
return;
}
emit datasetsLoaded(tmObjectId, ds.value);
emit datasetsLoaded(tmObjectId, data.value);
const auto files = repo_.loadTmRows(pid, tm, 1); // 文件
if (!files.ok) {
emit loadFailed(QStringLiteral("files"), QString::fromStdString(files.error));
return;
}
emit filesLoaded(tmObjectId, files.value);
}
} // namespace geopro::controller

View File

@ -28,7 +28,8 @@ signals:
void workspacesLoaded(const std::vector<geopro::data::Workspace>& list, const QString& currentId);
void projectsLoaded(const std::vector<geopro::data::ProjectSummary>& list, const QString& currentId);
void structureLoaded(const QString& projectName, const std::vector<geopro::data::StructNode>& nodes);
void datasetsLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsNode>& list);
void datasetsLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& list);
void filesLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& list);
void loadFailed(const QString& stage, const QString& message);
private:

View File

@ -63,12 +63,21 @@ RepoResult<std::vector<StructNode>> ApiProjectRepository::loadStructure(const st
return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}};
}
RepoResult<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) {
const QString path = QStringLiteral("/business/projectWorkbench/queryDsByTmObjectId/%1")
.arg(enc(tmObjectId));
const net::ApiResponse r = api_.get(path);
if (!ok(r)) return {false, {}, errorOf(r, "loadDatasetsOfTm failed")};
return {true, dto::parseDatasets(r.data.value(QStringLiteral("value")).toArray()), {}};
RepoResult<std::vector<DsRow>> ApiProjectRepository::loadTmRows(const std::string& projectId,
const std::string& tmObjectId,
int classifyType) {
const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page")
: QStringLiteral("/business/dsObject/data/page");
const QJsonObject body{
{QStringLiteral("projectId"), QString::fromStdString(projectId)},
{QStringLiteral("structParentId"), QString::fromStdString(tmObjectId)},
{QStringLiteral("structParentConfType"), 2},
{QStringLiteral("classifyTypeList"), QJsonArray{classifyType}},
{QStringLiteral("pageNo"), 1},
{QStringLiteral("pageSize"), 100}};
const net::ApiResponse r = api_.postJson(path, body);
if (!ok(r)) return {false, {}, errorOf(r, "loadTmRows failed")};
return {true, dto::parseDsRows(r.data.value(QStringLiteral("list")).toArray()), {}};
}
} // namespace geopro::data

View File

@ -14,7 +14,9 @@ public:
RepoResult<bool> switchWorkspace(const std::string& tenantId) override;
RepoResult<std::vector<ProjectSummary>> listProjects(const std::string& lastProjectId) override;
RepoResult<std::vector<StructNode>> loadStructure(const std::string& projectId) override;
RepoResult<std::vector<DsNode>> loadDatasetsOfTm(const std::string& tmObjectId) override;
RepoResult<std::vector<DsRow>> loadTmRows(const std::string& projectId,
const std::string& tmObjectId,
int classifyType) override;
private:
net::ApiClient& api_;

View File

@ -72,15 +72,20 @@ std::vector<StructNode> parseStructNodes(const QJsonArray& arr) {
return out;
}
std::vector<DsNode> parseDatasets(const QJsonArray& arr) {
std::vector<DsNode> out;
std::vector<DsRow> parseDsRows(const QJsonArray& arr) {
std::vector<DsRow> out;
out.reserve(static_cast<size_t>(arr.size()));
for (const QJsonValue& v : arr) {
const QJsonObject o = v.toObject();
DsNode d;
DsRow d;
d.id = str(o, "id");
d.name = str(o, "name");
d.ddType = str(o, "ddCode");
d.dsName = str(o, "dsName");
d.typeName = str(o, "name"); // 注意name 字段=ds类型名
d.ddCode = str(o, "ddCode");
const QJsonObject f = o.value(QStringLiteral("file")).toObject();
d.fileName = str(f, "name");
d.fileUrl = str(f, "url");
d.fileSize = static_cast<long long>(f.value(QStringLiteral("size")).toDouble());
out.push_back(std::move(d));
}
return out;

View File

@ -19,8 +19,8 @@ std::vector<ProjectSummary> parseProjectList(const QJsonArray& arr);
// 结构扁平节点数组queryProjectStruct 的 data["projectStructList"])→ 模型。
std::vector<StructNode> parseStructNodes(const QJsonArray& arr);
// DS 聚合数组queryDsByTmObjectId 的 data["value"])→ DsNode。ddCode → ddType
std::vector<DsNode> parseDatasets(const QJsonArray& arr);
// data/page / file/page 的 data["list"] 数组 → DsRow数据行无 file文件行含 file{name,size,url}
std::vector<DsRow> parseDsRows(const QJsonArray& arr);
// 扁平 StructNode 按 parentId 建树。叶子(无子节点)=TM。处理项目直挂 TM、孤儿 parentId、空表。
struct StructTreeNode {

View File

@ -21,7 +21,10 @@ public:
virtual RepoResult<bool> switchWorkspace(const std::string& tenantId) = 0;
virtual RepoResult<std::vector<ProjectSummary>> listProjects(const std::string& lastProjectId) = 0;
virtual RepoResult<std::vector<StructNode>> loadStructure(const std::string& projectId) = 0;
virtual RepoResult<std::vector<DsNode>> loadDatasetsOfTm(const std::string& tmObjectId) = 0;
// 按 TM 拉数据集/文件行classifyType 3=数据(data/page) 1=文件(file/page)。
virtual RepoResult<std::vector<DsRow>> loadTmRows(const std::string& projectId,
const std::string& tmObjectId,
int classifyType) = 0;
};
} // namespace geopro::data

View File

@ -3,6 +3,13 @@
#include <vector>
namespace geopro::data {
struct DsNode { std::string id, name, ddType; };
// data/page 或 file/page 的一条 ds。数据行只用 dsName/typeName/ddCode文件行另含 file*。
struct DsRow {
std::string id, dsName, typeName, ddCode;
std::string fileName, fileUrl;
long long fileSize = 0;
};
struct TmNode { std::string id, name, confCode; std::vector<DsNode> dss; };
struct GsNode { std::string id, name; std::vector<TmNode> tms; };
struct Project { std::string id, name; std::vector<GsNode> gss; };

View File

@ -64,17 +64,6 @@ TEST(NavDto, ParseStructNodesMapsParentAndType) {
EXPECT_EQ(ns[1].type, 2);
}
TEST(NavDto, ParseDatasetsMapsDdCodeToDdType) {
const auto arr = arrOf(R"([
{"id":"ds1","name":"批次1","ddCode":"dd_section","typeName":"剖面"}
])");
const auto ds = dto::parseDatasets(arr);
ASSERT_EQ(ds.size(), 1u);
EXPECT_EQ(ds[0].id, "ds1");
EXPECT_EQ(ds[0].name, "批次1");
EXPECT_EQ(ds[0].ddType, "dd_section");
}
TEST(NavDto, BuildStructTreeNestsGsTmAndDirectTm) {
const std::vector<StructNode> flat = {
{"gs1", "工区1", "", "GS", "", 1},
@ -155,3 +144,23 @@ TEST(NavDto, BuildStructTreeDropsDsAndTmStaysLeaf) {
EXPECT_TRUE(roots[0].children[0].isTm); // TM type2
EXPECT_TRUE(roots[0].children[0].children.empty()); // DS 不进树
}
TEST(NavDto, ParseDsRowsDataAndFile) {
const auto d = dto::parseDsRows(arrOf(R"([
{"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data"}
])"));
ASSERT_EQ(d.size(), 1u);
EXPECT_EQ(d[0].id, "d1");
EXPECT_EQ(d[0].dsName, "ERT1-WS");
EXPECT_EQ(d[0].typeName, "电阻率数据");
EXPECT_TRUE(d[0].fileName.empty());
const auto f = dto::parseDsRows(arrOf(R"([
{"id":"f1","dsName":"ERT1-WS.xlsx","name":"","ddCode":"dd_file",
"file":{"name":"ERT1-WS.xlsx","size":62760,"url":"/common/file/x.xlsx"}}
])"));
ASSERT_EQ(f.size(), 1u);
EXPECT_EQ(f[0].fileName, "ERT1-WS.xlsx");
EXPECT_EQ(f[0].fileSize, 62760);
EXPECT_EQ(f[0].fileUrl, "/common/file/x.xlsx");
}