geopro/src/app/main.cpp

1395 lines
82 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.

// M1 工作台(视图重构 Task B正确产品模型。
// - 左上 对象显示栏GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section可多条共存。
// - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
// 二维地图 = 对每个勾选数据集 buildSurveyLinelat/lon 红线俯视z=0+ applyTop2D浅底背景
// 三维视图 = 勾选测线的 buildCurtain竖直断面墙actor SetScale(1,1,3) 纵向夸张 + applyFree3D白底
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)
// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。
// 切视图 / 勾选变化 / 图层变化 → 重建对应内容。
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
// 单击某 DS → 显示该数据集:
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
// 原数据 = #17 彩色散点buildScatterx=距离/y=深度,按散点自带色阶上色)。
// 显示异常 = 在上图叠加异常圈定buildAnomaliesdashed 折线,同纵向夸张对齐)。
// 两者皆平躺俯视正交 + 属性。
// - 右 属性:选中数据集属性文本。
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准
#include <fstream>
#include <initializer_list>
#include <memory>
#include <sstream>
#include <string>
#include <typeinfo>
#include <vector>
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QColor>
#include <QDialog>
#include <QEasingCurve>
#include <QEvent>
#include <QFile>
#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QPushButton>
#include <QSlider>
#include <QGraphicsOpacityEffect>
#include <QDate>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QJsonObject>
#include <QMenu>
#include <QMessageBox>
#include <QPoint>
#include <QSet>
#include <QToolButton>
#include <QKeySequence>
#include <QProcess>
#include <QSettings>
#include <QShortcut>
#include <QPropertyAnimation>
#include <QVariantAnimation>
#include <QStringList>
#include <QTabWidget>
#include <QMainWindow>
#include <QStatusBar>
#include <QStyle>
#include <QSurfaceFormat>
#include <QSignalBlocker>
#include <QTimer>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include <QVBoxLayout>
#include <QWidget>
#include <QtWebEngineQuick/QtWebEngineQuick>
#include <DockAreaTitleBar.h>
#include <DockAreaWidget.h>
#include <DockManager.h>
#include <DockWidget.h>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
#include "ApiClient.hpp"
#include "AuthService.hpp"
#include "DatasetDimension.hpp"
#include "Credential.hpp"
#include "Glyphs.hpp"
#include "Logging.hpp"
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "SettingsDialog.hpp"
#include "TopBar.hpp"
#include "ProjectListDialog.hpp"
#include "ObjectFormDialog.hpp"
#include "ImportDatasetDialog.hpp"
#include "WorkbenchNavController.hpp"
#include "VtkSceneController.hpp"
#include "VtkSceneView.hpp"
#include "api/NavRequest.hpp"
#include "api/NavLoads.hpp"
#include "DatasetDetailController.hpp"
#include "panels/chart/ErtInversionStrategy.hpp"
#include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp"
#include "panels/chart/TrajectoryStrategy.hpp"
#include "panels/chart/GridStrategy.hpp"
#include "api/ApiProjectRepository.hpp"
#include "api/ApiDatasetRepository.hpp"
#include "api/Api3dRepository.hpp"
#include "panels/ObjectTreePanel.hpp"
#include "login/LoginWindow.hpp"
#include "panels/DatasetListPanel.hpp"
#include "panels/DatasetDetailPanel.hpp"
#include "panels/DynamicFormView.hpp"
#include "panels/ObjectAttrPanel.hpp"
#include "panels/DatasetAttrPanel.hpp"
#include "panels/ObjectExceptionPanel.hpp"
#include "TileBasemap.hpp"
#include "panels/columns/ColumnDrawer.hpp"
#include "panels/columns/Column3DDataset.hpp"
#include "panels/columns/Column2DDataset.hpp"
#include "panels/columns/Column3DAnalysis.hpp"
#include "CameraPreset.hpp"
#include "ColorLutBuilder.hpp"
#include "Scene.hpp"
#include "VoxelFromScatters.hpp"
#include "interact/InteractionManager.hpp"
#include "interact/SlicePlaneMath.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/CurtainActor.hpp"
#include "actors/ElectrodeActor.hpp"
#include "actors/GridContourActor.hpp"
#include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "actors/TerrainActor.hpp"
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include <algorithm>
#include <exception>
#include <memory>
#include <set>
#include <vector>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCameraInterpolator.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkLookupTable.h>
#include <vtkProperty.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
namespace {
// 居中浮层定位器:监视 host中央 QVTK尺寸/显示变化,把 overlay 浮层
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
class CenterOverlay : public QObject {
public:
CenterOverlay(QWidget* overlay, QWidget* host)
: QObject(host), overlay_(overlay), host_(host)
{
host_->installEventFilter(this);
}
void reposition()
{
overlay_->adjustSize();
const QSize h = host_->size();
const QSize o = overlay_->size();
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
host_->y() + (h.height() - o.height()) / 2);
overlay_->raise();
}
protected:
bool eventFilter(QObject* obj, QEvent* e) override
{
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
reposition();
return QObject::eventFilter(obj, e);
}
private:
QWidget* overlay_;
QWidget* host_;
};
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path)
{
std::ifstream in(path, std::ios::binary);
if (!in) return {};
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
// 取 vector 中位数(用于由测线 lat/lon 推世界系原点)。空则返回 0。
double median(std::vector<double> v)
{
if (v.empty()) return 0.0;
std::sort(v.begin(), v.end());
const size_t n = v.size();
return n % 2 ? v[n / 2] : 0.5 * (v[n / 2 - 1] + v[n / 2]);
}
// 纵向夸张倍数Z 基准统一M-3全项目共用同一倍数使 帘面(z) / 体素 / 切片 /
// 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。
// 单一可调常量:要整体调纵向观感改这一处即可。
constexpr double kVerticalExaggeration = 1.0;
// 项目 CRS(实证确定,STATUS §4):散点 projX/Y→经纬→GeoLocalFrame 配准体素到世界系。
constexpr const char* kProjectCrs = "EPSG:4547"; // CGCS2000 / 3-degree GK CM 114E
constexpr const char* kWgs84 = "EPSG:4326";
// 在给定 QMainWindow 上构建 M1 工作台。
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo,
geopro::data::IAsyncProjectRepository& projectRepo,
geopro::data::IAsyncDatasetRepository& datasetRepo,
geopro::controller::WorkbenchNavController& nav,
geopro::controller::DatasetDetailController& detailCtrl)
{
// ── 世界系:启动取一次 grid1 的 lat/lon用中位数作 GeoLocalFrame 原点 ──
// 全项目共享shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
const auto baseGrid = repo.loadGrid("grid1");
const double lat0 = median(baseGrid.lat);
const double lon0 = median(baseGrid.lon);
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(lat0, lon0);
// 测线地表高程基准(地形 z rebase 用,使地形落在测线附近而非按绝对高程浮空)。
const double refElev = baseGrid.elevation.empty() ? 0.0 : median(baseGrid.elevation);
// ── 中央 QVTK + Scene竖直帘面场景─────────────────────────────────
// Scene 非 QObject堆分配用 widget 销毁信号清理widget 随 window 销毁)。
auto* scene = new geopro::render::Scene();
auto* vtkWidget = new QVTKOpenGLStereoWidget();
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
vtkWidget->setRenderWindow(renderWindow);
renderWindow->AddRenderer(scene->renderer());
vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get();
// 中央渲染编排VtkSceneController + VtkSceneView取代旧 rebuildCentral lambda 与裸 show* 标志)。
// 3D 场景仓储用 Api3dRepository真实后端loadSection 走真实 ERT 反演端点,委托 datasetRepo
// 视图VtkSceneView非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo);
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
frame, refElev);
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
vtkWidget);
sceneCtrl->setVerticalExaggeration(kVerticalExaggeration);
// ── P3 切片交互编排InteractionManager─────────────────────────────────
// interactor 由 QVTK 在 setRenderWindow 后提供renderWindow->GetInteractor())。
// 安装自定义拾取样式 + 持活动切片。仅三维 + 有体素可用;切到二维 closeAll。
auto* interactionMgr = new geopro::render::interact::InteractionManager(
renderWindowPtr->GetInteractor(), renderWindowPtr, scene->renderer());
// sceneView->onVolumeChanged 在三栏接线处设置(把体素 image 推给 InteractionManager见下
// 非 QObject 堆对象统一在此清理,按构造逆序:
// interactionMgr(持 interactor/切片观察者) → sceneView(持 scene&) → scene3dRepo → scene。
// interactionMgr 先析构closeAll() 解绑所有切片观察者,再拆 scene/interactor防悬挂崩溃。
QObject::connect(vtkWidget, &QObject::destroyed,
[scene, scene3dRepo, sceneView, interactionMgr]() {
delete interactionMgr;
delete sceneView;
delete scene3dRepo;
delete scene;
});
// PROJ 可用性探测(体素/地形/切片层都需配准):三栏重构后浮层勾选已移除,
// 仅保留探测以便将来在三栏里据此禁用相关项;本期结果暂未消费。
bool crsAvailable = false;
try {
geopro::core::CrsTransform probe(kProjectCrs, kWgs84);
crsAvailable = true;
} catch (const std::exception&) {
crsAvailable = false;
}
(void)crsAvailable;
// 停靠系统配置(必须在 CDockManager 构造前设置):对齐原型——面板固定、
// 标题栏不显示「关闭 / 浮动 / 标签菜单」等子窗口操作按钮,并关闭自动隐藏(钉住)。
ads::CDockManager::setConfigFlags(ads::CDockManager::DefaultOpaqueConfig);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasTabsMenuButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::ActiveTabHasCloseButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::AlwaysShowTabs, true); // 单面板也显示标题头
ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, false);
ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::AutoHideFlags()); // 禁用自动隐藏
auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager);
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
// 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。
const QString dockBaseQss = dockManager->styleSheet();
auto applyDockSplitter = [dockManager, dockBaseQss]() {
dockManager->setStyleSheet(
dockBaseQss +
geopro::app::fillTokens(QStringLiteral(
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: {{divider}}; }"
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: {{accent/primary}}; }")));
};
applyDockSplitter();
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
dockManager, [applyDockSplitter]() { applyDockSplitter(); });
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮ADS 自带标题栏随后隐藏,
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
auto wrapWithHeader = [](geopro::app::Glyph icon, const QString& title, QWidget* content,
const QVector<geopro::app::HeaderAction>& actions = {}) {
auto* box = new QWidget();
auto* v = new QVBoxLayout(box);
v->setContentsMargins(0, 0, 0, 0);
v->setSpacing(0);
v->addWidget(geopro::app::buildPanelHeader(icon, title, actions));
v->addWidget(content, 1);
return box;
};
// 中央容器顶部「VTK视图」表头 + 下方 [左三栏抽屉 | 右 QVTK 画布]。
auto* centerWidget = new QWidget();
auto* centerLayout = new QVBoxLayout(centerWidget);
centerLayout->setContentsMargins(0, 0, 0, 0);
centerLayout->setSpacing(0);
// VTK视图面板表头Task 7图标 + 标题「VTK视图」+ 全屏操作按钮(全屏 connect 见 Task 8
auto* viewHeader = geopro::app::buildPanelHeader(
geopro::app::Glyph::Map, QStringLiteral("VTK视图"),
{{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}});
// 左侧内嵌三栏抽屉(自带折叠按钮)+ 右侧 GL 画布,水平并列(非 GL 覆盖层,避免 z-order/圆角伪影)。
auto* drawer = new geopro::app::ColumnDrawer(centerWidget);
auto* viewRow = new QHBoxLayout();
viewRow->setContentsMargins(0, 0, 0, 0);
viewRow->setSpacing(0);
viewRow->addWidget(drawer); // 左侧抽屉(自带折叠按钮)
viewRow->addWidget(vtkWidget, 1); // 右侧 GL 画布
centerLayout->addWidget(viewHeader);
centerLayout->addLayout(viewRow, 1);
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager切片基底
sceneView->onVolumeChanged = [interactionMgr, sceneView]() {
if (sceneView->hasVolume())
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
sceneView->currentColorScale(), sceneView->currentVmin(),
sceneView->currentVmax());
else
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
};
// ── 三栏抽屉信号 → 控制器/交互Task 7 接线)──────────────────────────────
auto* c3 = drawer->col3D();
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
&geopro::controller::VtkSceneController::setAxesMode);
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
&geopro::controller::VtkSceneController::setAxesUnit);
QObject::connect(c3, &geopro::app::Column3DDataset::verticalExaggerationChanged, sceneCtrl,
&geopro::controller::VtkSceneController::setVerticalExaggeration);
QObject::connect(c3, &geopro::app::Column3DDataset::viewRequested, sceneCtrl,
&geopro::controller::VtkSceneController::applyView);
QObject::connect(c3, &geopro::app::Column3DDataset::zoomInRequested, sceneCtrl,
&geopro::controller::VtkSceneController::zoomIn);
QObject::connect(c3, &geopro::app::Column3DDataset::zoomOutRequested, sceneCtrl,
&geopro::controller::VtkSceneController::zoomOut);
QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl,
&geopro::controller::VtkSceneController::fit);
// 渲染勾选的 3D 数据集:真实 ds id 直达控制器异步帘面路径
// setCheckedDatasets → Api3dRepository.loadSection(realId) → 真实 ERT 反演端点 → 真实帘面)。
QObject::connect(c3, &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl,
&geopro::controller::VtkSceneController::setCheckedDatasets);
// O点位置/字体本期 stubTODO P4弹框
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
[]() { /* TODO P4: O点位置弹框 */ });
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
[]() { /* TODO P4: 字体弹框 */ });
auto* ca = drawer->colAnalysis();
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget,
[interactionMgr](geopro::render::interact::SliceAxis axis) {
interactionMgr->addSlice(axis);
});
QObject::connect(ca, &geopro::app::Column3DAnalysis::detailRequested, &detailCtrl,
[&detailCtrl](const QString& dsId, const QString& ddCode, const QString& name) {
detailCtrl.openDataset(dsId, ddCode, name);
});
// ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token经同一共享 GeoLocalFrame 配准)──
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
// 当前底图选择(默认 天地图=Satellite对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。
auto basemapKind =
std::make_shared<geopro::app::TileBasemap::Kind>(geopro::app::TileBasemap::Satellite);
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::basemapChanged, basemap,
[basemap, basemapKind](int idx) {
// 地图下拉0 天地图(卫星影像) / 1 Google(暂未实现→隐藏) / 2 隐藏。
*basemapKind = (idx == 0) ? geopro::app::TileBasemap::Satellite
: geopro::app::TileBasemap::Hidden;
basemap->show(*basemapKind);
});
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
sceneView->onFrameReanchored = [basemap, basemapKind]() {
if (*basemapKind != geopro::app::TileBasemap::Hidden) basemap->show(*basemapKind);
};
// 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。
sceneView->onCameraChanged = [basemap]() { basemap->refresh(); };
// 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。
QObject::connect(drawer->col3D(), &geopro::app::Column3DDataset::verticalExaggerationChanged,
basemap, [basemap](double ve) { basemap->setVerticalExaggeration(ve); });
// 单一来源kVerticalExaggeration 一处定义,组合根下发到 控制器(上方259) / 底图 / UI 显示。
basemap->setVerticalExaggeration(kVerticalExaggeration);
drawer->col3D()->setVerticalExaggeration(kVerticalExaggeration);
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
// 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中;
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
auto* emptyState = new QFrame(centerWidget);
emptyState->setObjectName(QStringLiteral("centralEmpty"));
emptyState->setAttribute(Qt::WA_TransparentForMouseEvents);
// 背景取 canvas/bg(#0B1320)——与画布同色:在原生 GL 上覆盖透明无法生效(会回退成不透明浅底),
// 故用与画布等色的不透明底,卡片即「无缝隐形」,浅色提示字稳稳浮于深底(与左上视图详情浮层同法)。
geopro::app::applyTokenizedStyleSheet(
emptyState, QStringLiteral("#centralEmpty { background: {{canvas/bg}}; }"
"#centralEmpty QLabel { background: transparent; }"));
auto* esLay = new QVBoxLayout(emptyState);
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
geopro::app::space::kXl, geopro::app::space::kXl);
esLay->setSpacing(geopro::app::space::kMd);
esLay->setAlignment(Qt::AlignCenter);
auto* esIcon = new QLabel(emptyState);
esIcon->setPixmap(
geopro::app::makeGlyph(geopro::app::Glyph::Dataset,
geopro::app::tokenColor("canvas/text-dim"), 56)
.pixmap(56, 56));
esIcon->setAlignment(Qt::AlignCenter);
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
esTitle->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esTitle, QStringLiteral("color:{{canvas/text}}; font-size:%1px; font-weight:%2;")
.arg(geopro::app::scaledPx(geopro::app::type::kHeading))
.arg(geopro::app::type::kWeightSemibold));
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
"切到「三维视图」可叠加帘面、体素与地形图层"),
emptyState);
esHint->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esHint,
QStringLiteral("color:{{canvas/text-dim}}; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody)));
esLay->addWidget(esIcon);
esLay->addWidget(esTitle);
esLay->addWidget(esHint);
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
emptyCentering->reposition();
auto* vtkDock = new ads::CDockWidget(QStringLiteral("VTK视图"));
vtkDock->setWidget(centerWidget);
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
// ── 下方「数据详情」dock平面图表多 Tab 面板QGraphicsViewVTK 仅算几何)──
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
auto* detailPanel = new geopro::app::DatasetDetailPanel();
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
// ForceNoScrollArea禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。
// 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充;
// 需要时由内层(图表内容区)自行滚动,标题/页签固定。
auto* detailHeader = wrapWithHeader(
geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailPanel,
{{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}});
detailDock->setWidget(detailHeader, ads::CDockWidget::ForceNoScrollArea);
// 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
// 左上 dock对象树真实结构项目根 → GS → TM。被动视图数据由控制器推送。
auto* objectTree = new geopro::app::ObjectTreePanel();
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象"));
auto* objectBox = wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"), objectTree,
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}});
leftDock->setWidget(objectBox);
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget();
// 数据页签:树形列表(原版 el-table 树——派生数据挂源数据下,按 DsRow.parentId 嵌套)。
auto* datasetList = new QTreeWidget();
datasetList->setHeaderHidden(true);
datasetList->setColumnCount(1);
datasetList->setRootIsDecorated(true); // 显展开/折叠箭头
datasetList->setIndentation(geopro::app::scaledPx(14));
datasetList->setExpandsOnDoubleClick(false); // 双击=打开详情,不切展开(展开靠箭头)
datasetList->setSelectionBehavior(QAbstractItemView::SelectRows);
geopro::app::applyDatasetCardDelegate(datasetList);
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget();
geopro::app::applyDatasetCardDelegate(fileList);
datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据集"));
auto* datasetBox = wrapWithHeader(
geopro::app::Glyph::Dataset, QStringLiteral("数据集"), datasetTabs,
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
datasetDock->setWidget(datasetBox);
// 动态标题:选中测线后改为「数据集显示栏 · ERTx」(对齐原型)。
auto* datasetTitle = datasetBox->findChild<QLabel*>(QStringLiteral("panelTitle"));
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* exceptionPanel = new geopro::app::ObjectExceptionPanel();
auto* objAttrView = new geopro::app::ObjectAttrPanel(projectRepo);
auto anomalyPanel = geopro::app::buildTabbedPanel(
{{geopro::app::Glyph::Anomaly, QStringLiteral("对象异常"), exceptionPanel, true},
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrView, false}},
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
// colorize(C):异常计数用语义 warning“需注意”变体区别于普通中性计数徽标
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
// 注:徽标的填充/显隐由 exceptionTreeLoaded 连接驱动(勾选对象后按异常计数更新)。
if (anomalyBadge) {
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
anomalyBadge->style()->unpolish(anomalyBadge);
anomalyBadge->style()->polish(anomalyBadge);
}
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性"));
rightDock->setWidget(anomalyPanel.container);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
// 右下 dock属性数据集属性上半只读元字段 + 下半可编辑描述)。
auto* propView = new geopro::app::DatasetAttrPanel(projectRepo);
auto* propDock = new ads::CDockWidget(QStringLiteral("数据集属性"));
propDock->setWidget(
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propView));
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
// 抽成 lambdaADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏,
// 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。
const auto hideDockTitleBars = [&]() {
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
if (auto* area = d->dockAreaWidget())
if (auto* bar = area->titleBar()) bar->setVisible(false);
}
};
hideDockTitleBars();
// 中央渲染由 sceneCtrlVtkSceneController驱动勾选对象/2D-3D切换/图层勾选/主题 → 重建场景。
// (旧 rebuildCentral lambda + 裸 show* 标志已由控制器取代。)
// ── 单击左下数据列表的采集批次(DS) → 属性表单 + 聚焦详情已开页 ──
QObject::connect(datasetList, &QTreeWidget::itemClicked, datasetList,
[&nav, &detailCtrl, propView](QTreeWidgetItem* item, int) {
if (item->data(0, geopro::app::kDsLoadMoreRole).toBool()) {
nav.loadMoreData();
return;
}
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
if (dsId.isEmpty()) return;
nav.selectDataset(dsId); // 只读元字段表单datasetDetailLoaded
propView->selectDataset(dsId); // 可编辑描述:回填 + 启用保存
detailCtrl.focusDataset(dsId); // 单击=聚焦已开页
});
// ── 双击 → 打开/聚焦该数据集的详情图表页(拉真实反演剖面/散点/异常/色阶)──
QObject::connect(datasetList, &QTreeWidget::itemDoubleClicked, datasetList,
[&detailCtrl](QTreeWidgetItem* item, int) {
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName);
});
// ── 控制器信号 → 详情面板tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ──
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::datasetOpened,
detailPanel, &geopro::app::DatasetDetailPanel::onDatasetOpened);
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabReady,
detailPanel, &geopro::app::DatasetDetailPanel::onTabReady);
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabLoadStarted,
detailPanel, &geopro::app::DatasetDetailPanel::onTabLoadStarted);
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested,
detailPanel, &geopro::app::DatasetDetailPanel::focusDataset);
// ── 页签懒加载lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl,
&geopro::controller::DatasetDetailController::loadTab);
// ── 分页:分页器翻页/改每页条数 → 控制器按页加载 → 回填(同 tabReady 路径,刷新表格+分页器)──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabPageNeeded, &detailCtrl,
&geopro::controller::DatasetDetailController::loadTabPaged);
// context 用 detailPanel析构即自动断连避免野指针。window 比 detailPanel 活得久,
// 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
[&window, detailPanel](const QString& dsId, const QString& msg) {
detailPanel->onLoadFailed(dsId, msg);
window.statusBar()->showMessage(
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
});
// ── 详情面板切 Tab → 反向高亮数据集列表对应行 ──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged,
datasetList, [datasetList](const QString& dsId) {
for (QTreeWidgetItemIterator it(datasetList); *it; ++it)
if ((*it)->data(0, geopro::app::kDsIdRole).toString() == dsId) {
datasetList->setCurrentItem(*it);
break;
}
});
// ── 左上对象树勾选 → 拉取各 TM 的 ds 子树按维度分发到三栏列表spec §6.1/§8──
// 渲染由三栏勾选框驱动Task 7Column3DDataset::checkedDatasetsChanged → setCheckedDatasets
auto generation = std::make_shared<unsigned long long>(0);
QObject::connect(
objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window,
[&projectRepo, &nav, drawer, emptyState, generation](const QStringList& tmIds) {
const unsigned long long myGen = ++(*generation);
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
if (tmIds.isEmpty()) {
drawer->col3D()->setDatasets({});
drawer->col2D()->setDatasets({});
drawer->colAnalysis()->setDatasets({});
return;
}
// 多 TM 异步汇总:每个 TM 取整棵 ds 子树,全部回来后按维度分发到三栏。
auto acc = std::make_shared<std::vector<geopro::data::DsRow>>();
auto remaining = std::make_shared<int>(tmIds.size());
auto finish = [acc, drawer, generation, myGen]() {
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
drawer->col3D()->setDatasets(b.dim3D);
drawer->col2D()->setDatasets(b.dim2D);
drawer->colAnalysis()->setDatasets(b.analysis);
};
for (const QString& tm : tmIds) {
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(
nav.currentProjectId().toStdString(), tm.toStdString(), 2, 3, 1, 100000);
QObject::connect(req, &geopro::data::NavRequest::done, drawer,
[acc, remaining, finish](const QVariant& v) {
auto page = qvariant_cast<geopro::data::DsPage>(v);
acc->insert(acc->end(), page.rows.begin(), page.rows.end());
if (--(*remaining) == 0) finish();
});
QObject::connect(req, &geopro::data::NavRequest::failed, drawer,
[remaining, finish](const QString&) {
if (--(*remaining) == 0) finish(); // 单个失败不卡死,其余照常分发
});
}
});
// ── 启动:建立一次中央视图。三栏重构后删除了 2D/3D 切换,统一固定为三维视图
// (帘面默认开启 showCurtain_=true勾选 dd_section → 帘面)。无勾选 → 空场景 + 背景。
sceneCtrl->setViewMode(geopro::controller::ViewMode::View3D);
// VTK 背景随主题切换:控制器重渲染(走完整渲染路径、末尾必 Render
// context 用 sceneCtrl非 windowThemeManager 是进程级单例,连接须随 sceneCtrl 析构自动断开,
// 否则 window 析构期间 sceneCtrl(其孙级子对象)已销毁、主题异步变化会触悬垂指针。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, sceneCtrl,
[sceneCtrl]() { sceneCtrl->rebuild(); });
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
geopro::app::TopBar* topBar = nullptr;
{
auto* topChrome = new QWidget(&window);
auto* topLayout = new QVBoxLayout(topChrome);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->setSpacing(0);
topLayout->addWidget(geopro::app::buildMenuBar(topChrome));
topBar = new geopro::app::TopBar(topChrome);
topLayout->addWidget(topBar);
window.setMenuWidget(topChrome);
}
// ── 控制器 ↔ 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);
}
return loaded;
};
// 数据树的「加载更多」:末尾顶层项;已加载数=树中非"加载更多"项总数(含各层子节点)。
auto removeTreeLoadMore = [](QTreeWidget* tw) {
const int n = tw->topLevelItemCount();
if (n > 0 && tw->topLevelItem(n - 1)->data(0, geopro::app::kDsLoadMoreRole).toBool())
delete tw->takeTopLevelItem(n - 1);
};
// total = 根节点总数控制器按根分页loaded 也按「第一层节点(根)」计 → 加载更多/页签数一致。
auto addTreeLoadMore = [](QTreeWidget* tw, int total) {
int loaded = 0;
for (int i = 0; i < tw->topLevelItemCount(); ++i)
if (!tw->topLevelItem(i)->data(0, geopro::app::kDsLoadMoreRole).toBool()) ++loaded;
if (loaded < total) {
auto* m = new QTreeWidgetItem(tw);
m->setText(0, QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total));
m->setData(0, geopro::app::kDsLoadMoreRole, true);
m->setTextAlignment(0, Qt::AlignCenter);
}
return loaded;
};
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchWorkspace);
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchProject);
// 退出登录:清除记住的凭证(QtKeychain+QSettings) → 重启应用回到登录页。
QObject::connect(topBar, &geopro::app::TopBar::logoutRequested, &window, []() {
geopro::app::forgetSession();
QProcess::startDetached(QCoreApplication::applicationFilePath(),
QCoreApplication::arguments().mid(1));
qApp->quit();
});
// 设置:点齿轮 → 打开设置对话框(外观/关于)。
QObject::connect(topBar, &geopro::app::TopBar::settingsRequested, &window, [&window]() {
geopro::app::SettingsDialog dlg(&window);
dlg.exec();
});
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
[&projectRepo, &nav, topBar, &window]() {
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(dlg, &geopro::app::ProjectListDialog::projectChosen, &nav,
[&nav, topBar](const QString& id, const QString& name) {
topBar->setProjectButtonText(name);
nav.switchProject(id);
});
dlg->exec();
});
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, &nav,
&geopro::controller::WorkbenchNavController::selectObject);
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &nav,
&geopro::controller::WorkbenchNavController::setCheckedTms);
// 单击对象 → 对象属性面板渲染可编辑表单projectId 取当前项目;项目根只读占位)。
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectSelectedForEdit, objAttrView,
[objAttrView, objectTree, &nav](const QString& id, int confType,
const QString& typeId, const QString& name,
bool isRoot) {
objAttrView->loadObject(nav.currentProjectId(), typeId, id, confType, name,
isRoot, objectTree->parentObjectId(id));
});
// 当前选中的 TM idconfType==2 时记录,其它选中清空):数据集面板「上传」按钮据此定父对象。
auto currentTmId = std::make_shared<QString>();
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, &window,
[currentTmId](const QString& objectId, int confType) {
*currentTmId = (confType == 2) ? objectId : QString();
});
// 切项目/重建结构 → 旧选中 TM 失效,清空(避免「上传」按钮误用跨项目的 TM
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, &window,
[currentTmId](const QString&, const std::vector<geopro::data::StructNode>&) {
currentTmId->clear();
});
// ── 交互操作接线(对象/数据集视图:右键菜单 + 快速筛选 + 增删2D/3D 相关占位)────────
auto* anomalyTabGroup = anomalyPanel.tabGroup; // 捕指针anomalyPanel 为局部,勿按引用捕获)
// 状态栏轻提示toast 替代window 生命周期覆盖整个会话,按引用捕获安全)。
auto toast = [&window](const QString& msg) { window.statusBar()->showMessage(msg, 4000); };
// 表头操作按钮定位器:按 glyphId 在已包装面板表头里找具体 QToolButton。
auto findHeaderAction = [](QWidget* box, geopro::app::Glyph g) -> QToolButton* {
const int gid = static_cast<int>(g);
for (auto* b : box->findChildren<QToolButton*>(QStringLiteral("panelAction")))
if (b->property("glyphId").toInt() == gid) return b;
return nullptr;
};
// ── 全屏切换VTK视图 / 数据详情 表头右上角「全屏」按钮 ──────────────────────────
// 点击 → 目标 dock 全屏(隐藏其余所有 dock再点 → 还原(全部显示)。
// 使用 ADS CDockWidget::toggleView(bool) 控制可见性(标准 ADS APIv4+)。
{
const QList<ads::CDockWidget*> allDocks{vtkDock, detailDock, leftDock, datasetDock,
rightDock, propDock};
auto applyFullscreen = [](ads::CDockWidget* target,
const QList<ads::CDockWidget*>& all, bool on) {
for (ads::CDockWidget* d : all) {
if (d == target) continue;
d->toggleView(!on); // on=进入全屏→隐藏其它; off=还原→全部显示
}
};
auto* vtkFsBtn = findHeaderAction(viewHeader, geopro::app::Glyph::Fullscreen);
auto* detailFsBtn = findHeaderAction(detailHeader, geopro::app::Glyph::Fullscreen);
if (vtkFsBtn) {
vtkFsBtn->setCheckable(true);
QObject::connect(vtkFsBtn, &QToolButton::toggled, &window,
[applyFullscreen, vtkDock, allDocks, detailFsBtn, drawer](bool on) {
if (on && detailFsBtn && detailFsBtn->isChecked()) {
QSignalBlocker b(detailFsBtn);
detailFsBtn->setChecked(false);
}
// VTK 全屏含左侧三栏(drawer 本就在 vtkDock 内):进入时确保展开可见。
if (on) drawer->expand();
applyFullscreen(vtkDock, allDocks, on);
});
}
if (detailFsBtn) {
detailFsBtn->setCheckable(true);
QObject::connect(detailFsBtn, &QToolButton::toggled, &window,
[applyFullscreen, detailDock, allDocks, vtkFsBtn](bool on) {
if (on && vtkFsBtn && vtkFsBtn->isChecked()) {
QSignalBlocker b(vtkFsBtn);
vtkFsBtn->setChecked(false);
}
applyFullscreen(detailDock, allDocks, on);
});
}
}
// 对象树右键菜单动作路由。
QObject::connect(
objectTree, &geopro::app::ObjectTreePanel::contextActionRequested, &window,
[&nav, &projectRepo, &window, anomalyTabGroup, toast, objAttrView, objectTree](
const QString& action, const QString& id, int confType, const QString& typeId,
const QString& name) {
if (action == QStringLiteral("properties")) {
nav.selectObject(id, confType);
// 右键「属性」:用可编辑面板渲染(与左键单击同口径)。
objAttrView->loadObject(nav.currentProjectId(), typeId, id, confType, name, false,
objectTree->parentObjectId(id));
if (anomalyTabGroup)
if (auto* b = anomalyTabGroup->button(1)) b->click(); // 切到「对象属性」页签
} else if (action == QStringLiteral("exceptionDetail")) {
nav.showObjectExceptions(id, confType);
if (anomalyTabGroup)
if (auto* b = anomalyTabGroup->button(0)) b->click(); // 切到「对象异常」页签
} else if (action == QStringLiteral("delete")) {
const auto r = QMessageBox::question(
&window, QStringLiteral("删除确认"),
QStringLiteral("确定删除「%1」该操作不可撤销。").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (r == QMessageBox::Yes) nav.deleteObject(id, confType);
} else if (action == QStringLiteral("edit")) {
// 动态表单编辑器:拉 project/getDynamicForm 真实 schema 渲染可编辑表单;
// 确定→校验+提交PUTbody 为推断结构,确切性以服务端为准)→成功刷新结构。
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
&window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->editObject(typeId, id, confType, name, objectTree->parentObjectId(id));
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
[&nav, toast](int) {
toast(QStringLiteral("保存成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("newTm")) {
// 新建 TM对话框拉 tmList(全局方法类型)选类型 → getDynamicForm(type=2) → POST /tmObject。
// 父对象:在 GS/项目根上=该节点;在 TM 上=其父 GS/根(即新建同级 TM
const QString tmParent =
(confType == 2) ? objectTree->parentObjectId(id) : id;
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
&window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->newTm(tmParent);
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
[&nav, toast](int) {
toast(QStringLiteral("新建成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("newGs")) {
// 新建 GS对话框拉 gsList 选类型 → getDynamicForm(type=1) → POST /gsObject。
// 父对象 = 右键所在节点(GS/项目根) id。
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
&window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->newGs(id);
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
[&nav, toast](int) {
toast(QStringLiteral("新建成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("importDs")) {
// 导入 DSTM 右键 → 选数据类型/脚本/文件 → checkImport → importmultipart
auto* dlg = new geopro::app::ImportDatasetDialog(projectRepo, nav.currentProjectId(),
id, &window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(dlg, &geopro::app::ImportDatasetDialog::imported, &window,
[&nav, toast]() {
toast(QStringLiteral("导入成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("showHide") || action == QStringLiteral("locate")) {
toast(QStringLiteral("「%1」需要二维/三维视图,开发中").arg(name));
} else {
toast(QStringLiteral("该功能开发中,即将接入"));
}
});
// 对象属性面板保存成功 → toast + 刷新结构(重载当前项目,回填最新属性)。
QObject::connect(objAttrView, &geopro::app::ObjectAttrPanel::saved, &window,
[&nav, toast]() {
toast(QStringLiteral("保存成功"));
nav.switchProject(nav.currentProjectId());
});
// 数据集属性面板描述保存成功 → toast。
QObject::connect(propView, &geopro::app::DatasetAttrPanel::saved, &window,
[toast]() { toast(QStringLiteral("描述已保存")); });
// 增删改结果 → 状态栏反馈(成功后控制器已自行刷新)。
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::mutationSucceeded, &window,
[toast](const QString& msg) { toast(msg); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::mutationFailed, &window,
[&window](const QString& msg) {
auto* sb = window.statusBar();
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
sb->showMessage(QStringLiteral("操作失败:%1").arg(msg), 6000);
QTimer::singleShot(6000, sb, [sb]() { sb->setStyleSheet(QString()); });
});
// 对象树表头「筛选」按钮 → 快速筛选弹出菜单(按类型批量勾选/反选 TM
if (auto* objFilterBtn = findHeaderAction(objectBox, geopro::app::Glyph::Filter)) {
objFilterBtn->setToolTip(QStringLiteral("快速筛选"));
QObject::connect(objFilterBtn, &QToolButton::clicked, objectTree,
[objectTree, objFilterBtn]() {
QMenu m(objectTree);
m.addAction(QStringLiteral("全选测线"), objectTree,
[objectTree]() { objectTree->setAllTmsChecked(true); });
m.addAction(QStringLiteral("取消全选"), objectTree,
[objectTree]() { objectTree->setAllTmsChecked(false); });
m.addAction(QStringLiteral("反选"), objectTree,
[objectTree]() { objectTree->invertTmChecks(); });
m.exec(objFilterBtn->mapToGlobal(QPoint(0, objFilterBtn->height())));
});
}
// 对象树表头「新建对象」按钮 → 小菜单(新建检测对象/新建方法对象,复用右键 newGs/newTm 流程)。
// 父对象 = 当前选中节点;未选中则取项目根(由 ObjectTreePanel::currentParentForNew 决定)。
if (auto* objAddBtn = findHeaderAction(objectBox, geopro::app::Glyph::Plus)) {
objAddBtn->setToolTip(QStringLiteral("新建对象"));
QObject::connect(
objAddBtn, &QToolButton::clicked, objectTree,
[objAddBtn, objectTree, &projectRepo, &nav, &window, toast]() {
const QString parentId = objectTree->currentParentForNew();
if (parentId.isEmpty()) {
toast(QStringLiteral("请先选择项目"));
return;
}
// 与右键 newGs/newTm 完全一致的对话框流程(文案统一:新建检测对象/新建方法对象)。
auto openForm = [&projectRepo, &nav, &window, toast, parentId](bool gs) {
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
&window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
if (gs) dlg->newGs(parentId); else dlg->newTm(parentId);
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
[&nav, toast](int) {
toast(QStringLiteral("新建成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
};
// 按选中类型决定菜单项:选 项目根/GS → 新建GS+TM选 TM → 仅新建TM同级
// 父对象由 currentParentForNew() 统一给出TM→父GS、GS/根→自身、未选→根),三种情况均正确。
QMenu m(objectTree);
if (objectTree->currentSelectedConfType() != 2) // 非 TM可新建检测对象(GS)
m.addAction(QStringLiteral("新建检测对象"), objectTree,
[openForm]() { openForm(true); });
m.addAction(QStringLiteral("新建方法对象"), objectTree,
[openForm]() { openForm(false); });
m.exec(objAddBtn->mapToGlobal(QPoint(0, objAddBtn->height())));
});
}
// 模型/插件目录:全局一次性拉取并缓存,供数据集右键「插件」子菜单同步填充。
auto modelsCache = std::make_shared<std::vector<geopro::data::ModelInfo>>();
{
auto* mReq = projectRepo.listModelsAsync();
QObject::connect(mReq, &geopro::data::NavRequest::done, &window,
[modelsCache](const QVariant& v) {
*modelsCache = qvariant_cast<std::vector<geopro::data::ModelInfo>>(v);
});
}
// ── 数据集列表右键菜单(数据集详情 / 属性 / 插件 / 导出 / 删除)──
datasetList->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(
datasetList, &QWidget::customContextMenuRequested, datasetList,
[datasetList, &detailCtrl, &nav, &projectRepo, &window, toast, modelsCache, propView](
const QPoint& pos) {
QTreeWidgetItem* item = datasetList->itemAt(pos);
if (!item || item->data(0, geopro::app::kDsLoadMoreRole).toBool()) return;
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
if (dsId.isEmpty()) return;
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
QMenu menu(datasetList);
menu.addAction(QStringLiteral("数据集详情"), datasetList,
[&detailCtrl, dsId, ddCode, dsName]() {
detailCtrl.openDataset(dsId, ddCode, dsName);
});
menu.addAction(QStringLiteral("属性"), datasetList, [&nav, propView, dsId]() {
nav.selectDataset(dsId); // 只读元字段
propView->selectDataset(dsId); // 可编辑描述
});
menu.addSeparator();
QMenu* plugins = menu.addMenu(QStringLiteral("插件"));
if (modelsCache->empty()) {
plugins->addAction(QStringLiteral("(模型列表加载中…)"))->setEnabled(false);
} else {
for (const auto& m : *modelsCache) {
const QString mn = QString::fromStdString(m.scriptName);
plugins->addAction(mn, datasetList, [toast, mn]() {
toast(QStringLiteral("插件「%1」调用待接入").arg(mn));
});
}
}
menu.addAction(QStringLiteral("导出…"), datasetList, [toast]() {
// ds 右键上下文拿不到 tmTypeBaseConfId空配置打开会直接「加载模板失败」。
// 暂不开对话框提示改用批量导出ExportDatasetDialog 保留供有 confId 场景调用)。
toast(QStringLiteral("从此处导出暂不可用(缺方法配置),请从批量导出使用"));
});
menu.addSeparator();
menu.addAction(QStringLiteral("删除"), datasetList, [&nav, &window, dsId, dsName]() {
const auto r = QMessageBox::question(
&window, QStringLiteral("删除确认"),
QStringLiteral("确定删除数据集「%1」该操作不可撤销。").arg(dsName),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (r == QMessageBox::Yes) nav.deleteDataset(dsId);
});
menu.exec(datasetList->viewport()->mapToGlobal(pos));
});
// 数据集表头「筛选」按钮 → 按类型 + 创建日期快速筛选(客户端隐藏不匹配行;状态跨弹出保留)。
if (auto* dsFilterBtn = findHeaderAction(datasetBox, geopro::app::Glyph::Filter)) {
dsFilterBtn->setToolTip(QStringLiteral("快速筛选"));
auto hiddenTypes = std::make_shared<QSet<QString>>(); // 当前被取消勾选的类型
auto minDate = std::make_shared<QDate>(); // 创建日期下限(无效=不限)
auto reapply = [datasetList, hiddenTypes, minDate]() {
QSet<QString> visible;
for (const QString& x : geopro::app::collectDatasetTypeNames(datasetList))
if (!hiddenTypes->contains(x)) visible.insert(x);
geopro::app::applyDatasetFilter(datasetList, visible, *minDate);
};
QObject::connect(
dsFilterBtn, &QToolButton::clicked, datasetList,
[datasetList, dsFilterBtn, hiddenTypes, minDate, reapply]() {
const QStringList types = geopro::app::collectDatasetTypeNames(datasetList);
QMenu m(datasetList);
if (types.isEmpty()) {
m.addAction(QStringLiteral("(当前无数据集)"))->setEnabled(false);
}
for (const QString& t : types) {
QAction* a = m.addAction(t);
a->setCheckable(true);
a->setChecked(!hiddenTypes->contains(t));
QObject::connect(a, &QAction::toggled, datasetList,
[hiddenTypes, reapply, t](bool on) {
if (on) hiddenTypes->remove(t);
else hiddenTypes->insert(t);
reapply();
});
}
m.addSeparator();
QMenu* dm = m.addMenu(QStringLiteral("创建日期"));
dm->addAction(QStringLiteral("全部"), datasetList,
[minDate, reapply]() { *minDate = QDate(); reapply(); });
dm->addAction(QStringLiteral("近 7 天"), datasetList, [minDate, reapply]() {
*minDate = QDate::currentDate().addDays(-7);
reapply();
});
dm->addAction(QStringLiteral("近 30 天"), datasetList, [minDate, reapply]() {
*minDate = QDate::currentDate().addDays(-30);
reapply();
});
m.addAction(QStringLiteral("清除筛选"), datasetList,
[hiddenTypes, minDate, reapply]() {
hiddenTypes->clear();
*minDate = QDate();
reapply();
});
m.exec(dsFilterBtn->mapToGlobal(QPoint(0, dsFilterBtn->height())));
});
}
// 数据集表头「上传」按钮 → 导入数据集(复用 ImportDatasetDialog父对象=当前选中 TM
// 未选中 TM 时按钮禁用并提示先选方法对象(随选中变化动态启停)。
if (auto* dsUploadBtn = findHeaderAction(datasetBox, geopro::app::Glyph::Upload)) {
dsUploadBtn->setToolTip(QStringLiteral("导入数据集(需先选方法对象)"));
auto syncUploadEnabled = [dsUploadBtn, currentTmId]() {
const bool hasTm = !currentTmId->isEmpty();
dsUploadBtn->setEnabled(hasTm);
dsUploadBtn->setToolTip(hasTm ? QStringLiteral("导入数据集…")
: QStringLiteral("导入数据集(需先选方法对象)"));
};
syncUploadEnabled(); // 初始:未选 TM → 禁用
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, dsUploadBtn,
[syncUploadEnabled](const QString&, int) { syncUploadEnabled(); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded,
dsUploadBtn,
[syncUploadEnabled](const QString&,
const std::vector<geopro::data::StructNode>&) {
syncUploadEnabled();
});
QObject::connect(
dsUploadBtn, &QToolButton::clicked, &window,
[currentTmId, &projectRepo, &nav, &window, toast]() {
if (currentTmId->isEmpty()) {
toast(QStringLiteral("请先选择方法对象TM再导入数据集"));
return;
}
auto* dlg = new geopro::app::ImportDatasetDialog(
projectRepo, nav.currentProjectId(), *currentTmId, &window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(dlg, &geopro::app::ImportDatasetDialog::imported, &window,
[&nav, toast]() {
toast(QStringLiteral("导入成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
});
}
// 控制器异常/数据集表单 → 被动面板。
// 对象属性改为可编辑面板:由 ObjectTreePanel::objectSelectedForEdit 直接驱动(见下),
// 不再消费只读的 objectDetailLoaded。
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::exceptionTreeLoaded,
exceptionPanel,
[exceptionPanel, anomalyBadge](
const std::vector<geopro::data::ObjectExceptionGroup>& groups, int total) {
exceptionPanel->setGroups(groups);
if (anomalyBadge) {
anomalyBadge->setText(QString::number(total));
anomalyBadge->setVisible(total > 0);
}
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetDetailLoaded, propView,
[propView](const geopro::data::DynamicForm& form) { propView->setForm(form); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::workspacesLoaded, topBar,
[topBar](const std::vector<geopro::data::Workspace>& list, const QString& cur) {
topBar->setWorkspaces(list, cur);
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar,
[topBar](const std::vector<geopro::data::ProjectSummary>& list,
const QString& cur, int total) {
topBar->setProjects(list, cur, total > static_cast<int>(list.size()));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
[objectTree, datasetList, fileList, datasetTitle, datasetTabs, exceptionPanel,
objAttrView, propView, anomalyBadge](
const QString& projectName,
const std::vector<geopro::data::StructNode>& nodes) {
objectTree->setStructure(projectName, nodes);
datasetList->clear();
fileList->clear();
exceptionPanel->showMessage(QStringLiteral("(勾选对象后显示其异常 / 异常体)"));
objAttrView->showMessage(QStringLiteral("(选中对象后显示其属性)"));
propView->showMessage(QStringLiteral("(单击数据集查看属性)"));
if (anomalyBadge) anomalyBadge->setVisible(false);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(0, QStringLiteral("数据"));
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
removeTreeLoadMore(datasetList);
geopro::app::populateDatasetList(datasetList, rows, append);
const int loaded = addTreeLoadMore(datasetList, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("数据"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
[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, 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) {
if (stage == QStringLiteral("structure") ||
stage == QStringLiteral("projects"))
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
// 状态栏错误反馈:临时染 danger 红8s 后随消息超时还原全局中性色。
auto* sb = window.statusBar();
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
sb->showMessage(QStringLiteral("加载失败(%1%2").arg(stage, msg), 8000);
QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); });
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
[](bool busy) {
if (busy)
QApplication::setOverrideCursor(Qt::WaitCursor);
else
QApplication::restoreOverrideCursor();
});
// 底部状态栏常驻显示坐标系与世界系原点wayfinding用户随时知道当前空间基准
window.statusBar()->showMessage(
QStringLiteral("就绪 | 坐标系 %1 | 世界系原点 %2, %3")
.arg(QString::fromUtf8(kProjectCrs))
.arg(lat0, 0, 'f', 5)
.arg(lon0, 0, 'f', 5));
// ── dock 布局/窗口几何持久化 ──────────────────────────────────────────
// 恢复上次的停靠布局与窗口几何(须在全部 dock 创建后ADS 按 dock 标题作键匹配)。
{
const QSettings settings;
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
if (!geo.isEmpty()) window.restoreGeometry(geo);
// 注意ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局,
// 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v3")).toByteArray();
if (!dockState.isEmpty()) {
dockManager->restoreState(dockState);
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
hideDockTitleBars();
}
}
// 退出时保存当前布局与几何aboutToQuit 早于 window 析构dockManager/window 仍存活)。
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
QSettings settings;
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
settings.setValue(QStringLiteral("ui/dockState_v3"), dockManager->saveState());
});
}
} // namespace
namespace {
// 顶层异常护栏:任何 slot / 事件处理器抛出的 C++ 异常都会经过 QApplication::notify。
// 默认 Qt 不允许异常穿透事件循环 → terminate崩溃。这里拦截 + 记录(异常信息 + 接收者
// 对象类名 + 事件类型,足以定位崩点),并吞掉以**保证后端故障等异常不致整个客户端退出**。
// 注:吞异常后该次事件处理中断,可能留下局部不一致;但"不崩 + 有日志"优先于直接退出。
class GuardedApplication : public QApplication {
public:
using QApplication::QApplication;
bool notify(QObject* receiver, QEvent* e) override {
try {
return QApplication::notify(receiver, e);
} catch (const std::exception& ex) {
qCritical("[guard] 拦截未捕获异常: %s | type=%s | receiver=%s | event=%d",
ex.what(), typeid(ex).name(),
receiver ? receiver->metaObject()->className() : "null",
e ? static_cast<int>(e->type()) : 0);
} catch (...) {
qCritical("[guard] 拦截未捕获非 std 异常 | receiver=%s | event=%d",
receiver ? receiver->metaObject()->className() : "null",
e ? static_cast<int>(e->type()) : 0);
}
return false;
}
};
} // namespace
int main(int argc, char* argv[])
{
// Qt WebEngine地图页签的 QWebEngineView必须在 QApplication 构造前初始化,
// 且需启用跨上下文共享 OpenGLQtWebEngine 与 QVTK 同进程共用 GL context避免黑屏/崩溃)。
// AA_ShareOpenGLContexts 须在 QApplication 之前设置QtWebEngineQuick::initialize() 同样须前置。
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QtWebEngineQuick::initialize();
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
// 必须在 QApplication 构造前设置。
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
GuardedApplication app(argc, argv); // 顶层异常护栏slot/事件里的异常不致客户端崩溃
// 异步 ApiCall::finished 等信号携带 ApiResponse注册元类型以支持跨 QueuedConnection 传递
// (当前详情链路为同线程 DirectConnection非严格必需但作防御性注册见 spec §5.1)。
qRegisterMetaType<geopro::net::ApiResponse>();
// 组织/应用名QSettings 持久化dock 布局、登录记忆等)按此定位存储位置。
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
// 日志 + 崩溃捕获:尽早安装(依赖上面的 Org/App 名定位日志目录)。生产桌面端问题可回溯。
geopro::app::initLogging();
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。
// 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。
geopro::app::applyPersistedThemeMode();
geopro::app::applyPersistedFontScale();
geopro::app::applyThemeMode(app, geopro::app::isDarkTheme());
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
if (qEnvironmentVariableIsEmpty("PROJ_DATA")) {
const QString appDir = QCoreApplication::applicationDirPath();
const QStringList candidates = {
appDir + "/proj",
appDir + "/../../vcpkg_installed/x64-windows/share/proj",
QStringLiteral(
"D:/Git/lanbingtech/geopro/build/release/vcpkg_installed/x64-windows/share/proj"),
};
for (const auto& c : candidates) {
if (QFile::exists(c + "/proj.db")) {
qputenv("PROJ_DATA", c.toUtf8());
break;
}
}
}
// 网络层:共享会话 ApiClient + 登录编排 AuthServiceRSA 公钥从 resources 读取)。
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");
geopro::net::AuthService auth(api, pem);
// 记住登录:若上次勾选「记住」且未超 30 天,凭证库里有有效 token → 免登录直接进。
QString token = geopro::app::recallValidToken(30);
if (token.isEmpty()) {
geopro::app::LoginWindow login(auth);
if (login.exec() != QDialog::Accepted) return 0;
token = login.token();
if (login.remember())
geopro::app::rememberSession(token); // 安全存 token + 时间戳
else
geopro::app::forgetSession(); // 未勾选:清除旧记忆
}
api.setToken(token); // 注入 token 供后续 API 使用
// 登录成功 → 构建并显示工作台。
geopro::data::LocalSampleRepository repo(
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
// 导航仓储 + 控制器(接口/逻辑层):用同一共享会话 ApiClient。
geopro::data::ApiProjectRepository projectRepo(api);
geopro::controller::WorkbenchNavController nav(projectRepo);
// 数据详情仓储 + 控制器(接真实反演 API同一共享会话 ApiClient。
geopro::data::ApiDatasetRepository datasetRepo(api);
// 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
geopro::controller::ChartStrategyRegistry chartRegistry;
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::TrajectoryStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GridStrategy>());
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其
// setCentralWidget/setMenuWidget/statusBar 承载工作台。
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
auto* window = new QMainWindow;
window->setWindowTitle(kTitle);
window->resize(1280, 800);
window->setMinimumSize(1024, 680);
buildWorkbench(*window, repo, projectRepo, datasetRepo, nav, detailCtrl);
// 主题桥ThemeManager 明/暗切换 → 重应用全局 QSS+调色板(标准控件 + ADS内联 chrome 经各自连接)。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
window, [&app]() { geopro::app::applyThemeMode(app, geopro::app::isDarkTheme()); });
// 主题切换快捷键 Ctrl+Shift+T持久化设置→外观 亦可改)。
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), window);
QObject::connect(themeSc, &QShortcut::activated, window, [] {
geopro::app::setThemeModePreference(geopro::app::isDarkTheme() ? QStringLiteral("light")
: QStringLiteral("dark"));
});
window->show();
nav.start(); // 进入工作台后拉真实 空间/项目/结构
return app.exec();
}