geopro/src/controller/WorkbenchNavController.cpp

467 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "WorkbenchNavController.hpp"
#include <QDebug>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include "api/NavLoads.hpp"
#include "api/NavRequest.hpp"
#include "dto/NavDto.hpp"
#include "repo/IAsyncProjectRepository.hpp"
namespace geopro::controller {
namespace {
// 数据页树形:一次取全的大 pageSize远超单 TM 实际 DS 数;超出会日志告警)+ 每页根节点数。
constexpr int kFetchAllPageSize = 1000;
constexpr int kDataRootPageSize = 5;
} // namespace
using data::DsPage;
using data::DsRow;
using data::DynamicForm;
using data::ExceptionRow;
using data::NavRequest;
using data::ProjectListPage;
using data::ProjectSummary;
using data::StructNode;
using data::Workspace;
WorkbenchNavController::WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent)
: QObject(parent), repo_(repo) {}
WorkbenchNavController::~WorkbenchNavController() { abortAll(); }
bool WorkbenchNavController::anyInflight() const {
if (startStepReq_ || structReq_ || selDataReq_ || selFileReq_ || selDetailReq_ ||
moreFilesReq_ || datasetReq_)
return true;
for (const auto& h : checkedInflight_)
if (h) return true;
return false;
}
void WorkbenchNavController::emitBusyIfChanged() {
const bool now = anyInflight();
if (now != lastBusy_) {
lastBusy_ = now;
emit busyChanged(now);
}
}
void WorkbenchNavController::abortAll() {
if (startStepReq_) startStepReq_->abort();
if (structReq_) structReq_->abort();
if (selDataReq_) selDataReq_->abort();
if (selFileReq_) selFileReq_->abort();
if (selDetailReq_) selDetailReq_->abort();
if (moreFilesReq_) moreFilesReq_->abort();
if (datasetReq_) datasetReq_->abort();
for (const auto& h : checkedInflight_)
if (h) h->abort();
checkedInflight_.clear();
}
void WorkbenchNavController::resetSelectionState() {
tmExceptionCache_.clear();
currentParentId_.clear();
currentParentConfType_ = 0;
allDataRows_.clear();
dataRootsShown_ = 0;
dataTotal_ = 0;
}
// ── start / switchWorkspace 依赖链listWorkspaces → pageProjects → loadStructure ──
// 用 NavRequest 续延(每级 done 内用业务值构造下一级startStepReq_ 跟踪当前在飞级。
// abort-and-replacestart/switchWorkspace 入口 abort 旧 startStepReq_ → 自然丢弃旧链迟到信号。
void WorkbenchNavController::start() {
if (startStepReq_) startStepReq_->abort();
NavRequest* req = repo_.listWorkspacesAsync();
startStepReq_ = req;
emitBusyIfChanged();
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
if (req != startStepReq_) return; // §5.0 身份比对
startStepReq_.clear();
const auto ws = qvariant_cast<std::vector<Workspace>>(v);
QString cur;
for (const auto& w : ws)
if (w.isCurrent) cur = QString::fromStdString(w.id);
if (cur.isEmpty() && !ws.empty()) cur = QString::fromStdString(ws.front().id);
currentWorkspaceId_ = cur.toStdString();
emit workspacesLoaded(ws, cur);
runProjectsAndStructure();
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != startStepReq_) return;
startStepReq_.clear();
emit loadFailed(QStringLiteral("workspaces"), msg);
emitBusyIfChanged();
});
}
void WorkbenchNavController::switchWorkspace(const QString& tenantId) {
if (tenantId.isEmpty()) return;
if (startStepReq_) startStepReq_->abort();
NavRequest* req = repo_.switchWorkspaceAsync(tenantId.toStdString());
startStepReq_ = req;
emitBusyIfChanged();
const std::string tid = tenantId.toStdString();
QObject::connect(req, &NavRequest::done, this, [this, req, tid](const QVariant&) {
if (req != startStepReq_) return;
startStepReq_.clear();
currentWorkspaceId_ = tid;
runProjectsAndStructure();
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != startStepReq_) return;
startStepReq_.clear();
emit loadFailed(QStringLiteral("switchWorkspace"), msg);
emitBusyIfChanged();
});
}
void WorkbenchNavController::runProjectsAndStructure() {
NavRequest* req = repo_.pageProjectsAsync(std::string(), std::string(), 1, 10); // 下拉首页 10
startStepReq_ = req;
emitBusyIfChanged();
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
if (req != startStepReq_) return;
startStepReq_.clear();
const auto page = qvariant_cast<ProjectListPage>(v);
lastProjects_ = page.rows;
resetSelectionState();
QString curP;
if (!page.rows.empty()) {
const auto& first = page.rows.front();
curP = QString::fromStdString(first.id);
currentProjectId_ = first.id;
currentProjectName_ = first.name;
currentCrsCode_ = first.crsCode;
} else {
currentProjectId_.clear();
currentProjectName_.clear();
currentCrsCode_.clear();
}
emit projectsLoaded(page.rows, curP, page.total);
if (curP.isEmpty()) {
lastStructNodes_.clear();
emit structureLoaded(QString(), {}); // 暂无项目 → 空树
emitBusyIfChanged();
return;
}
NavRequest* st = repo_.loadStructureAsync(currentProjectId_);
startStepReq_ = st;
emitBusyIfChanged();
QObject::connect(st, &NavRequest::done, this, [this, st](const QVariant& sv) {
if (st != startStepReq_) return;
startStepReq_.clear();
const auto nodes = qvariant_cast<std::vector<StructNode>>(sv);
lastStructNodes_ = nodes;
emit structureLoaded(QString::fromStdString(currentProjectName_), nodes);
emitBusyIfChanged();
});
QObject::connect(st, &NavRequest::failed, this, [this, st](const QString& msg) {
if (st != startStepReq_) return;
startStepReq_.clear();
emit loadFailed(QStringLiteral("structure"), msg);
emitBusyIfChanged();
});
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != startStepReq_) return;
startStepReq_.clear();
emit loadFailed(QStringLiteral("projects"), msg);
emitBusyIfChanged();
});
}
// ── switchProject单请求 loadStructure ──
void WorkbenchNavController::switchProject(const QString& projectId) {
if (projectId.isEmpty()) return;
currentProjectId_ = projectId.toStdString();
for (const auto& p : lastProjects_)
if (p.id == currentProjectId_) {
currentProjectName_ = p.name;
currentCrsCode_ = p.crsCode;
}
if (structReq_) structReq_->abort(); // abort-and-replace
NavRequest* req = repo_.loadStructureAsync(currentProjectId_);
structReq_ = req;
emitBusyIfChanged();
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
if (req != structReq_) return; // §5.0 身份比对
structReq_.clear();
const auto nodes = qvariant_cast<std::vector<StructNode>>(v);
lastStructNodes_ = nodes;
resetSelectionState();
emit structureLoaded(QString::fromStdString(currentProjectName_), nodes);
emitBusyIfChanged();
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != structReq_) return;
structReq_.clear();
emit loadFailed(QStringLiteral("structure"), msg);
emitBusyIfChanged();
});
}
// ── selectObject三并发data 行 / file 行 / 对象详情),各自身份比对、独立 emit ──
void WorkbenchNavController::selectObject(const QString& objectId, int confType) {
if (objectId.isEmpty()) return;
if (selDataReq_) selDataReq_->abort(); // abort-and-replace 三路
if (selFileReq_) selFileReq_->abort();
if (selDetailReq_) selDetailReq_->abort();
currentParentId_ = objectId.toStdString();
currentParentConfType_ = confType;
const std::string pid = currentProjectId_;
dataPageNo_ = 1;
filePageNo_ = 1;
allDataRows_.clear();
dataRootsShown_ = 0;
// 数据页:一次取全(大 pageSize再按根客户端分页——保证树完整子节点不会跨服务端分页丢失父
NavRequest* dReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 3, 1, kFetchAllPageSize);
selDataReq_ = dReq;
NavRequest* fReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 1, filePageNo_);
selFileReq_ = fReq;
NavRequest* detReq = repo_.loadObjectDetailAsync(currentParentId_, confType);
selDetailReq_ = detReq;
emitBusyIfChanged();
QObject::connect(dReq, &NavRequest::done, this, [this, dReq, objectId](const QVariant& v) {
if (dReq != selDataReq_) return;
selDataReq_.clear();
const auto page = qvariant_cast<DsPage>(v);
if (static_cast<int>(page.rows.size()) < page.total)
qWarning() << "[nav] data/page 未取全listCount=" << page.rows.size()
<< " total=" << page.total << " → 树可能不完整pageSize 不足)";
allDataRows_ = page.rows; // 全量缓存;按根分页由 emitNextDataRootPage 切
dataRootsShown_ = 0;
emitNextDataRootPage(false); // 首页append=false
emitBusyIfChanged();
});
QObject::connect(dReq, &NavRequest::failed, this, [this, dReq](const QString& msg) {
if (dReq != selDataReq_) return;
selDataReq_.clear();
emit loadFailed(QStringLiteral("datasets"), msg);
emitBusyIfChanged();
});
QObject::connect(fReq, &NavRequest::done, this, [this, fReq, objectId](const QVariant& v) {
if (fReq != selFileReq_) return;
selFileReq_.clear();
const auto page = qvariant_cast<DsPage>(v);
fileTotal_ = page.total;
emit filesLoaded(objectId, page.rows, page.total, false);
emitBusyIfChanged();
});
QObject::connect(fReq, &NavRequest::failed, this, [this, fReq](const QString& msg) {
if (fReq != selFileReq_) return;
selFileReq_.clear();
emit loadFailed(QStringLiteral("files"), msg);
emitBusyIfChanged();
});
QObject::connect(detReq, &NavRequest::done, this, [this, detReq, objectId](const QVariant& v) {
if (detReq != selDetailReq_) return;
selDetailReq_.clear();
emit objectDetailLoaded(objectId, qvariant_cast<DynamicForm>(v));
emitBusyIfChanged();
});
QObject::connect(detReq, &NavRequest::failed, this, [this, detReq](const QString& msg) {
if (detReq != selDetailReq_) return;
selDetailReq_.clear();
emit loadFailed(QStringLiteral("objectDetail"), msg);
emitBusyIfChanged();
});
}
// ── 数据页树形分页:从 allDataRows_ 按根切下一页(同步,无请求)──
// 根 = parentId 为空或不在本 TM 全量集合内(其父是源文件节点,不在 data/page 返回里)。
// 每页取 kDataRootPageSize 个根 + 各自整棵子树;行序保持后端原序(便于稳定显示)。
void WorkbenchNavController::emitNextDataRootPage(bool append) {
const QString parent = QString::fromStdString(currentParentId_);
// 本 TM 全部行 id 集合(判定谁是根)。
std::unordered_set<std::string> ids;
ids.reserve(allDataRows_.size());
for (const auto& r : allDataRows_) ids.insert(r.id);
// 根索引(按原序)+ parentId→子索引表。
std::vector<std::size_t> rootIdx;
std::unordered_map<std::string, std::vector<std::size_t>> kids;
for (std::size_t i = 0; i < allDataRows_.size(); ++i) {
const std::string& p = allDataRows_[i].parentId;
if (p.empty() || ids.find(p) == ids.end())
rootIdx.push_back(i);
else
kids[p].push_back(i);
}
const int rootCount = static_cast<int>(rootIdx.size());
dataTotal_ = rootCount;
// 取本页根 [shown, end) 的整棵子树DFS 收集索引),再按原序输出保稳定。
const int end = std::min(dataRootsShown_ + kDataRootPageSize, rootCount);
std::unordered_set<std::size_t> picked;
for (int k = dataRootsShown_; k < end; ++k) {
std::vector<std::size_t> stack{rootIdx[k]};
while (!stack.empty()) {
const std::size_t cur = stack.back();
stack.pop_back();
if (!picked.insert(cur).second) continue;
auto it = kids.find(allDataRows_[cur].id);
if (it != kids.end())
for (std::size_t c : it->second) stack.push_back(c);
}
}
std::vector<DsRow> out;
out.reserve(picked.size());
for (std::size_t i = 0; i < allDataRows_.size(); ++i)
if (picked.count(i)) out.push_back(allDataRows_[i]);
dataRootsShown_ = end;
emit datasetsLoaded(parent, out, rootCount, append);
}
// ── loadMoreData数据页树形——同步切下一页根无请求。loadMoreFiles文件页服务端分页 ──
void WorkbenchNavController::loadMoreData() {
if (currentParentId_.empty()) return;
if (dataRootsShown_ >= dataTotal_) return; // 无更多根
emitNextDataRootPage(true);
}
void WorkbenchNavController::loadMoreFiles() {
if (currentParentId_.empty()) return;
if (moreFilesReq_) moreFilesReq_->abort();
NavRequest* req =
repo_.loadRowsAsync(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_);
moreFilesReq_ = req;
const QString parent = QString::fromStdString(currentParentId_);
emitBusyIfChanged();
QObject::connect(req, &NavRequest::done, this, [this, req, parent](const QVariant& v) {
if (req != moreFilesReq_) return;
moreFilesReq_.clear();
const auto page = qvariant_cast<DsPage>(v);
fileTotal_ = page.total;
emit filesLoaded(parent, page.rows, page.total, true);
emitBusyIfChanged();
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != moreFilesReq_) return;
moreFilesReq_.clear();
--filePageNo_; // 回滚:本请求失败,页号复位,避免下次 loadMoreFiles 跳页
emit loadFailed(QStringLiteral("files"), msg);
emitBusyIfChanged();
});
}
// ── selectDataset单请求 ──
void WorkbenchNavController::selectDataset(const QString& dsObjectId) {
if (dsObjectId.isEmpty()) return;
if (datasetReq_) datasetReq_->abort();
NavRequest* req = repo_.loadDatasetFormAsync(dsObjectId.toStdString());
datasetReq_ = req;
emitBusyIfChanged();
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
if (req != datasetReq_) return;
datasetReq_.clear();
emit datasetDetailLoaded(qvariant_cast<DynamicForm>(v));
emitBusyIfChanged();
});
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
if (req != datasetReq_) return;
datasetReq_.clear();
emit loadFailed(QStringLiteral("datasetDetail"), msg);
emitBusyIfChanged();
});
}
// ── setCheckedTms未命中缓存项并发拉取全到齐后组装新勾选 abort 旧批(以最后一次为准)──
void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) {
for (const auto& h : checkedInflight_) // abort-and-replace 旧批
if (h) h->abort();
checkedInflight_.clear();
// 入口去重:保留首次出现顺序,剔除重复 id避免重复请求 + 异常树重复 group
QStringList deduped;
QSet<QString> seen;
for (const QString& tmQ : tmObjectIds) {
if (seen.contains(tmQ)) continue;
seen.insert(tmQ);
deduped.push_back(tmQ);
}
QStringList missing;
for (const QString& tmQ : deduped) {
const std::string tm = tmQ.toStdString();
if (tmExceptionCache_.find(tm) == tmExceptionCache_.end()) missing.push_back(tmQ);
}
if (missing.isEmpty()) { // 全命中缓存同步组装busyChanged 不抖动
assembleAndEmitExceptionTree(deduped);
emitBusyIfChanged();
return;
}
// 并发拉取未命中项;每个 done 写缓存、清自身槽,全到齐后组装。计数用 shared 计数器。
auto remaining = std::make_shared<int>(missing.size());
auto failedFlag = std::make_shared<bool>(false);
for (const QString& tmQ : missing) {
const std::string tm = tmQ.toStdString();
NavRequest* req = repo_.loadExceptionsByTmAsync(tm);
checkedInflight_.push_back(req);
QObject::connect(req, &NavRequest::done, this,
[this, req, tm, remaining, failedFlag, deduped](const QVariant& v) {
// 身份比对req 仍在当前批中才接受(旧批已 abort + 从 vector 移除)。
bool inCurrent = false;
for (const auto& h : checkedInflight_)
if (h == req) inCurrent = true;
if (!inCurrent) return;
tmExceptionCache_[tm] = qvariant_cast<std::vector<ExceptionRow>>(v);
if (--(*remaining) == 0 && !*failedFlag) {
checkedInflight_.clear();
assembleAndEmitExceptionTree(deduped);
emitBusyIfChanged();
}
});
QObject::connect(req, &NavRequest::failed, this,
[this, req, remaining, failedFlag](const QString& msg) {
bool inCurrent = false;
for (const auto& h : checkedInflight_)
if (h == req) inCurrent = true;
if (!inCurrent) return;
if (*failedFlag) return; // 仅首个失败发一次
*failedFlag = true;
for (const auto& h : checkedInflight_) // abort 其余在飞
if (h && h != req) h->abort();
checkedInflight_.clear();
emit loadFailed(QStringLiteral("exceptions"), msg);
emitBusyIfChanged();
});
}
emitBusyIfChanged();
}
void WorkbenchNavController::assembleAndEmitExceptionTree(const QStringList& tmObjectIds) {
auto nameOf = [this](const std::string& id) -> std::string {
for (const auto& n : lastStructNodes_)
if (n.id == id) return n.name;
return id;
};
std::vector<data::ObjectExceptionGroup> groups;
int total = 0;
for (const QString& tmQ : tmObjectIds) {
const std::string tm = tmQ.toStdString();
auto it = tmExceptionCache_.find(tm);
if (it == tmExceptionCache_.end()) continue; // 防御:理论上此时全命中
auto grouped = data::dto::groupExceptionsByConsortium(it->second);
data::ObjectExceptionGroup g;
g.objectId = tm;
g.objectName = nameOf(tm);
g.consortia = std::move(grouped.consortia);
g.looseExceptions = std::move(grouped.loose);
total += static_cast<int>(it->second.size());
groups.push_back(std::move(g));
}
emit exceptionTreeLoaded(groups, total);
}
} // namespace geopro::controller