feat(nav): ds数据/文件页签创建时间显示 + 加载更多分页(loadTmRows分页+total)

This commit is contained in:
gaozheng 2026-06-09 15:29:42 +08:00
parent 7cdc7b8077
commit ee8342f4bf
12 changed files with 139 additions and 43 deletions

View File

@ -502,7 +502,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
[propLabel, detailRendererPtr, detailRenderWindowPtr](QListWidgetItem* item) {
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
nav.loadMoreData();
return;
}
const QString name =
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
detailRendererPtr->RemoveAllViewProps();
@ -617,6 +621,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}
// ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
// "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。
auto removeLoadMore = [](QListWidget* lw) {
if (lw->count() > 0 &&
lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool())
delete lw->takeItem(lw->count() - 1);
};
auto addLoadMore = [](QListWidget* lw, int total) {
const int loaded = lw->count();
if (loaded < total) {
auto* m = new QListWidgetItem(
QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total), lw);
m->setData(geopro::app::kDsLoadMoreRole, true);
m->setTextAlignment(Qt::AlignCenter);
m->setForeground(QColor("#2D6CB5"));
}
return loaded;
};
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchWorkspace);
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
@ -643,19 +664,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& list) {
geopro::app::populateDatasetList(datasetList, list);
[removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
removeLoadMore(datasetList);
geopro::app::populateDatasetList(datasetList, rows, append);
const int loaded = addLoadMore(datasetList, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(list.size())));
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("数据"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
[fileList, datasetTabs](const QString&,
const std::vector<geopro::data::DsRow>& list) {
geopro::app::populateFileList(fileList, list);
[removeLoadMore, addLoadMore, fileList, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
removeLoadMore(fileList);
geopro::app::populateFileList(fileList, rows, append);
const int loaded = addLoadMore(fileList, total);
datasetTabs->setTabText(
1, QStringLiteral("文件 (%1)").arg(static_cast<int>(list.size())));
1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("文件"));
});
QObject::connect(fileList, &QListWidget::itemClicked, fileList,
[&nav](QListWidgetItem* item) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles();
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
[objectTree, &window](const QString& stage, const QString& msg) {

View File

@ -16,22 +16,25 @@ QString humanSize(long long b) {
}
} // namespace
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows) {
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
if (!list) return;
list->clear();
if (!append) list->clear();
for (const auto& d : rows) {
QString text = QString::fromStdString(d.dsName);
if (!d.typeName.empty()) text += QStringLiteral("\n%1").arg(QString::fromStdString(d.typeName));
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
if (!d.typeName.empty())
sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型
if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub);
auto* item = new QListWidgetItem(text, list);
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) {
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
if (!list) return;
list->clear();
if (rows.empty()) {
if (!append) list->clear();
if (!append && rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
@ -41,7 +44,9 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
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));
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小
const QString text = fname + QStringLiteral("\n%1").arg(sub);
auto* item = new QListWidgetItem(text, list);
item->setData(kDsIdRole, QString::fromStdString(d.id));
item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl));

View File

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

View File

@ -106,20 +106,48 @@ void WorkbenchNavController::switchProject(const QString& projectId) {
void WorkbenchNavController::selectTm(const QString& tmObjectId) {
if (tmObjectId.isEmpty() || busy_) return;
BusyGuard guard(this, &busy_);
currentTmId_ = tmObjectId.toStdString();
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));
dataPageNo_ = 1;
filePageNo_ = 1;
const auto d = repo_.loadTmRows(pid, currentTmId_, 3, dataPageNo_);
if (!d.ok) {
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
return;
}
emit datasetsLoaded(tmObjectId, data.value);
const auto files = repo_.loadTmRows(pid, tm, 1); // 文件
if (!files.ok) {
emit loadFailed(QStringLiteral("files"), QString::fromStdString(files.error));
dataTotal_ = d.value.total;
emit datasetsLoaded(tmObjectId, d.value.rows, d.value.total, false);
const auto f = repo_.loadTmRows(pid, currentTmId_, 1, filePageNo_);
if (!f.ok) {
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
return;
}
emit filesLoaded(tmObjectId, files.value);
fileTotal_ = f.value.total;
emit filesLoaded(tmObjectId, f.value.rows, f.value.total, false);
}
void WorkbenchNavController::loadMoreData() {
if (currentTmId_.empty() || busy_) return;
BusyGuard guard(this, &busy_);
const auto d = repo_.loadTmRows(currentProjectId_, currentTmId_, 3, ++dataPageNo_);
if (!d.ok) {
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
return;
}
dataTotal_ = d.value.total;
emit datasetsLoaded(QString::fromStdString(currentTmId_), d.value.rows, d.value.total, true);
}
void WorkbenchNavController::loadMoreFiles() {
if (currentTmId_.empty() || busy_) return;
BusyGuard guard(this, &busy_);
const auto f = repo_.loadTmRows(currentProjectId_, currentTmId_, 1, ++filePageNo_);
if (!f.ok) {
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
return;
}
fileTotal_ = f.value.total;
emit filesLoaded(QString::fromStdString(currentTmId_), f.value.rows, f.value.total, true);
}
} // namespace geopro::controller

View File

@ -22,14 +22,18 @@ public slots:
void switchWorkspace(const QString& tenantId);
void switchProject(const QString& projectId);
void selectTm(const QString& tmObjectId);
void loadMoreData();
void loadMoreFiles();
signals:
void busyChanged(bool busy);
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::DsRow>& list);
void filesLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& list);
void datasetsLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
int total, bool append);
void filesLoaded(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
int total, bool append);
void loadFailed(const QString& stage, const QString& message);
private:
@ -39,6 +43,11 @@ private:
bool busy_ = false;
std::vector<data::ProjectSummary> lastProjects_;
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
std::string currentTmId_;
int dataPageNo_ = 0;
int filePageNo_ = 0;
int dataTotal_ = 0;
int fileTotal_ = 0;
};
} // namespace geopro::controller

View File

@ -63,9 +63,9 @@ RepoResult<std::vector<StructNode>> ApiProjectRepository::loadStructure(const st
return {true, dto::parseStructNodes(r.data.value(QStringLiteral("value")).toArray()), {}};
}
RepoResult<std::vector<DsRow>> ApiProjectRepository::loadTmRows(const std::string& projectId,
const std::string& tmObjectId,
int classifyType) {
RepoResult<DsPage> ApiProjectRepository::loadTmRows(const std::string& projectId,
const std::string& tmObjectId, int classifyType,
int pageNo) {
const QString path = (classifyType == 1) ? QStringLiteral("/business/dsObject/file/page")
: QStringLiteral("/business/dsObject/data/page");
const QJsonObject body{
@ -73,11 +73,11 @@ RepoResult<std::vector<DsRow>> ApiProjectRepository::loadTmRows(const std::strin
{QStringLiteral("structParentId"), QString::fromStdString(tmObjectId)},
{QStringLiteral("structParentConfType"), 2},
{QStringLiteral("classifyTypeList"), QJsonArray{classifyType}},
{QStringLiteral("pageNo"), 1},
{QStringLiteral("pageNo"), pageNo},
{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()), {}};
return {true, dto::parseDsPage(r.data), {}};
}
} // namespace geopro::data

View File

@ -14,9 +14,8 @@ 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<DsRow>> loadTmRows(const std::string& projectId,
const std::string& tmObjectId,
int classifyType) override;
RepoResult<DsPage> loadTmRows(const std::string& projectId, const std::string& tmObjectId,
int classifyType, int pageNo) override;
private:
net::ApiClient& api_;

View File

@ -82,6 +82,7 @@ std::vector<DsRow> parseDsRows(const QJsonArray& arr) {
d.dsName = str(o, "dsName");
d.typeName = str(o, "name"); // 注意name 字段=ds类型名
d.ddCode = str(o, "ddCode");
d.createTime = str(o, "createTime");
const QJsonObject f = o.value(QStringLiteral("file")).toObject();
d.fileName = str(f, "name");
d.fileUrl = str(f, "url");
@ -91,6 +92,13 @@ std::vector<DsRow> parseDsRows(const QJsonArray& arr) {
return out;
}
DsPage parseDsPage(const QJsonObject& data) {
DsPage p;
p.rows = parseDsRows(data.value(QStringLiteral("list")).toArray());
p.total = data.value(QStringLiteral("total")).toInt();
return p;
}
std::vector<StructTreeNode> buildStructTree(const std::vector<StructNode>& flat) {
// 过滤 DS(type==3)DS 不进对象树(按 TM 单独拉取到数据列表)。
std::vector<StructNode> nodes;

View File

@ -22,6 +22,9 @@ std::vector<StructNode> parseStructNodes(const QJsonArray& arr);
// data/page / file/page 的 data["list"] 数组 → DsRow数据行无 file文件行含 file{name,size,url})。
std::vector<DsRow> parseDsRows(const QJsonArray& arr);
// data/page 或 file/page 的整个 data 对象 → DsPagerows + total
DsPage parseDsPage(const QJsonObject& data);
// 扁平 StructNode 按 parentId 建树。叶子(无子节点)=TM。处理项目直挂 TM、孤儿 parentId、空表。
struct StructTreeNode {
StructNode node;

View File

@ -21,10 +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;
// 按 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;
// 按 TM 分页拉数据/文件行classifyType 3=数据 1=文件pageNo 从 1 起pageSize 固定 100
virtual RepoResult<DsPage> loadTmRows(const std::string& projectId,
const std::string& tmObjectId, int classifyType,
int pageNo) = 0;
};
} // namespace geopro::data

View File

@ -6,10 +6,11 @@ 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 id, dsName, typeName, ddCode, createTime;
std::string fileName, fileUrl;
long long fileSize = 0;
};
struct DsPage { std::vector<DsRow> rows; int total = 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

@ -147,12 +147,13 @@ TEST(NavDto, BuildStructTreeDropsDsAndTmStaysLeaf) {
TEST(NavDto, ParseDsRowsDataAndFile) {
const auto d = dto::parseDsRows(arrOf(R"([
{"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data"}
{"id":"d1","dsName":"ERT1-WS","name":"电阻率数据","ddCode":"dd_inversion_data","createTime":"2026-03-25 16:48:57"}
])"));
ASSERT_EQ(d.size(), 1u);
EXPECT_EQ(d[0].id, "d1");
EXPECT_EQ(d[0].dsName, "ERT1-WS");
EXPECT_EQ(d[0].typeName, "电阻率数据");
EXPECT_EQ(d[0].createTime, "2026-03-25 16:48:57");
EXPECT_TRUE(d[0].fileName.empty());
const auto f = dto::parseDsRows(arrOf(R"([
@ -163,4 +164,12 @@ TEST(NavDto, ParseDsRowsDataAndFile) {
EXPECT_EQ(f[0].fileName, "ERT1-WS.xlsx");
EXPECT_EQ(f[0].fileSize, 62760);
EXPECT_EQ(f[0].fileUrl, "/common/file/x.xlsx");
const auto page = dto::parseDsPage(objOf(R"({
"total": 18,
"list": [{"id":"x","dsName":"a","name":"t","ddCode":"dd","createTime":"2026-01-01 00:00:00"}]
})"));
EXPECT_EQ(page.total, 18);
ASSERT_EQ(page.rows.size(), 1u);
EXPECT_EQ(page.rows[0].dsName, "a");
}