467 lines
19 KiB
C++
467 lines
19 KiB
C++
#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-replace:start/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
|