feat(ui): 工作台与登录 UI/UX 整体重构,对齐 Web 原型

- 登录窗品牌化重设计(头部横幅+纵向字段),移除默认账号密码
- 全局浅色主题精致化:基准字体 10pt、去线框留白、下划线标签页、扁平分隔条、High-DPI 直通、压平 Fusion 3D 立体描边
- 顶部:菜单栏(视图/项目管理/业务工具/设备 多级菜单)+ 工具条(工作空间切换/项目选择/帮助·通知·设置/用户)
- ADS 子窗口去除关闭·浮动·拖动等操作并隐藏自带标题栏;改用自绘面板表头(图标+标题+操作按钮)
- 右侧异常列表/对象属性合并为 Tab 面板,属性独立面板;数据集动态标题+数量徽标
- 矢量图标体系(Lucide SVG via QSvgRenderer);状态栏常驻坐标系/世界系原点
- 对象树修复:单标题、保留折叠箭头、选中高亮不覆盖缩进列
This commit is contained in:
gaozheng 2026-06-08 21:06:59 +08:00
parent e27a93073a
commit 72761fc05f
15 changed files with 1779 additions and 74 deletions

View File

@ -67,9 +67,10 @@ setx VCPKG_ROOT "C:\dev\vcpkg" # 永久(新开终端生效)
**用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用): **用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用):
1. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。 1. `.\qt-online-installer-windows-x64-4.11.0.exe --mirror https://ftp.jaist.ac.jp/pub/qtproject`
2. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。 2. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。
3. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。 3. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。
4. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。
CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。 CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。

View File

@ -0,0 +1,376 @@
# Geopro 3.0 桌面客户端 — M1 架构设计
**日期**2026-06-07
**版本**v2已按双专家评审 + 数据核验修订;修订点见 §16
**状态**:待用户复核 v2
**范围**M1 里程碑 = 完整工作台外壳 + 登录功能 + 三维视图(基础渲染 / dd_voxel 体绘制与切片 / DEM 地形)
**上位文档**`docs/Geopro3.0_技术选型与架构规约.md`(技术基线,本文遵从其全部约束)
---
## 1. 目标与范围
### 1.1 M1 交付目标
复刻 Geopro 3.0 最核心的「项目分析视图」桌面版,并把登录做实:
1. **登录功能完全可用**:真实连接生产后端(`pop-api`),走验证码 + RSA 加密密码流程token 安全存储。登录页样式参考现有 web 系统。
2. **完整工作台外壳**ADS 三区停靠布局,还原原型(左:对象树 + 数据集列表2D/3D 视图 + 数据详情;右:异常列表/对象属性 + 属性)。
3. **三维视图**M1 核心难点):
- ① 基础渲染:剖面散点、网格等值面/等值线/标注、异常圈定(直接渲染)
- ② dd_voxel 体绘制 + 鼠标交互切片dd_slice——C++ 进程内三维插值;**追求可信体**(非演示性),故对输入数据有要求(见 §10、§13 分阶段)
- ④ DEM 地形起伏 + 影像贴图
- 二维俯视相机预设(验证「单一场景」架构)
4. **业务数据来源**:登录联网;工作台业务数据 M1 用本地样本文件,经 Repository 抽象注入,未来无缝切 API。
### 1.2 不在 M1 范围
- ③ 雷达单/多通道渲染、⑤ 在线底图瓦片 → M1.5
- 反演/数据处理算法本体M1 只做「展示期插值」,不做反演)
- 项目管理、设备连接、在线监测、报告、平台后台、Web 端
- 完整算法插件架构(进程隔离 + manifest→ 规约 D-3 推迟M1 仅以 `IInterpolator` 接口预留
- 在线更新三通道(规约 §8
- macOS 构建M1 先 Windows / MSVC 2022架构保持跨平台可移植
---
## 2. 关键决策记录
| 编号 | 决策 | 结论 |
|---|---|---|
| K-1 | 2D/3D 视图架构 | **单一 VTK 三维场景 + 相机预设切换**;底图做可插拔 GroundLayer规约 §5.3、D-5。被现有 web「Cesium 单 3D 引擎」实践印证。 |
| K-2 | 启动节奏 | 先出设计文档 → **spike 预研门槛** → 再写完整实现计划 |
| K-3 | M1 外壳范围 | 完整工作台A 方案) |
| K-4 | 业务数据来源 | 登录走真实 API业务数据本地样本 + Repository 抽象B 方案) |
| K-5 | M1 三维内容 | ① 基础 + ② dd_voxel可信体图分阶段+ ④ DEM③ 雷达、⑤ 底图瓦片留 M1.5 |
| K-6 | 三维插值实现 | C++ 进程内IDW 起步),`IInterpolator` 接口隔离、**返回 core 中立类型**推迟完整插件架构D-1/D-3 |
| K-7 | 坐标系 | **每数据源各记源 CRS + 各自 LocalFrame** → 统一 rebase 到唯一「项目世界系(局部米,含 Z 基准GIS/经纬/底图用 PROJ 实时换算(见 §5 |
| K-8 | 构建/部署 | **方案②-修订**(经双专家评审+实机勘验改定):单一 Qt = **官方 MSVC 预编译 Qt**(`D:\Qt\6.11.1\msvc2022_64`);**VTK/ADS/QtKeychain 对接该官方 Qt**(VTK 源码编到 install 前缀、ADS/QtKeychain 走 FetchContent),**绝不走 vcpkg**(否则 vcpkg 再拉一份 Qt = 双份);仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg。**关键事实**:用户原装的 `D:\Qt\6.11.1`**MinGW 版**,MSVC 下不可链,须补装 MSVC kit;VTK 无 MSVC 预编译、三方案均须源码编;VS18=MSVC 14.51 链官方 Qt(v143)属"新链旧"ABI 安全。 |
| K-9 | 视图 widget | 评估 **`QVTKOpenGLStereoWidget`QOpenGLWidget 系)** 优先于 native缓解 ADS reparent 上下文丢失spike 验证) |
| K-10 | dd_voxel 可信度 | 维持可信体目标可信度取决于输入数据充分性≥3 非共线剖面或 3D 网格),列为数据依赖(见 §10、§14 |
---
## 3. 分层架构与目录结构
遵循规约 §10.3 清晰分层core / data / view / controller / app细分 net、render。
```
geopro/
├─ CMakeLists.txt / CMakePresets.json / vcpkg.json
├─ .clang-format / .clangd # AI 编码上下文基础设施(规约 §10.1
├─ cmake/ # Find 模块、打包、dll 部署
├─ src/
│ ├─ core/ # 纯业务,零 Qt / 零 VTK 依赖(可独立单测)
│ │ ├─ model/ # Project, GsObject, TmObject, DsObject, Anomaly, ColorScale, Grid, ScatterField, ScalarVolume
│ │ ├─ geo/ # LocalFrame原点+Z基准+轴向、CrsTransformPROJ 封装,多 CRS
│ │ └─ algo/ # IInterpolator 接口 + IdwInterpolator返回 core 的 ScalarVolume绝不含 VTK
│ ├─ data/ # 数据访问层(异步契约)
│ │ ├─ repo/ # IProjectRepository, IDatasetRepositoryQFuture/回调 + 取消 + 分页)
│ │ ├─ local/ # LocalSampleRepositoryQtConcurrent 线程池跑解析)+ 各格式解析器
│ │ ├─ api/ # ApiRepositoryM1 骨架,签名对齐)
│ │ └─ dto/ # 后端 JSON DTO + → model 映射
│ ├─ net/ # ApiClientQtNetwork/ AuthService验证码+RSA+login2/ CredentialQtKeychain
│ ├─ render/ # VTK 渲染层(独占 vtkRenderWindow统一管理所有 actor
│ │ ├─ Scene # 场景图、世界坐标空间、可见性;持有 RenderWindow
│ │ ├─ actors/ # ScatterActor, GridContourActor, VoxelVolumeActor, AnomalyActor, TerrainActor
│ │ ├─ color/ # ColorLutBuildercolorBar → vtkLookupTable 离散阶梯), ScalarBar
│ │ ├─ camera/ # CameraPresetTop2D / Free3D
│ │ ├─ interact/ # InteractionManager + InteractionToolMeasureTool/SliceTool/PickSelectTool
│ │ └─ ground/ # IGroundLayer + DemImageGroundLayerM1TileGroundLayerM1.5 预留)
│ ├─ view/ # QtWidgets 视图(被动;持有 VTK widget 外壳,不 new actor
│ │ ├─ login/ # LoginWindow样式参考 web
│ │ ├─ panels/ # ObjectTreePanel, DatasetListPanel, MapViewPanel(QVTKOpenGLStereoWidget),
│ │ │ # DataDetailPanel, AnomalyPanel, ObjectPropertyPanel, PropertyPanel
│ │ └─ widgets/ # ColorScaleEditor, ToolbarBits
│ ├─ controller/ # 联动编排(按交互闭环拆分,避免 God Object
│ │ ├─ SelectionController # 勾选/选中状态
│ │ ├─ RenderSyncController # 状态→Scene 渲染同步
│ │ └─ DetailSyncController # 列表↔详情↔视图定位三向联动
│ └─ app/ # main / MainWindowADS 布局、主题)/ AppContextDI 根)
├─ resources/ # QSS 主题、QtAwesome、登录页素材
├─ tests/ # gtestcore/data/algo+ Qt Testview/controller
└─ docs/
```
**架构铁律(写入 .clangd 供 AI 读取)**
- `core` 绝不 `#include` 任何 Qt / VTK 头(含 `IInterpolator`,返回 `core::ScalarVolume`)。
- VTK actor / RenderWindow 一律由 `render` 层创建与持有;`view` 只持有 `QVTKOpenGLStereoWidget` 外壳,把其 interactor 注入 render并将**拾取/交互事件回流**给 controller见 §4.4),禁止直接 new actor。
- 数据流双向已显式化:`view → render`(交互注入)与 `render → controller`(拾取/选择出站信号)。
- 信号槽连接集中在各 `*Controller` / `MainWindow``wireUp()`
- 所有落盘路径经 `QStandardPaths`(规约 §7.1)。
---
## 4. 渲染核心:单一 VTK 场景K-1
> **⚠️ 实现修正2026-06-07经离屏 PNG 核对;权威做法以此为准,详见 `plans/2026-06-07-m1-view-redesign.md` + STATUS**
> 本节 §4.2「2D/3D 仅切相机预设、零数据重建」的理想对**当前 M1 测线数据不成立**:剖面是竖直帘面,俯视只剩一条发丝线 → 俯视图空白。M1 落地做法:
> - **二维地图****三维视图** 是**两种不同渲染内容**(非同一物体换相机):二维地图 = 测线 `lat/lon` 轨迹**线**(`MapLineActor`,俯视);三维视图 = 沿测线的**竖直帘面**(`CurtainActor`,z 取负、纵向夸张、分段色带)。
> - **数据详情**(独立 QVTK)才显示单条数据集的 **#18 平面反演剖面**(`GridContourActor`,y 取负、显式 structuredGrid、colorBar 真实分段值、纵向夸张)。
> - 坐标统一用 `core::GeoLocalFrame`(经纬→局部米)。**dd_voxel/dd_slice 搁置**(散点 projX/Y 真实 CRS 未确认,无法与 lat/lon 配准)。
> - K-1「单场景 + 相机预设」仍是**长期目标**,但需要从俯视/透视都可读的内容(如带底图的地面 + 测线落地线 + 帘面共存)才成立。
> - **等值线/体素着色必须用 colorBar 真实非均匀分段值**(均匀分级会一片蓝)。**渲染改动必须用 `tests/spike/render_verify.cpp` 离屏 PNG 核对**。
### 4.1 Scene 与 RenderWindow 所有权
- `render::Scene` 持有**唯一** `vtkRenderWindow` + `vtkRenderer` + 项目世界坐标空间里的全部 actor维护当前色阶与坐标系。
- **单一 `QVTKOpenGLStereoWidget`**K-9QOpenGLWidget 系FBO 合成、reparent 友好)承载渲染窗口,**不放进 Tab**;中央面板的「二维/三维」是工具栏上的模式切换,不是两个 widget。
- view 仅持有该 widget 外壳RenderWindow/Interactor 所有权归 render。
### 4.2 2D / 3D = 三要素组合
| 模式 | CameraPreset | InteractorStyle | 典型可见性 |
|---|---|---|---|
| 二维 | Top2D正交投影、俯视、Z 锁定 | Locked2D禁旋转平移/缩放/正南正北 | 地面 + 平面要素 + 俯视散点/网格 |
| 三维 | Free3D透视、自由轨道 | Orbit3D自由旋转/缩放/平移 | 全部 actor体素、剖面、地形起伏 |
切换 = 切相机预设 + 交互器样式 + 工具集 + actor 可见性,**零数据重建**。
### 4.3 数据 → VTK 管线映射(已按评审修正)
| 数据类型 | 来源 | VTK 管线(修正后) | 备注 |
|---|---|---|---|
| 剖面散点 | 剖面原数据2597 点) | `vtkPolyData`(verts) + `vtkLookupTable` 着色 | 图 #17 |
| 网格等值面/线/标注 | 网格数据(规则栅格 x[100]×y[22]v[22][100]z 抬升) | **`vtkImageData`origin+spacingz 抬升用 `vtkWarpScalar`)→ `vtkDataSetSurfaceFilter`/`vtkGeometryFilter` → `vtkBandedPolyDataContourFilter`(开 `GenerateContourEdgesOn()` 一次产 banded 面+等值线,共用阈值)→ `vtkLabeledDataMapper` 标注** | 图 #18**不可让 structured/image 直连 banded filter**B-1 |
| dd_voxel 体绘制 | 多剖面散点 → `IInterpolator``core::ScalarVolume` | `ScalarVolume`render 转)`vtkImageData` → `vtkGPUVolumeRayCastMapper` + 颜色/不透明度传递函数 | 图 #09插值域受限§10 |
| dd_slice 切片 | voxel + 受控切面 | **`vtkResliceCursorWidget` / `vtkImageReslice`**(受控正交/任意切片),随相机模式启停 | 替代 `vtkImagePlaneWidget`避免与交互器抢事件M-2 |
| 异常圈定 | 异常数据markType 1点/2线/3面 + legend + z/elevation | **按 markType 三条子管线**:点 `vtkGlyph3D`(pointShape)、线 polydata+dashed、面 `vtkPolygon`/`vtkTriangleFilter` 填充+边框;标注屏幕空间 billboard | legend 的 `*NoOpacity` 0100 → 归一 [0,1]z 取值同剖面 Z 基准§5 |
| DEM 地形 + 影像 | dem.tif + image.tif + tfw**可能异源 CRS** | GDAL 读 → **PROJ/GDAL 重投影到项目世界 CRS**`vtkImageData``vtkWarpScalar` 抬升 + 影像纹理 | 图 #05;影像实测为 EPSG:3857须重投影§5、M-1 |
| 色阶 | colorBar[值, 颜色] 阶梯 | `vtkLookupTable`(离散阶梯,取下界)+ `vtkScalarBarActor` | 见 §7 |
### 4.4 模态交互与拾取回流M-2、B-3
- `InteractionManager` 管理**模态工具**激活互斥与 VTK observer 优先级:`MeasureTool`、`SliceTool`、`PickSelectTool`。工具激活/退出负责其临时 actor 生命周期。
- 3D Widget切片与自定义 InteractorStyle 共享同一 interactor须显式管理 `SetEnabled()` 与事件优先级,避免抢事件。
- **拾取回流通道**`render` 拾取到对象 → 经 view 中转发出出站信号 → `DetailSyncController` → 列表/详情定位。此箭头在分层图中显式存在§3
### 4.5 GroundLayer 可插拔
`IGroundLayer { build(Scene&); setVisible(bool); }`M1 `DemImageGroundLayer`M1.5 `TileGroundLayer`。若 VTK 贴瓦片体验差D-5可仅替换二维为 MapLibre 而不动 data/render 的 actor 体系。
---
## 5. 坐标系设计K-7评审最大短板已重写
数据现实(已核验真实样本):
- 剖面/网格/异常:带 GIS 投影 `projectX`≈516868=**Easting**、`projectY`≈2494259=**Northing**;另带局部米 `xlist/ylist`(各数据集自原点起算)。
- ⚠️ **CRS 待确认(Phase 1 用 PROJ 实测纠正)**:`projectX/Y` 的真实 CRS **不是 EPSG:32649**。PROJ 实测 `(516868,2494259)` 在 EPSG:32649 下解出 lon≈**111.16°E**,而网格自带 lat/lon 是 **114.16°E**(docx 标明为**香港** Volia 数据,香港≈114°E)——真实 CRS 的中央经线在 ~114°E(疑为港式/自定义 TM)。**做底图/影像配准(M1.5)前必须向客户确认项目 CRS**。
- **对 M1 core 无影响**:`LocalFrame` 用相对米(减原点,CRS 无关),网格自带 lat/lon;`CrsTransform` 已实现并单测验证 PROJ 机制本身。
- 影像 `image.tfw`:原点 (12708343, 2577685) = **EPSG:3857Web 墨卡托)**,与剖面**不同投影**。
- 网格另带 `elevation[100]` / `lat/lon`经纬度EPSG:4326
- API 几何 `tm/geometry/get` 返回 **EPSG:4326**
**设计**
1. **唯一权威系 = 项目世界系**:局部米,含明确 Z 基准;选定一个工作平面 CRS默认项目 UTM如 EPSG:32649+ 双精度原点偏移。
2. **每数据源各记源 CRS + 源 LocalFrame**:领域模型为每个数据集保存其源 CRS 与(如有)自身局部原点。**不假设全项目单一 CRS**。
3. **统一 rebase 管线**(显式步骤,非一句话):任何几何进入 Scene 前,`CrsTransform`PROJ`源局部米 →(源原点)→ 源 CRS GIS → 项目世界 CRS →(减项目原点)→ 项目世界米`。多数据集因此对齐到同一世界系(解决 B-1 多原点冲突)。
4. **轴向约定钉死**world.x = Easting = `projectX`world.y = Northing = `projectY`world.z 向上为正、单位米。**解析器不信 `eastCoord/northCoord` 字段名**(实测与值颠倒),按 projectX/Y 取值单测对照B-2
5. **垂向Z基准统一**M-3`LocalFrame` 定义高程基准面、向上为正、单位米、可选垂向夸张 z-scale。网格 z剖面深度/构造面、DEM `elevation`(地表高程)、体素 Z 在进入 Scene 前统一归算到该基准,避免地形与剖面垂直穿插。
6. **影像/DEM 重投影**:装载时经 GDAL/PROJ 重投影到项目世界 CRS 再贴地,**不能简单减原点**M-1
7. **float 精度**:世界=局部米(小数值)从根本规避 VTK float 大坐标抖动。
---
## 6. 数据层RepositoryK-4异步契约
接口即按 **API 现实形态**定义(异步 + 分页 + 取消),本地实现用 QtConcurrent 满足同一签名M-1
```
IProjectRepository {
QFuture<Project> loadProject(id);
QFuture<vector<GsObject>> loadStructure(projectId); // GS/TM 树
}
IDatasetRepository {
QFuture<Page<DsObject>> listDatasets(tmObjectId, PageReq); // 分页
QFuture<ScatterField> loadScatter(dsId);
QFuture<Grid> loadGrid(dsId);
QFuture<ColorScale> loadColorScale(dsId);
QFuture<vector<Anomaly>> loadAnomalies(dsId);
QFuture<TerrainTile> loadTerrain(...);
// 大数据(体素/雷达):返回带取消句柄 + 进度回调M1.5 走 FlatBuffers/Protobuf 流
RequestHandle loadVolumeStream(dsId, sink, onProgress); // 可 cancel
}
```
- 切换 ds 时取消上一个未完成请求;列表类带游标/分页;大数据流式 + 进度 + 取消。
- **M1**`LocalSampleRepository` 读样本目录解析器映射成领域模型DTO ↔ model 在 `data/dto` 隔离)。
- **未来**`ApiRepository` 同签名对接 `pop-api`
### 6.1 样本文件 → 模型解析约定(已核对真实样本)
| 文件 | 结构 | 解析要点 |
|---|---|---|
| 剖面原数据N.txt | `{data:{min,max,projectXList,projectYList,vlist,xlist,ylist,hlist}}` | 2597 点local(x,y)+gis(projX=East,projY=North)+value |
| 剖面网格数据N.txt | `{data:{x[100],y[22],v[22][100],z[22][100],elevation[100],lat[100],lon[100],vmin,vmax,overlayCoordinate,overlayElevation}}` | **规则栅格**(dx≈0.709,dy≈0.704 恒定)→ vtkImageData**v/z 为 [j=y][i=x],灌点序 i 最快**;无顶层 min/max对未知字段宽容 |
| 剖面网格数据的色阶数据N.txt | `{data:{properties:{colorBar:[[值,rgba]],lineConfig,labelConfig,lvlMinMax}}}` | 17 段阶梯;`lineType` 实测 "solid"(以配置为准,勿硬编码 dashed |
| 剖面网格数据N——对应的异常圈定数据.txt | `{data:[{exceptionName,exceptionMarkType(1点/2线/3面),legend{point*/polyline*/polygon*},location:{coordinate[{x,y}]},zlist?,elevationList?,geographicalCoordinates{projectX,projectY,...}}]}` | 字段比早期列举多;`eastCoord/northCoord` 名值颠倒,按 projectX/Y 取 |
| dem.tif / image.tif / image.tfw | GeoTIFF + world file | **影像 tfw 为 EPSG:3857**GDAL 读 + PROJ 重投影到世界 CRS |
| test_001_A*.head/.data/.cor | GPR 原始462×4100×int16多通道分文件 | **属 M1.5 雷达LocalSampleRepository 不解析** |
---
## 7. 色阶ColorScale
`colorBar``[值, 颜色]` 阶梯数组(颜色支持 `#RRGGBB``rgba(r,g,b,a)`)。映射:值落相邻两 stop 间取**下界 stop** 颜色(阶梯,非线性插值)。
- **实现统一为离散 `vtkLookupTable`**贴合「取下界阶梯」语义2D/3D 共用同一可信源),显式定义 under低于首 stop/ over高于末 stop/ NaN 颜色。
- **alpha 量纲按色阶来源文件类型判定**(网格色阶 0255、LVL 色阶 01解析器入口带 source 标记,**不按数值范围猜**m-2
- `lineConfig`:等值线显隐/颜色/`lineType`(以配置为准)/zmin/zmax`labelConfig`:标注显隐/颜色;`equalAreaLayerCount`/`logLinesCount`。
- 视图层 `ColorScaleEditor`M1 读取与基本调整;命名保存对接后续色阶模板 API。
---
## 8. 登录与网络层M1 必做,真实流程已抓取)
### 8.1 已确认的生产实现细节
- **API 基址** `http://tenant.geomative.cn/pop-api`openresty 反代OpenAPI 的 `/admin/*`、`/business/*` 加 `/pop-api` 前缀)。
- **认证头** `geomativeauthorization: Geomative <token>`(不透明会话令牌,非 JWT
- **登录三步**:① `GET /business/system/personalUser/getImageCode`→验证码图+`codeId` → ② `POST /business/system/personalUser/verifyCodeCheck {code,codeId}` → ③ `POST /admin/tenant/auth/login2 {username, password=RSA加密, checkCode}`→token。
- **密码加密 = JSEncrypt RSA-2048**(前端 vendor 用 JSEncrypt 库;密文 base64 ~344 字符 = 256 字节。token 取响应 **`data.accessToken`**(值即 `"Geomative <hash>"`,存 web localStorage `token`)。
- 另有 `/email`、`/phone` 登录支线(非 M1
- 登录后:`getInfo` / `list-menus` / `enterprise/info` / `enterprise/joined/list`
### 8.2 实现要点
- `AuthService`:取验证码→展示→校验→**OpenSSL RSA** 加密密码→login2→持有 token。
- `Credential`**QtKeychain**token 存平台密钥库,严禁明文(规约 §7.4)。
- `ApiClient`:注入 `geomativeauthorization`、基址、超时、错误码、401 处理QtNetwork 原生。
- **登录窗 UI**:样式参考现有 web 登录页(实现阶段截图复刻)。
### 8.3 ⚠️ 前置确认项(与 RSA 同级M-5
抓取的真实流程里**未见 refresh-token 实际使用login2 只返不透明会话 token**。因此:
- **RSA 公钥已取得 ✅**(Phase 3,用 Playwright `page.route` 拦截 JS chunk 给 `setPublicKey` 注入 hook + 缓存绕过强制加载补丁版,触发一次真登录捕获)。RSA-2048 SPKI,存于 `resources/rsa_public_key.pem`。加密用 PKCS#1 v1.5(JSEncrypt 默认),`RsaEncryptor`(OpenSSL)已实现+单测。
- **token 生命周期 / 是否有 refresh 机制**待确认。据此二选一设计:
- (a) 有 refresh token → 标准静默刷新、401 静默续期。
- (b) 仅会话 token → 「免登录」= 持久化会话 token 至其有效期;**到期/401 引导用户重新登录(含验证码),不声称静默重登**。
- 本项在 spike/实现前向后端确认spec 不把「静默刷新」当既定能力。
---
## 9. UI 外壳完整工作台K-3
- **停靠框架**ADSLGPL规约 §6.2)。**VTK 面板默认不可浮动**(或浮动时占位、停靠回重建),缓解 reparent 上下文问题spike 验证M-4
- **三区布局**(还原原型):左(对象树 + 数据集列表)/ 中2D-3D 视图 + 数据详情)/ 右(异常-对象属性 + 属性)。
- **主题**QSS + QDarkStyleSheet 打底 + QtAwesome 图标。
- **布局持久化**ADS 透视图 + 窗口几何存 QSettingsWindows 强制 INI规约 §7.2)。
- **联动**controller 按闭环拆分§3勾选 GS/TM→按 dd 类型筛选 ds→勾选 ds→渲染列表↔详情↔视图定位三向色阶调整两视角实时更新。
---
## 10. 算法展示期三维插值K-6、K-10
- `core/algo/IInterpolator``core::ScalarVolume interpolate(const PointSet& pts, const GridSpec& spec)`——**返回 core 中立类型**dims/spacing/origin/double 数组),绝不含 VTKM-2。render 层 `VoxelVolumeActor``ScalarVolume`→`vtkImageData`。
- M1 实现 `IdwInterpolator`反距离加权Eigen 辅助2597 点规模**不需要 PCL/KD-tree**m-1
- **可信度与数据依赖K-10、B-3**:可信体素需 **≥3 条非共线剖面或真实 3D 网格dd_Property3D/ 反演网格** 输入。仅两条近平行剖面 IDW 会得到「夹层片状」幻影。故:
- 插值**限定在输入包络内 + 最大距离 clamp**,包络外置 blank/透明,避免 ray cast 渲染整盒幻影。
- M1 体绘制按 §13 分阶段:先在**充分输入**数据上出可信体;输入不足的复杂体后置。
- **数据依赖**需客户提供达到可信度的体素级输入数据≥3 剖面 / 3D 网格)——列入 §14 待办。
- **不做反演**上游、Python 生态 ResIPy 等),未来按规约 §8.3 进程隔离接入M1 仅接口预留。
---
## 11. 构建与依赖K-8方案②-修订:官方 MSVC Qt + 源码 VTK + vcpkg 非 Qt 依赖)
- **构建**CMake 3.21+。MSVC 工具集 **VS18 / 14.51**实机C++17生成 `compile_commands.json`
- **单一 Qt 纪律(核心)**:全链路只用**一份官方 MSVC 预编译 Qt**`D:\Qt\6.11.1\msvc2022_64`,经 `CMAKE_PREFIX_PATH`)。**凡依赖 Qt 的组件都不走 vcpkg**vcpkg 任何 Qt 依赖端口都会再编一份 qtbase = 双份冲突,已核 `ports/vtk/vcpkg.json``vtk[qt]`→`qtbase`+`qtdeclarative`)。
- **VTK**:无 MSVC 预编译,**必须源码编**。预先用官方 Qt 把 VTK 9.3 配置/编译/`install` 到 `external/vtk-install``-DVTK_GROUP_ENABLE_Qt=YES -DQt6_DIR=...`app 经 `VTK_DIR` `find_package(VTK)`。一次编好、隔离于 app 构建。
- **ADS / QtKeychain**:经 **FetchContent** 对接同一份官方 Qt体量小源码编可接受**不走 vcpkg**。
- **非 Qt 依赖经 vcpkg**GDAL/PROJ/OpenSSL/Eigen/spdlog/fmt/nlohmann-json/gtest这些不拉 Qt
- **M1 依赖矩阵**
| 依赖 | 来源 | 用途 | 许可证 |
|---|---|---|---|
| Qt 6.11.1msvc2022_64预编译 | 官方安装器(MSVC kit) | UI/网络/SQL/并发 | LGPLv3动态 商务 D-2 |
| VTK 9.3[qt,opengl] + gdal/proj 可选) | **源码编→install 前缀** | 三维渲染 + QVTK widget | BSD ✅ |
| ADSQt-Advanced-Docking-System | FetchContent对接官方 Qt | 停靠布局 | LGPL v2.1 ✅ |
| QtKeychain | FetchContent对接官方 Qt | 凭证存储 | BSD ✅ |
| gdal / proj | vcpkg | DEM/影像/坐标重投影 | MIT 类 ✅ |
| openssl | vcpkg | RSA/HTTPS | Apache 2.0 ✅ |
| eigen3 | vcpkg头文件 | 数值/插值 | MPL2 ✅ |
| spdlog / fmt | vcpkg | 日志 | MIT ✅ |
| nlohmann-json | vcpkg头文件 | JSON | MIT ✅ |
| gtest | vcpkg | 单测 | BSD ✅ |
- ~~PCL~~**M1 移除**(点规模不需要)。
- **ABI**:官方 Qt 为 MSVC 2022(v143)预编译,本机 VS18(14.51)编 VTK/app/ADS/QtKeychain"新链旧"在 MSVC v14x 兼容区内**安全**,全程动态 CRT `/MD[d]`、Release 链 Release、Debug 链 Debug,不跨配置混链。
- 部署:用**官方 Qt 的 `windeployqt`**(`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe`)部署 Qt + 插件;VTK/vcpkg dll 用 `TARGET_RUNTIME_DLLS` 拷贝。确保 exe 目录只有这一份 Qt。
- **二进制缓存**vcpkg 实测无缓存,落地前先配 `VCPKG_BINARY_SOURCES`(省 GDAL/PROJ 等重编)。
- 环境从零搭建:见 `docs/ENV_SETUP_Windows.md`(方案②-修订)。
---
## 12. 测试策略(规约 §10.2
- `core`/`data`/`algo`gtest坐标 rebase/轴向、Z 基准归算、colorBar LUT 映射、v[j][i] 灌点序、样本解析、IDW 正确性)。
- `view`/`controller`Qt Test联动、模态工具互斥
- 失败路径必测:登录失败/验证码错/token 失效、样本缺失/损坏、空数据集、异源 CRS 重投影。
- 渲染以小基准截图人工核对(对照图 #17/#18/#09)。
- clang-tidy + cppcheck 入 CIDebug 启用 ASan/UBSan。
---
## 13. M1 验收标准图分阶段K-5/K-10
**M1-a先达成**
1. 启动→登录窗(近 web→真连 `pop-api` 登录成功。
2. 完整工作台 ADS 三区停靠;对象树/数据集来自本地样本。
3. ① 渲染剖面散点(图 #17 样)、网格 banded 等值面+等值线+标注(图 #18 样)、异常按 markType 圈定。
4. ④ DEM 地形起伏 + 影像(经重投影对齐)。
5. 色阶离散 LUT 可调,两视角实时联动;二维俯视相机预设可切。
**M1-b在充分输入数据上达成可信体**
6. 由 ≥3 剖面/3D 网格经 IDW 生成**可信** dd_voxel 体绘制,插值域受限;`vtkResliceCursorWidget` 交互切片得 dd_slice。
**通用**core 单测通过clang-tidy 无新增告警;布局/偏好持久化生效2D/3D 切换零数据重建、坐标对齐正确。
---
## 14. 风险与待决(承接规约 §11
| 风险/待决 | 说明 | 处理 |
|---|---|---|
| D-2 Qt 许可证 | LGPL 动态 vs 商业 | M1 按 LGPL 动态链接;商务并行 |
| RSA 公钥来源 + token 生命周期 | 登录加密公钥、是否有 refresh | spike 前向后端确认§8.3 |
| 可信体输入数据 | 2 平行剖面不足以出可信体K-10 | 需客户提供 ≥3 剖面/3D 网格M1-b 验收依赖此 |
| ADS 端口 + QVTK reparent | vcpkg 端口可用性、浮动黑屏 | spike 门槛验证§15 |
| 全 vcpkg 首编译耗时 | VTK+Qt 编译久 | 二进制缓存 `VCPKG_BINARY_SOURCES` |
| 异源 CRS 配准 | 影像 3857 vs 剖面 32649 | GDAL/PROJ 重投影§5 |
| macOS / OpenGL 废弃 | 规约 §3.3 | M1 仅 Windows保持可移植 |
---
## 15. Spike 预研门槛K-2进入完整实现计划前必过
第一周先跑通三个高风险点,任一不过则调整方案后再展开计划:
1. **构建/部署 spike**:全 vcpkgqtbase + vtk[qt] 共用一份 Qt配置、编译、出 exe、单一链路部署、无双 Qt 冲突。
2. **UI 上下文 spike**ADSvcpkg 或 FetchContent+ `QVTKOpenGLStereoWidget`,验证停靠/浮动/重停靠不黑屏、相机预设切换稳定。
3. **渲染管线 spike**:用真实样本跑通 `vtkImageData(+warp) → geometry filter → vtkBandedPolyDataContourFilter(GenerateContourEdges) → 标注`,目视对照图 #18;散点 + 离散 LUT 色阶对照图 #17
---
## 16. v2 修订记录(对应评审)
- 网格管线改 `vtkImageData(+warp)→geometry filter→banded contour(GenerateContourEdges)`B-1/B-2 code
- 坐标系重写:多源 CRS + 各自 LocalFrame + 统一 rebase + 轴向钉死 + Z 基准 + 影像重投影B-1/B-2 arch、M-1/M-3 code、M-3 arch
- dd_voxel 维持可信体但列数据依赖、插值域受限、验收分阶段B-3 code、K-10
- Repository 改异步契约(分页/取消/流M-1 arch
- `IInterpolator` 返回 core `ScalarVolume`,去 VTKM-2 arch
- 交互:模态工具抽象 + 拾取回流 + 切片改 `vtkResliceCursorWidget`B-3 arch、M-2 code
- widget 改 `QVTKOpenGLStereoWidget` + VTK 面板不可浮动 + spikeM-4 code
- 构建改全 vcpkg已核 vtk[qt]→qtbase 依赖)、删 PCLM-5 code、m-1
- 登录 refresh/token 生命周期降为前置确认M-5 arch
- 色阶离散 LUT + under/over/NaN + alpha 按来源 + lineType 以配置为准m-1/m-2/m-4 各)。
- controller 拆 Selection/RenderSync/DetailSyncm-5 arch
- 新增 §15 spike 门槛K-2
---
*v2 经双专家评审 + 数据核验修订。下一步spike 预研 → writing-plans。*

View File

@ -0,0 +1,25 @@
对象:项目/gs/tm对象鼠标右键tm导入ds编辑在右侧面板直接做gs也有导入ds功能
数据集:单击更新属性,双击时打开或选中数据详情,选中三维视图;文件列表,双击按支持的文件格式预览,不支持的文件格式提示+下载;
二维三维视图对象视图中选中的TM需要使用树形列表展示可渲染图形的tm/ds树并支持选中或取消某个tm/ds决定是否渲染展示异常鼠标点选图形时如果在垂直屏幕方向有多个图形叠加需要把多个图形的信息列出来方便用户选中被遮住的图形
数据详情:
异常gs选择后展示下面的合集异常体不渲染通过类型或状态过滤支持多选异常创建异常体支持将异常拖入异常体
项目列表:新增项目,最后访问时间倒序;项目列表可切换地图模式;
分析视图、大屏视图web按项目类型设置绑定视图客户端可切换分析视图默认
个人资料:显示基本信息、修改邮箱、修改密码;
大文件上传ds中的地形文件需要走独立的接口

View File

@ -11,9 +11,14 @@ find_package(VTK REQUIRED COMPONENTS
FiltersModeling FiltersModeling
) )
find_package(nlohmann_json CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Svg)
add_executable(geopro_desktop WIN32 add_executable(geopro_desktop WIN32
main.cpp main.cpp
Theme.cpp
TopBar.cpp
Glyphs.cpp
PanelHeader.cpp
login/LoginWindow.cpp login/LoginWindow.cpp
panels/AnomalyListPanel.cpp panels/AnomalyListPanel.cpp
panels/DatasetListPanel.cpp) panels/DatasetListPanel.cpp)
@ -21,7 +26,7 @@ add_executable(geopro_desktop WIN32
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_desktop PRIVATE target_link_libraries(geopro_desktop PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg
${VTK_LIBRARIES} ${VTK_LIBRARIES}
ads::qt6advanceddocking ads::qt6advanceddocking
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json

153
src/app/Glyphs.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "Glyphs.hpp"
#include <QByteArray>
#include <QDir>
#include <QImage>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QString>
#include <QSvgRenderer>
namespace geopro::app {
namespace {
// 每个图标的 SVG 内容线性风格24x24 视窗,描边由外层注入颜色)。
// 路径取自 Lucide 图标集MIT 许可,业界通用),保证专业、清晰、语义正确。
QString svgPathFor(Glyph t)
{
switch (t) {
case Glyph::Tree:
return QStringLiteral(
"<path d='M21 12h-8'/><path d='M21 6H8'/><path d='M21 18h-8'/>"
"<path d='M3 6v4c0 1.1.9 2 2 2h3'/><path d='M3 10v6c0 1.1.9 2 2 2h3'/>");
case Glyph::Dataset:
return QStringLiteral(
"<ellipse cx='12' cy='5' rx='9' ry='3'/>"
"<path d='M3 5v14a9 3 0 0 0 18 0V5'/><path d='M3 12a9 3 0 0 0 18 0'/>");
case Glyph::Map:
return QStringLiteral(
"<path d='m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 "
"0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z'/>"
"<path d='m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65'/>"
"<path d='m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65'/>");
case Glyph::Detail:
return QStringLiteral(
"<path d='M3 3v18h18'/><path d='M18 17V9'/><path d='M13 17V5'/><path d='M8 17v-3'/>");
case Glyph::Anomaly:
return QStringLiteral(
"<path d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 "
"1.73-3Z'/><path d='M12 9v4'/><path d='M12 17h.01'/>");
case Glyph::Property:
return QStringLiteral(
"<circle cx='12' cy='12' r='10'/><path d='M12 16v-4'/><path d='M12 8h.01'/>");
case Glyph::Plus:
return QStringLiteral("<path d='M5 12h14'/><path d='M12 5v14'/>");
case Glyph::Filter:
return QStringLiteral(
"<polygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/>");
case Glyph::Upload:
return QStringLiteral(
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
"<path d='M17 8l-5-5-5 5'/><path d='M12 3v12'/>");
case Glyph::Download:
return QStringLiteral(
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
"<path d='M7 10l5 5 5-5'/><path d='M12 15V3'/>");
case Glyph::Collapse:
return QStringLiteral("<path d='m17 11-5-5-5 5'/><path d='m17 18-5-5-5 5'/>");
case Glyph::Workspace:
return QStringLiteral(
"<rect width='7' height='7' x='3' y='3' rx='1'/>"
"<rect width='7' height='7' x='14' y='3' rx='1'/>"
"<rect width='7' height='7' x='14' y='14' rx='1'/>"
"<rect width='7' height='7' x='3' y='14' rx='1'/>");
case Glyph::Folder:
return QStringLiteral(
"<path d='M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 "
"0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'/>");
case Glyph::Help:
return QStringLiteral(
"<circle cx='12' cy='12' r='10'/>"
"<path d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/><path d='M12 17h.01'/>");
case Glyph::Bell:
return QStringLiteral(
"<path d='M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9'/>"
"<path d='M10.3 21a1.94 1.94 0 0 0 3.4 0'/>");
case Glyph::Gear:
return QStringLiteral(
"<path d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 "
"0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 "
"1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73 "
".73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 "
"2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 "
"2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 "
".73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 "
"1-1-1.73V4a2 2 0 0 0-2-2z'/><circle cx='12' cy='12' r='3'/>");
}
return QString();
}
} // namespace
QIcon makeGlyph(Glyph type, const QColor& color, int px)
{
const QString svg =
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
"stroke='%1' stroke-width='2' stroke-linecap='round' "
"stroke-linejoin='round'>%2</svg>")
.arg(color.name(), svgPathFor(type));
QSvgRenderer renderer(svg.toUtf8());
// 以 3x 超采样渲染再设 devicePixelRatio保证在任意缩放/DPI 下都清晰。
constexpr qreal kSuper = 3.0;
const int dim = qRound(px * kSuper);
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&p, QRectF(0, 0, dim, dim));
p.end();
QPixmap pm = QPixmap::fromImage(img);
pm.setDevicePixelRatio(kSuper);
return QIcon(pm);
}
QString writeChevronIcon(bool open, const QColor& color)
{
constexpr int px = 16;
constexpr int kScale = 3;
QPixmap pm(px * kScale, px * kScale);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
QPen pen(color, px * kScale * 0.1);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
p.setPen(pen);
const double w = px * kScale;
const double h = px * kScale;
if (open) { // ▼ 向下
p.drawLine(QPointF(w * 0.32, h * 0.42), QPointF(w * 0.5, h * 0.60));
p.drawLine(QPointF(w * 0.5, h * 0.60), QPointF(w * 0.68, h * 0.42));
} else { // ▶ 向右
p.drawLine(QPointF(w * 0.42, h * 0.32), QPointF(w * 0.60, h * 0.5));
p.drawLine(QPointF(w * 0.60, h * 0.5), QPointF(w * 0.42, h * 0.68));
}
p.end();
const QString path =
QDir(QDir::tempPath()).filePath(open ? QStringLiteral("geopro_tree_open.png")
: QStringLiteral("geopro_tree_closed.png"));
pm.save(path, "PNG");
return path;
}
} // namespace geopro::app

41
src/app/Glyphs.hpp Normal file
View File

@ -0,0 +1,41 @@
#pragma once
// 程序绘制的扁平线性图标(无需图片资源):用于面板表头与表头操作按钮,
// 风格对齐原型(细线、圆角端点、单色随主题)。仅外观,无交互逻辑。
#include <QColor>
#include <QIcon>
#include <QString>
namespace geopro::app {
enum class Glyph {
// 面板表头图标
Tree, // 对象显示栏(层级)
Dataset, // 数据真实显示栏(数据集/数据库)
Map, // 二维/三维视图(图层)
Detail, // 数据详情(图表)
Anomaly, // 异常列表(警示三角)
Property, // 属性(信息)
// 表头操作按钮图标
Plus, // 新建/添加
Filter, // 筛选(漏斗)
Upload, // 上传
Download, // 导出/下载
Collapse, // 折叠(双箭头)
// 顶部应用栏图标
Workspace, // 工作空间2x2 宫格)
Folder, // 项目(文件夹)
Help, // 帮助(?
Bell, // 通知(铃铛)
Gear, // 设置(齿轮)
};
// 生成指定颜色、像素尺寸的图标(默认 16px内部按 2x 绘制保证清晰)。
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16);
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。
QString writeChevronIcon(bool open, const QColor& color);
} // namespace geopro::app

151
src/app/PanelHeader.cpp Normal file
View File

@ -0,0 +1,151 @@
#include "PanelHeader.hpp"
#include <QButtonGroup>
#include <QColor>
#include <QHBoxLayout>
#include <QLabel>
#include <QSize>
#include <QStackedWidget>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
namespace geopro::app {
namespace {
// ── 专业图标/字号尺寸(统一放大)──
constexpr int kHeaderHeight = 42;
constexpr int kTitleIcon = 20; // 表头标题图标
constexpr int kActionIcon = 19; // 表头操作按钮图标
constexpr int kTabIcon = 19; // Tab 图标
// 表头统一样式(标准表头 + Tab 表头共用)。
const char* kHeaderQss =
"#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }"
"#panelTitle { color:#1F2A3D; font-size:14px; font-weight:600; }"
"#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;"
" padding:1px 7px; font-size:12px; font-weight:600; }"
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
"QToolButton#panelAction:hover { background:#EEF3FB; }"
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
" padding:8px 4px; font-size:14px; }"
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
"QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:600;"
" border-bottom:2px solid #2D6CB5; }";
// 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。
QLabel* makeBadge(QWidget* parent)
{
auto* badge = new QLabel(parent);
badge->setObjectName(QStringLiteral("panelBadge"));
badge->setAlignment(Qt::AlignCenter);
badge->setMinimumWidth(16);
badge->setVisible(false);
return badge;
}
// 表头操作按钮(静态占位)。
QToolButton* makeActionButton(QWidget* parent, const HeaderAction& a)
{
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("panelAction"));
btn->setIcon(makeGlyph(a.first, QColor("#5A6B85"), kActionIcon));
btn->setIconSize(QSize(kActionIcon, kActionIcon));
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(a.second + QStringLiteral("(占位)"));
btn->setAutoRaise(true);
return btn;
}
} // namespace
QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<HeaderAction>& actions)
{
auto* header = new QWidget();
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
auto* lay = new QHBoxLayout(header);
lay->setContentsMargins(12, 0, 8, 0);
lay->setSpacing(8);
auto* iconLbl = new QLabel(header);
iconLbl->setPixmap(makeGlyph(icon, QColor("#44546B"), kTitleIcon).pixmap(kTitleIcon, kTitleIcon));
lay->addWidget(iconLbl);
auto* titleLbl = new QLabel(title, header);
titleLbl->setObjectName(QStringLiteral("panelTitle"));
lay->addWidget(titleLbl);
lay->addWidget(makeBadge(header)); // 默认隐藏,调用方可经 findChild 更新
lay->addStretch();
for (const auto& a : actions) lay->addWidget(makeActionButton(header, a));
return header;
}
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<HeaderAction>& actions)
{
auto* box = new QWidget();
auto* v = new QVBoxLayout(box);
v->setContentsMargins(0, 0, 0, 0);
v->setSpacing(0);
auto* header = new QWidget(box);
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
auto* hlay = new QHBoxLayout(header);
hlay->setContentsMargins(10, 0, 8, 0);
hlay->setSpacing(2);
auto* stack = new QStackedWidget(box);
auto* group = new QButtonGroup(box);
group->setExclusive(true);
TabbedPanel result;
result.container = box;
for (int i = 0; i < tabs.size(); ++i) {
const PanelTab& t = tabs[i];
auto* btn = new QToolButton(header);
btn->setObjectName(QStringLiteral("tabBtn"));
btn->setText(t.title);
btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), kTabIcon));
btn->setIconSize(QSize(kTabIcon, kTabIcon));
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
btn->setCheckable(true);
btn->setCursor(Qt::PointingHandCursor);
btn->setAutoRaise(true);
group->addButton(btn, i);
hlay->addWidget(btn);
QLabel* badge = nullptr;
if (t.hasBadge) {
badge = makeBadge(header);
hlay->addWidget(badge);
}
result.badges.append(badge);
stack->addWidget(t.content);
hlay->addSpacing(10);
}
hlay->addStretch();
for (const auto& a : actions) hlay->addWidget(makeActionButton(header, a));
QObject::connect(group, &QButtonGroup::idClicked, stack, &QStackedWidget::setCurrentIndex);
if (auto* first = group->button(0)) first->setChecked(true);
stack->setCurrentIndex(0);
v->addWidget(header);
v->addWidget(stack, 1);
return result;
}
} // namespace geopro::app

46
src/app/PanelHeader.hpp Normal file
View File

@ -0,0 +1,46 @@
#pragma once
// 面板自绘表头(对齐原型):
// - buildPanelHeader图标 + 标题 + (可选数量徽标) + 右侧操作按钮(标准面板)。
// - buildTabbedPanel表头为若干 Tab图标+标题+可选徽标)+ 右侧操作按钮,下方堆叠对应内容。
// 由调用方放在面板内容顶部,并隐藏 ADS 自带标题栏,从而完全掌控表头外观。
// 操作按钮当前为静态占位tooltip 标注),后续接真实功能。
#include <QPair>
#include <QString>
#include <QVector>
#include "Glyphs.hpp"
class QWidget;
class QLabel;
namespace geopro::app {
// 一个表头操作项:图标 + 提示文案。
using HeaderAction = QPair<Glyph, QString>;
// 标准面板表头。标题 QLabel objectName="panelTitle"、徽标 QLabel objectName="panelBadge"
// (默认隐藏),调用方可经 container->findChild 动态更新。
QWidget* buildPanelHeader(Glyph icon, const QString& title,
const QVector<HeaderAction>& actions = {});
// 一个 Tab 规格:图标 + 标题 + 内容 + 是否带数量徽标。
struct PanelTab {
Glyph icon;
QString title;
QWidget* content;
bool hasBadge;
};
// 带 Tab 表头的面板构建结果:容器 + 各 Tab 的徽标标签(无徽标处为 nullptr供动态更新
struct TabbedPanel {
QWidget* container;
QVector<QLabel*> badges;
};
// 构建带 Tab 表头的面板(首个 Tab 默认激活;点击 Tab 切换下方堆叠内容)。
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs,
const QVector<HeaderAction>& actions = {});
} // namespace geopro::app

428
src/app/Theme.cpp Normal file
View File

@ -0,0 +1,428 @@
#include "Theme.hpp"
#include <QApplication>
#include <QColor>
#include <QFont>
#include <QPalette>
#include <QStyleFactory>
namespace geopro::app {
namespace {
// 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。
// 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写,
// 就需要自带勾选 image否则勾选态会变成空白方块。这里交给 Fusion 原生绘制,
// 它会自动采用调色板的 Highlight(#2D6CB5) 作勾选色,省去打包图片资源。
const char* kStyleSheet = R"QSS(
/* ── 基础 ───────────────────────────────────────────────── */
QWidget {
color: #1F2A3D;
}
QMainWindow, QDialog {
background: #F4F6FA;
}
QToolTip {
background: #1F2A3D;
color: #F4F6FA;
border: 1px solid #2D6CB5;
border-radius: 4px;
padding: 4px 8px;
}
/* ── 视图内工具条2D/3D、数据详情白底分段控件柔和不刺眼 ── */
QToolBar {
background: #FFFFFF;
border: none;
border-bottom: 1px solid #EAEEF4;
padding: 6px 8px;
spacing: 4px;
}
QToolBar QToolButton {
background: transparent;
color: #5A6B85;
border: none;
border-radius: 7px;
padding: 6px 14px;
font-weight: 500;
}
QToolBar QToolButton:hover {
background: #EEF3FB;
color: #1F2A3D;
}
QToolBar QToolButton:pressed {
background: #DCE9F8;
}
QToolBar QToolButton:checked {
background: #EAF1FB;
color: #2D6CB5;
font-weight: 600;
}
QToolBar QToolButton:checked:hover {
background: #DCE9F8;
}
QToolBar::separator {
background: #EAEEF4;
width: 1px;
margin: 6px 8px;
}
/* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */
QTreeWidget, QListWidget, QTreeView, QListView {
background: #FFFFFF;
border: none;
padding: 6px;
outline: none;
}
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
padding: 7px 8px;
border-radius: 6px;
margin: 1px 4px;
}
QTreeWidget::item:hover, QListWidget::item:hover,
QTreeView::item:hover, QListView::item:hover {
background: #EEF3FB;
}
QTreeWidget::item:selected, QListWidget::item:selected,
QTreeView::item:selected, QListView::item:selected {
background: #DCE9F8;
color: #1B3D67;
}
/* 注意:不要给 QTreeView::branch 设 background——一旦改写 branchQt 会停止绘制
/ indicator */
/* 表头(对象显示栏) */
QHeaderView::section {
background: #EDF1F7;
color: #3A475C;
border: none;
border-bottom: 1px solid #D5DBE5;
padding: 6px 8px;
font-weight: 600;
}
/* ── 标签页(数据 / 文件):现代下划线 tab无边框盒子 ──────── */
QTabWidget::pane {
border: none;
border-top: 1px solid #EAEEF4;
top: 0;
background: #FFFFFF;
}
QTabBar {
background: transparent;
}
QTabBar::tab {
background: transparent;
color: #5A6B85;
border: none;
border-bottom: 2px solid transparent;
padding: 8px 16px;
margin-right: 4px;
}
QTabBar::tab:selected {
color: #2D6CB5;
border-bottom: 2px solid #2D6CB5;
font-weight: 600;
}
QTabBar::tab:hover:!selected {
color: #1F2A3D;
}
/* ── 复选框(仅调间距/字色indicator 交给 Fusion 原生)──── */
QCheckBox {
spacing: 7px;
color: #1F2A3D;
}
QCheckBox:disabled {
color: #9AA6B6;
}
/* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */
QPushButton {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #C2CCDA;
border-radius: 6px;
padding: 6px 14px;
}
QPushButton:hover {
background: #EEF3FB;
border-color: #2D6CB5;
}
QPushButton:pressed {
background: #DCE9F8;
}
QPushButton:default {
background: #2D6CB5;
color: #FFFFFF;
border-color: #2D6CB5;
}
QPushButton:default:hover {
background: #2862A6;
}
QPushButton:disabled {
background: #F0F2F6;
color: #9AA6B6;
border-color: #DCE0E7;
}
QLineEdit {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #C7D2E0;
border-radius: 6px;
padding: 5px 8px;
selection-background-color: #2D6CB5;
selection-color: #FFFFFF;
}
QLineEdit:focus {
border: 1px solid #2D6CB5;
}
QLineEdit:disabled {
background: #F0F2F6;
color: #8A93A3;
}
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
QScrollBar:vertical {
background: transparent;
width: 12px;
margin: 2px;
}
QScrollBar::handle:vertical {
background: #C2CCDA;
border-radius: 5px;
min-height: 28px;
}
QScrollBar::handle:vertical:hover {
background: #A7B4C7;
}
QScrollBar:horizontal {
background: transparent;
height: 12px;
margin: 2px;
}
QScrollBar::handle:horizontal {
background: #C2CCDA;
border-radius: 5px;
min-width: 28px;
}
QScrollBar::handle:horizontal:hover {
background: #A7B4C7;
}
QScrollBar::add-line, QScrollBar::sub-line {
width: 0;
height: 0;
}
QScrollBar::add-page, QScrollBar::sub-page {
background: transparent;
}
/* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */
QSplitter::handle {
background: #EAEEF4;
}
QSplitter::handle:hover {
background: #C7D2E0;
}
ads--CDockSplitter::handle {
background: #EAEEF4;
}
ads--CDockSplitter::handle:hover {
background: #C7D2E0;
}
/* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */
QStatusBar {
background: #FFFFFF;
color: #5A6B85;
border-top: 1px solid #EAEEF4;
}
QStatusBar::item {
border: none;
}
QStatusBar QLabel {
color: #5A6B85;
padding: 0 4px;
}
/* ── 菜单栏 / 菜单(按需出现时也与主题一致)────────────────── */
QMenuBar {
background: #EDF1F7;
color: #1F2A3D;
border-bottom: 1px solid #D5DBE5;
}
QMenuBar::item {
background: transparent;
padding: 5px 12px;
border-radius: 6px;
}
QMenuBar::item:selected {
background: #DCE6F4;
}
QMenu {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #D5DBE5;
border-radius: 8px;
padding: 5px;
}
QMenu::item {
padding: 6px 24px 6px 14px;
border-radius: 5px;
}
QMenu::item:selected {
background: #DCE9F8;
color: #1B3D67;
}
QMenu::separator {
height: 1px;
background: #E1E6EE;
margin: 5px 8px;
}
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
QComboBox {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #C2CCDA;
border-radius: 6px;
padding: 5px 10px;
min-height: 18px;
}
QComboBox:hover {
border-color: #2D6CB5;
}
QComboBox:focus {
border-color: #2D6CB5;
}
QComboBox::drop-down {
border: none;
width: 22px;
}
QComboBox QAbstractItemView {
background: #FFFFFF;
border: 1px solid #D5DBE5;
border-radius: 6px;
selection-background-color: #DCE9F8;
selection-color: #1B3D67;
outline: none;
}
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
QGroupBox {
border: 1px solid #D5DBE5;
border-radius: 8px;
margin-top: 10px;
padding-top: 6px;
font-weight: 600;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 4px;
color: #3A475C;
}
/* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */
QProgressBar {
background: #E6EBF3;
border: none;
border-radius: 5px;
height: 8px;
text-align: center;
color: #5A6B85;
}
QProgressBar::chunk {
background: #2D6CB5;
border-radius: 5px;
}
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
(//) +
线 + */
ads--CDockAreaWidget {
background: #F4F6FA;
}
ads--CDockAreaTitleBar {
background: #EDF1F7;
border-bottom: 1px solid #D5DBE5;
padding: 0;
}
ads--CDockWidgetTab {
background: #EDF1F7;
border: none;
border-bottom: 2px solid transparent;
padding: 7px 12px;
min-height: 22px;
}
ads--CDockWidgetTab[activeTab="true"] {
background: #EDF1F7;
border-bottom: 2px solid #2D6CB5;
}
ads--CDockWidgetTab QLabel {
color: #5A6B85;
font-weight: 600;
}
ads--CDockWidgetTab[activeTab="true"] QLabel {
color: #1F2A3D;
font-weight: 600;
}
)QSS";
// 浅色专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。
QPalette buildPalette()
{
QPalette p;
const QColor shell("#F4F6FA");
const QColor panel("#FFFFFF");
const QColor text("#1F2A3D");
const QColor mutedText("#5A6B85");
const QColor accent("#2D6CB5");
p.setColor(QPalette::Window, shell);
p.setColor(QPalette::WindowText, text);
p.setColor(QPalette::Base, panel);
p.setColor(QPalette::AlternateBase, QColor("#F0F3F8"));
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, QColor("#EDF1F7"));
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D"));
p.setColor(QPalette::ToolTipText, shell);
p.setColor(QPalette::Highlight, accent);
p.setColor(QPalette::HighlightedText, panel);
p.setColor(QPalette::PlaceholderText, mutedText);
p.setColor(QPalette::Link, accent);
// 关键:把 Fusion 用于绘制 3D 凹凸(斜角/凹槽/分隔条阴影)的明暗角色统一压成相近浅灰,
// 立体效果即塌成平面。ADS 分隔条用 palette(dark),这样也变成一条扁平浅灰细线(无 3D
p.setColor(QPalette::Light, QColor("#FFFFFF"));
p.setColor(QPalette::Midlight, QColor("#EEF1F5"));
p.setColor(QPalette::Mid, QColor("#E1E6EE"));
p.setColor(QPalette::Dark, QColor("#D7DEE8"));
p.setColor(QPalette::Shadow, QColor("#D7DEE8"));
// 禁用态:统一灰化,避免 Fusion 默认禁用色偏暗看不清。
p.setColor(QPalette::Disabled, QPalette::Text, QColor("#9AA6B6"));
p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#9AA6B6"));
p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#9AA6B6"));
return p;
}
} // namespace
void applyTheme(QApplication& app)
{
// Fusion跨平台一致且对 QSS 友好Windows 原生风对部分控件会忽略样式表)。
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
// 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。
// 10pt≈13px对齐主流商用客户端基准9pt 偏小显拥挤。抗锯齿优先,观感更精致。
QFont base(QStringLiteral("Microsoft YaHei UI"), 10);
base.setStyleStrategy(QFont::PreferAntialias);
app.setFont(base);
app.setPalette(buildPalette());
app.setStyleSheet(QString::fromUtf8(kStyleSheet));
}
} // namespace geopro::app

19
src/app/Theme.hpp Normal file
View File

@ -0,0 +1,19 @@
#pragma once
// 全局视觉主题浅色专业方向Fusion 风格 + 浅色 QPalette + 结构化 QSS。
// 仅外观——不改任何信号槽 / 渲染 / 数据逻辑。在 QApplication 构造后、弹登录窗前调用一次。
//
// 设计令牌(与登录窗、视图详情浮层共享,保证全项目一脉相承):
// 外壳底 #F4F6FA 面板白 #FFFFFF 抬升/表头 #EDF1F7
// 强调 #2D6CB5 悬停 #2862A6 按下 #234F87 选中行 #DCE9F8
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
// 危险 #C0392B
class QApplication;
namespace geopro::app {
// 应用浅色专业主题Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。
void applyTheme(QApplication& app);
} // namespace geopro::app

238
src/app/TopBar.cpp Normal file
View File

@ -0,0 +1,238 @@
#include "TopBar.hpp"
#include <QActionGroup>
#include <QColor>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QSize>
#include <QStringList>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
#include "Glyphs.hpp"
namespace geopro::app {
namespace {
// ── 专业图标尺寸(统一放大;菜单栏字号亦同步加大)──
constexpr int kToolIcon = 22; // 工具条右侧图标
constexpr int kWorkspaceIcon = 20; // 工作空间 / 项目图标
// 竖直分隔细线。
QFrame* makeDivider(QWidget* parent)
{
auto* line = new QFrame(parent);
line->setObjectName(QStringLiteral("topDivider"));
line->setFrameShape(QFrame::VLine);
line->setFixedWidth(1);
line->setFixedHeight(24);
return line;
}
// 右侧图标按钮(仅图标,悬停显示文本)。
QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip)
{
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("iconBtn"));
btn->setIcon(makeGlyph(g, QColor("#5A6B85"), kToolIcon));
btn->setIconSize(QSize(kToolIcon, kToolIcon));
btn->setToolTip(tip);
btn->setCursor(Qt::PointingHandCursor);
btn->setAutoRaise(true);
return btn;
}
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
QMenu* buildViewMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("视图"), p);
m->addAction(QStringLiteral("分析视图"));
m->addAction(QStringLiteral("大屏视图"));
return m;
}
QMenu* buildProjectMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("项目管理"), p);
m->addAction(QStringLiteral("数据视图"));
auto* cfg = m->addMenu(QStringLiteral("项目配置"));
cfg->addAction(QStringLiteral("基本信息"));
cfg->addAction(QStringLiteral("项目结构"));
cfg->addAction(QStringLiteral("视图配置"));
m->addAction(QStringLiteral("数据管理"));
auto* biz = m->addMenu(QStringLiteral("业务管理"));
biz->addAction(QStringLiteral("异常管理"));
biz->addAction(QStringLiteral("异常体管理"));
auto* mon = m->addMenu(QStringLiteral("在线监测"));
mon->addAction(QStringLiteral("项目设备"));
mon->addAction(QStringLiteral("在线任务管理"));
auto* doc = m->addMenu(QStringLiteral("项目资料管理"));
doc->addAction(QStringLiteral("项目资料管理"));
doc->addAction(QStringLiteral("报告列表"));
auto* tools = m->addMenu(QStringLiteral("工具组件"));
tools->addAction(QStringLiteral("装置与脚本"));
tools->addAction(QStringLiteral("色阶配置"));
tools->addAction(QStringLiteral("异常类型管理"));
tools->addAction(QStringLiteral("模型管理"));
auto* exp = m->addMenu(QStringLiteral("批量导出"));
exp->addAction(QStringLiteral("文件导出"));
exp->addAction(QStringLiteral("报告导出"));
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
alarm->addAction(QStringLiteral("设备告警"));
alarm->addAction(QStringLiteral("告警查询"));
m->addAction(QStringLiteral("自动任务"));
m->addAction(QStringLiteral("模板管理"));
return m;
}
QMenu* buildToolsMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("业务工具"), p);
m->addAction(QStringLiteral("ERT 思维分析"));
m->addAction(QStringLiteral("电法脚本与装置"));
m->addAction(QStringLiteral("Geo 反演"));
m->addAction(QStringLiteral("三维 GPR 综合分析"));
return m;
}
QMenu* buildDeviceMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("设备"), p);
m->addAction(QStringLiteral("连接设备"));
m->addAction(QStringLiteral("设备管理"));
return m;
}
} // namespace
QWidget* buildMenuBar(QWidget* parent)
{
auto* mb = new QMenuBar(parent);
mb->setObjectName(QStringLiteral("appMenuBar"));
// 自带样式(覆盖全局),加大字号/内边距,专业观感。
mb->setStyleSheet(QStringLiteral(
"#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }"
"#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:14px; color:#1F2A3D; }"
"#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }"
"#appMenuBar::item:pressed { background:#DCE6F4; }"));
mb->addMenu(buildViewMenu(mb));
mb->addMenu(buildProjectMenu(mb));
mb->addMenu(buildToolsMenu(mb));
mb->addMenu(buildDeviceMenu(mb));
return mb;
}
QWidget* buildTopToolBar(QWidget* parent)
{
auto* bar = new QWidget(parent);
bar->setObjectName(QStringLiteral("appToolBar"));
bar->setFixedHeight(56);
bar->setStyleSheet(QStringLiteral(
"#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }"
"#topDivider { color:#E1E6EE; }"
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
" font-size:14px; font-weight:600; }"
"#wsSwitcher:hover { background:#EEF3FB; }"
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
"QToolButton::menu-indicator { image:none; }"
"#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:700;"
" font-size:13px; }"
"#userName { color:#1F2A3D; font-size:13px; font-weight:600; }"
"#userRole { color:#8A93A3; font-size:11px; }"));
auto* lay = new QHBoxLayout(bar);
lay->setContentsMargins(14, 0, 14, 0);
lay->setSpacing(0);
// ── 工作空间切换(最左):显示当前空间,点击下拉切换 ──
auto* wsBtn = new QToolButton(bar);
wsBtn->setObjectName(QStringLiteral("wsSwitcher"));
wsBtn->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), kWorkspaceIcon));
wsBtn->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
wsBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
wsBtn->setPopupMode(QToolButton::InstantPopup);
wsBtn->setCursor(Qt::PointingHandCursor);
auto* wsMenu = new QMenu(bar);
auto* wsHeader = wsMenu->addAction(QStringLiteral("切换空间"));
wsHeader->setEnabled(false);
wsMenu->addSeparator();
auto* wsGroup = new QActionGroup(bar);
wsGroup->setExclusive(true);
const QStringList spaces = {QStringLiteral("个人工作空间"), QStringLiteral("勘探一队"),
QStringLiteral("研究院共享")};
for (const auto& s : spaces) {
auto* a = wsMenu->addAction(s);
a->setCheckable(true);
wsGroup->addAction(a);
if (s == spaces.front()) a->setChecked(true);
QObject::connect(a, &QAction::triggered, wsBtn,
[wsBtn, s]() { wsBtn->setText(s + QStringLiteral("")); });
}
wsBtn->setMenu(wsMenu);
wsBtn->setText(spaces.front() + QStringLiteral(""));
lay->addWidget(wsBtn);
lay->addSpacing(10);
lay->addWidget(makeDivider(bar));
lay->addSpacing(10);
// ── 项目选择器(与工作空间切换同款样式:无边框 + 图标 + 文本 + 下拉)──
auto* projBtn = new QToolButton(bar);
projBtn->setObjectName(QStringLiteral("wsSwitcher"));
projBtn->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), kWorkspaceIcon));
projBtn->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
projBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
projBtn->setPopupMode(QToolButton::InstantPopup);
projBtn->setCursor(Qt::PointingHandCursor);
auto* projMenu = new QMenu(bar);
auto* projHeader = projMenu->addAction(QStringLiteral("切换项目"));
projHeader->setEnabled(false);
projMenu->addSeparator();
auto* projCur = projMenu->addAction(QStringLiteral("青海湖北岸勘探项目"));
projCur->setCheckable(true);
projCur->setChecked(true);
projBtn->setMenu(projMenu);
projBtn->setText(QStringLiteral("青海湖北岸勘探项目 青海·海北州 ▾"));
lay->addWidget(projBtn);
lay->addStretch();
// ── 右侧:帮助 / 通知 / 设置(仅图标,悬停显示文本)──
lay->addWidget(makeIconButton(bar, Glyph::Help, QStringLiteral("帮助")));
lay->addWidget(makeIconButton(bar, Glyph::Bell, QStringLiteral("通知")));
lay->addWidget(makeIconButton(bar, Glyph::Gear, QStringLiteral("设置")));
lay->addSpacing(10);
lay->addWidget(makeDivider(bar));
lay->addSpacing(12);
// ── 用户:圆形头像 + 姓名/职务 ──
auto* avatar = new QLabel(QStringLiteral("ZL"), bar);
avatar->setObjectName(QStringLiteral("avatar"));
avatar->setFixedSize(34, 34);
avatar->setAlignment(Qt::AlignCenter);
lay->addWidget(avatar);
lay->addSpacing(8);
auto* userBox = new QWidget(bar);
auto* userLay = new QVBoxLayout(userBox);
userLay->setContentsMargins(0, 0, 0, 0);
userLay->setSpacing(0);
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
userName->setObjectName(QStringLiteral("userName"));
auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBox);
userRole->setObjectName(QStringLiteral("userRole"));
userLay->addWidget(userName);
userLay->addWidget(userRole);
lay->addWidget(userBox);
return bar;
}
} // namespace geopro::app

19
src/app/TopBar.hpp Normal file
View File

@ -0,0 +1,19 @@
#pragma once
// 顶部应用区(对齐原型,静态视觉壳):
// - buildMenuBar最上方的菜单栏视图 / 项目管理 / 业务工具 / 设备,含多级子菜单)。
// - buildTopToolBar菜单栏下方的工具条工作空间切换 + 项目选择 + 帮助/通知/设置 + 用户)。
// 调用方将两者纵向堆叠后经 QMainWindow::setMenuWidget 挂到主窗口顶部。
// 菜单/按钮当前为静态占位,后续接真实页面与数据。
class QWidget;
namespace geopro::app {
// 顶部菜单栏(返回 QWidget*,内部是 QMenuBar调用方放在最上一行
QWidget* buildMenuBar(QWidget* parent = nullptr);
// 菜单栏下方的工具条(工作空间/项目/帮助/通知/设置/用户)。
QWidget* buildTopToolBar(QWidget* parent = nullptr);
} // namespace geopro::app

View File

@ -2,7 +2,6 @@
#include <QColor> #include <QColor>
#include <QFont> #include <QFont>
#include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
@ -12,6 +11,7 @@
#include <QPushButton> #include <QPushButton>
#include <QRandomGenerator> #include <QRandomGenerator>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget>
#include "AuthService.hpp" #include "AuthService.hpp"
@ -77,76 +77,126 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
: QDialog(parent), auth_(auth) : QDialog(parent), auth_(auth)
{ {
setWindowTitle(QStringLiteral("Geopro 3.0 登录")); setWindowTitle(QStringLiteral("Geopro 3.0 登录"));
setFixedSize(380, 320); setFixedSize(400, 500);
// 显式样式QLineEdit 在所有状态都白底深字+边框(否则失焦时文字色取调色板默认、与背景相近不可见)。
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
setStyleSheet(QStringLiteral( setStyleSheet(QStringLiteral(
"QDialog { background: #F5F7FD; }" "QDialog { background: #F4F6FA; }"
"QLabel { color: #2B3A55; }" "#headerBand {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
" stop:0 #2D6CB5, stop:1 #234F87); }"
"#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }"
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }"
"#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }"
"QLineEdit {" "QLineEdit {"
" background: #FFFFFF; color: #1F2A3D;" " background: #FFFFFF; color: #1F2A3D;"
" border: 1px solid #C7D2E0; border-radius: 5px; padding: 6px 9px;" " border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
" selection-background-color: #3A6EA5; selection-color: #FFFFFF; }" " selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
"QLineEdit:focus { border: 1px solid #3A6EA5; }" "QLineEdit:focus { border: 1px solid #2D6CB5; }"
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }")); "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }"));
auto* root = new QVBoxLayout(this); auto* root = new QVBoxLayout(this);
root->setContentsMargins(30, 24, 30, 24); root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(14); root->setSpacing(0);
auto* title = new QLabel(QStringLiteral("Geopro 3.0 登录"), this); // ── 品牌头部:强调色横幅 + 产品名 + 副标题(建立产品身份与视觉锚点)──
QFont titleFont = title->font(); auto* headerBand = new QWidget(this);
titleFont.setPointSize(15); headerBand->setObjectName(QStringLiteral("headerBand"));
titleFont.setBold(true); headerBand->setFixedHeight(108);
title->setFont(titleFont); auto* headerLayout = new QVBoxLayout(headerBand);
title->setAlignment(Qt::AlignCenter); headerLayout->setContentsMargins(32, 0, 32, 0);
title->setStyleSheet(QStringLiteral("color: #2B3A55;")); headerLayout->setSpacing(4);
root->addWidget(title); headerLayout->addStretch();
auto* brandTitle = new QLabel(QStringLiteral("Geopro 3.0"), headerBand);
brandTitle->setObjectName(QStringLiteral("brandTitle"));
auto* brandSubtitle = new QLabel(QStringLiteral("地球物理数据分析平台"), headerBand);
brandSubtitle->setObjectName(QStringLiteral("brandSubtitle"));
headerLayout->addWidget(brandTitle);
headerLayout->addWidget(brandSubtitle);
headerLayout->addStretch();
root->addWidget(headerBand);
auto* form = new QFormLayout(); // ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
form->setSpacing(10); auto* body = new QWidget(this);
form->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); auto* form = new QVBoxLayout(body);
form->setContentsMargins(32, 24, 32, 26);
form->setSpacing(6);
userEdit_ = new QLineEdit(QStringLiteral("sydk"), this); // 统一字段构造小号muted标签 + 40px 高输入框 + 字段间距。
pwdEdit_ = new QLineEdit(QStringLiteral("123456"), this); auto addField = [&](const QString& labelText, QLineEdit* edit) {
auto* lbl = new QLabel(labelText, body);
lbl->setObjectName(QStringLiteral("fieldLabel"));
edit->setMinimumHeight(40);
form->addWidget(lbl);
form->addWidget(edit);
form->addSpacing(6);
};
userEdit_ = new QLineEdit(body);
userEdit_->setPlaceholderText(QStringLiteral("请输入用户名"));
userEdit_->setClearButtonEnabled(true);
addField(QStringLiteral("用户名"), userEdit_);
pwdEdit_ = new QLineEdit(body);
pwdEdit_->setEchoMode(QLineEdit::Password); pwdEdit_->setEchoMode(QLineEdit::Password);
form->addRow(QStringLiteral("用户名"), userEdit_); pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码"));
form->addRow(QStringLiteral("密码"), pwdEdit_); addField(QStringLiteral("密码"), pwdEdit_);
// 验证码:标签 + 一行(输入框占主,验证码图固定在右)。
auto* codeLbl = new QLabel(QStringLiteral("验证码"), body);
codeLbl->setObjectName(QStringLiteral("fieldLabel"));
form->addWidget(codeLbl);
// 验证码行:图 + 输入框 + 刷新
auto* captchaRow = new QHBoxLayout(); auto* captchaRow = new QHBoxLayout();
captchaLabel_ = new QLabel(this); captchaRow->setSpacing(10);
codeEdit_ = new QLineEdit(body);
codeEdit_->setMinimumHeight(40);
codeEdit_->setPlaceholderText(QStringLiteral("请输入验证码"));
captchaLabel_ = new QLabel(body);
captchaLabel_->setObjectName(QStringLiteral("captchaImg"));
captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight); captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight);
captchaLabel_->setFrameShape(QFrame::StyledPanel); captchaLabel_->setAlignment(Qt::AlignCenter);
codeEdit_ = new QLineEdit(this);
codeEdit_->setPlaceholderText(QStringLiteral("验证码"));
captchaRow->addWidget(captchaLabel_);
captchaRow->addWidget(codeEdit_, 1); captchaRow->addWidget(codeEdit_, 1);
form->addRow(QStringLiteral("验证码"), captchaRow); captchaRow->addWidget(captchaLabel_);
form->addLayout(captchaRow);
refreshBtn_ = new QPushButton(QStringLiteral("看不清?刷新"), this); // 刷新链接(右对齐,次要操作弱化为文字链接)。
auto* refreshRow = new QHBoxLayout();
refreshRow->addStretch();
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
refreshBtn_->setFlat(true); refreshBtn_->setFlat(true);
refreshBtn_->setCursor(Qt::PointingHandCursor); refreshBtn_->setCursor(Qt::PointingHandCursor);
refreshBtn_->setStyleSheet(QStringLiteral("color: #3A6EA5; border: none; text-align: right;")); refreshBtn_->setStyleSheet(QStringLiteral(
form->addRow(QString(), refreshBtn_); "QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
refreshRow->addWidget(refreshBtn_);
form->addLayout(refreshRow);
root->addLayout(form); // 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
errorLabel_ = new QLabel(this); errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B;"));
errorLabel_->setWordWrap(true); errorLabel_->setWordWrap(true);
errorLabel_->setMinimumHeight(16); errorLabel_->setMinimumHeight(18);
root->addWidget(errorLabel_); form->addWidget(errorLabel_);
loginBtn_ = new QPushButton(QStringLiteral("立即登录"), this); form->addStretch();
loginBtn_->setMinimumHeight(34);
// 主操作满宽强调主按钮von Restorff唯一高强调元素引导主流程
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body);
loginBtn_->setMinimumHeight(44);
loginBtn_->setCursor(Qt::PointingHandCursor); loginBtn_->setCursor(Qt::PointingHandCursor);
loginBtn_->setStyleSheet(QStringLiteral( loginBtn_->setStyleSheet(QStringLiteral(
"QPushButton { background: #3A6EA5; color: white; border: none; border-radius: 4px; " "QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; "
"font-weight: bold; }" "font-size: 15px; font-weight: 600; }"
"QPushButton:hover { background: #325E8C; }" "QPushButton:hover { background: #2862A6; }"
"QPushButton:pressed { background: #234F87; }"
"QPushButton:disabled { background: #9FB4CC; }")); "QPushButton:disabled { background: #9FB4CC; }"));
loginBtn_->setDefault(true); loginBtn_->setDefault(true);
root->addWidget(loginBtn_); form->addWidget(loginBtn_);
root->addWidget(body, 1);
connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha); connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha);
connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin); connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin);
@ -154,6 +204,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin); connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
refreshCaptcha(); // 打开即拉一张验证码 refreshCaptcha(); // 打开即拉一张验证码
userEdit_->setFocus(); // 焦点落在第一个待填字段
} }
void LoginWindow::refreshCaptcha() void LoginWindow::refreshCaptcha()

View File

@ -17,6 +17,7 @@
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准 // 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准
#include <fstream> #include <fstream>
#include <initializer_list>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -25,6 +26,7 @@
#include <QActionGroup> #include <QActionGroup>
#include <QApplication> #include <QApplication>
#include <QCheckBox> #include <QCheckBox>
#include <QColor>
#include <QDialog> #include <QDialog>
#include <QFile> #include <QFile>
#include <QFrame> #include <QFrame>
@ -35,6 +37,7 @@
#include <QStringList> #include <QStringList>
#include <QTabWidget> #include <QTabWidget>
#include <QMainWindow> #include <QMainWindow>
#include <QStatusBar>
#include <QSurfaceFormat> #include <QSurfaceFormat>
#include <QToolBar> #include <QToolBar>
#include <QTreeWidget> #include <QTreeWidget>
@ -42,6 +45,8 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <DockAreaTitleBar.h>
#include <DockAreaWidget.h>
#include <DockManager.h> #include <DockManager.h>
#include <DockWidget.h> #include <DockWidget.h>
@ -51,6 +56,10 @@
#include "ApiClient.hpp" #include "ApiClient.hpp"
#include "AuthService.hpp" #include "AuthService.hpp"
#include "Glyphs.hpp"
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "TopBar.hpp"
#include "login/LoginWindow.hpp" #include "login/LoginWindow.hpp"
#include "panels/AnomalyListPanel.hpp" #include "panels/AnomalyListPanel.hpp"
#include "panels/DatasetListPanel.hpp" #include "panels/DatasetListPanel.hpp"
@ -199,9 +208,41 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
crs.reset(); crs.reset();
} }
// 停靠系统配置(必须在 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); auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager); window.setCentralWidget(dockManager);
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。这里在其后追加同选择器规则覆盖为极淡分隔。
dockManager->setStyleSheet(
dockManager->styleSheet() +
QStringLiteral(
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }"
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"));
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮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;
};
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。 // 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
auto* centerWidget = new QWidget(); auto* centerWidget = new QWidget();
auto* centerLayout = new QVBoxLayout(centerWidget); auto* centerLayout = new QVBoxLayout(centerWidget);
@ -227,13 +268,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto* layerPanel = new QFrame(centerWidget); auto* layerPanel = new QFrame(centerWidget);
layerPanel->setFrameShape(QFrame::StyledPanel); layerPanel->setFrameShape(QFrame::StyledPanel);
layerPanel->setStyleSheet( layerPanel->setStyleSheet(
QStringLiteral("QFrame{background:rgba(255,255,255,0.92);border:1px solid #b0b4bb;" QStringLiteral("QFrame{background:rgba(255,255,255,0.96);border:1px solid #D5DBE5;"
"border-radius:6px;} QCheckBox{padding:1px;}")); "border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}"
"QCheckBox:disabled{color:#9AA6B6;}"));
auto* layerLayout = new QVBoxLayout(layerPanel); auto* layerLayout = new QVBoxLayout(layerPanel);
layerLayout->setContentsMargins(10, 8, 12, 8); layerLayout->setContentsMargins(13, 10, 15, 11);
layerLayout->setSpacing(4); layerLayout->setSpacing(6);
auto* layerTitle = new QLabel(QStringLiteral("视图详情")); auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
layerTitle->setStyleSheet(QStringLiteral("font-weight:bold;border:none;background:transparent;")); layerTitle->setStyleSheet(QStringLiteral(
"font-weight:600;color:#2D6CB5;border:none;background:transparent;padding-bottom:3px;"));
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)")); auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
chkCurtain->setChecked(true); chkCurtain->setChecked(true);
auto* chkVoxel = new QCheckBox(QStringLiteral("体素dd_voxel")); auto* chkVoxel = new QCheckBox(QStringLiteral("体素dd_voxel"));
@ -301,19 +344,37 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
detailLayout->addWidget(detailWidget, 1); detailLayout->addWidget(detailWidget, 1);
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情")); auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
detailDock->setWidget(detailContainer); detailDock->setWidget(wrapWithHeader(
geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailContainer,
{{geopro::app::Glyph::Collapse, QStringLiteral("折叠")},
{geopro::app::Glyph::Download, QStringLiteral("导出")}}));
// 放在中央视图下方。 // 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea); dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
// 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。 // 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。
auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure()); auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure());
// 左上 dock对象树(GS→TM,测线复选)。 // 左上 dock对象树(GS→TM,测线复选)。表头交给自绘 PanelHeader隐藏树自带列头(避免双标题)。
auto* tree = new QTreeWidget(); auto* tree = new QTreeWidget();
tree->setHeaderLabel(QStringLiteral("对象显示栏")); tree->setHeaderHidden(true);
populateTree(tree, *structure); populateTree(tree, *structure);
// 选中行高亮不覆盖左侧缩进/折叠箭头列:给 branch 设白底(与树底一致),并用生成的箭头图片
// 保留展开/折叠图标(直接给 branch 设背景会触发 Qt 不再画默认箭头的陷阱)。
{
const QString openArrow = geopro::app::writeChevronIcon(true, QColor("#8A93A3"));
const QString closedArrow = geopro::app::writeChevronIcon(false, QColor("#8A93A3"));
tree->setStyleSheet(
QStringLiteral(
"QTreeView::branch { background: #FFFFFF; }"
"QTreeView::branch:has-children:!has-siblings:closed,"
"QTreeView::branch:closed:has-children:has-siblings { image: url(%1); }"
"QTreeView::branch:open:has-children:!has-siblings,"
"QTreeView::branch:open:has-children:has-siblings { image: url(%2); }")
.arg(closedArrow, openArrow));
}
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏")); auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
leftDock->setWidget(tree); leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"), tree,
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 // 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
@ -322,27 +383,61 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
datasetList->setAlternatingRowColors(true); datasetList->setAlternatingRowColors(true);
datasetTabs->addTab(datasetList, QStringLiteral("数据")); datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget(); // M1 文件 tab 占位 auto* fileList = new QListWidget(); // M1 文件 tab 占位
{ // 空状态引导M1 暂无文件来源,给出说明而非空白面板(识别优先于回忆)。
auto* hint = new QListWidgetItem(QStringLiteral("M1 暂无关联文件)"), fileList);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter);
}
datasetTabs->addTab(fileList, QStringLiteral("文件")); datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
datasetDock->setWidget(datasetTabs); 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); dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
// 右上 dock异常列表对齐原型颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。 // 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* anomalyList = new QListWidget(); auto* anomalyList = new QListWidget();
anomalyList->setAlternatingRowColors(true); anomalyList->setAlternatingRowColors(true);
auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表")); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
anomalyDock->setWidget(anomalyList); objAttrLabel->setWordWrap(true);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock); objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
objAttrLabel->setMargin(8);
// 右下 dock属性。 auto anomalyPanel = geopro::app::buildTabbedPanel(
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常列表"), anomalyList, true},
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}},
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
rightDock->setWidget(anomalyPanel.container);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
// 右下 dock属性数据集属性键值对齐原型下半独立面板
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)")); auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
propLabel->setWordWrap(true); propLabel->setWordWrap(true);
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
propLabel->setMargin(8); propLabel->setMargin(8);
auto* propDock = new ads::CDockWidget(QStringLiteral("属性")); auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
propDock->setWidget(propLabel); propDock->setWidget(
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel));
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea); dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
// 注AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。
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);
}
// ── 中央视图重建(核心)───────────────────────────────────────────── // ── 中央视图重建(核心)─────────────────────────────────────────────
// 按勾选的测线(TM)整体重建scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。 // 按勾选的测线(TM)整体重建scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
// 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。 // 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。
@ -451,13 +546,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
rebuildCentral(); rebuildCentral();
}); });
// 单击测线(TM) → 左下数据列表填充其采集批次(数据集) // 单击测线(TM) → 左下数据列表填充其采集批次(数据集) + 动态标题 + 数据 Tab 数量
QObject::connect(tree, &QTreeWidget::itemClicked, tree, QObject::connect(tree, &QTreeWidget::itemClicked, tree,
[structure, datasetList](QTreeWidgetItem* item, int) { [structure, datasetList, datasetTitle, datasetTabs](QTreeWidgetItem* item, int) {
const QString tmId = item->data(0, kRoleTmId).toString(); const QString tmId = item->data(0, kRoleTmId).toString();
if (tmId.isEmpty()) return; // GS 节点无数据集 if (tmId.isEmpty()) return; // GS 节点无数据集
const auto* tm = findTm(*structure, tmId.toStdString()); const auto* tm = findTm(*structure, tmId.toStdString());
if (tm) geopro::app::populateDatasetList(datasetList, tm->dss); if (!tm) return;
geopro::app::populateDatasetList(datasetList, tm->dss);
if (datasetTitle)
datasetTitle->setText(QStringLiteral("数据集显示栏 · %1").arg(item->text(0)));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(tm->dss.size())));
}); });
// ── 数据详情共享状态 + 重建 ────────────────────────────────────────── // ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
@ -527,8 +627,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}; };
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms]( auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms,
const QString& dsId, const QString& name) { anomalyBadge](const QString& dsId, const QString& name) {
if (dsId.isEmpty()) return; if (dsId.isEmpty()) return;
*currentDsId = dsId; *currentDsId = dsId;
@ -539,6 +639,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged先屏蔽 const QSignalBlocker block(anomalyList); // 重填触发 itemChanged先屏蔽
geopro::app::populateAnomalyList(anomalyList, anomalies); geopro::app::populateAnomalyList(anomalyList, anomalies);
} }
// 异常列表 Tab 数量徽标。
if (anomalyBadge) {
anomalyBadge->setText(QString::number(anomalies.size()));
anomalyBadge->setVisible(!anomalies.empty());
}
rebuildDetail(); rebuildDetail();
@ -664,6 +769,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
} }
if (!picked) continue; if (!picked) continue;
geopro::app::populateDatasetList(datasetList, picked->dss); geopro::app::populateDatasetList(datasetList, picked->dss);
if (datasetTitle)
datasetTitle->setText(
QStringLiteral("数据集显示栏 · %1").arg(QString::fromStdString(picked->name)));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(picked->dss.size())));
for (const auto& ds : picked->dss) for (const auto& ds : picked->dss)
if (ds.ddType == "dd_section") { if (ds.ddType == "dd_section") {
loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name)); loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name));
@ -671,16 +781,43 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
} }
break; break;
} }
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
{
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));
topLayout->addWidget(geopro::app::buildTopToolBar(topChrome));
window.setMenuWidget(topChrome);
}
// 底部状态栏常驻显示坐标系与世界系原点wayfinding用户随时知道当前空间基准
window.statusBar()->showMessage(
QStringLiteral("就绪 | 坐标系 %1 | 世界系原点 %2, %3")
.arg(QString::fromUtf8(kProjectCrs))
.arg(lat0, 0, 'f', 5)
.arg(lon0, 0, 'f', 5));
} }
} // namespace } // namespace
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
// 必须在 QApplication 构造前设置。
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。 // QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv); QApplication app(argc, argv);
// 浅色专业主题Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。
geopro::app::applyTheme(app);
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量; // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。 // 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
if (qEnvironmentVariableIsEmpty("PROJ_DATA")) { if (qEnvironmentVariableIsEmpty("PROJ_DATA")) {
@ -717,6 +854,7 @@ int main(int argc, char* argv[])
QMainWindow window; QMainWindow window;
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
window.resize(1280, 800); window.resize(1280, 800);
window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸
buildWorkbench(window, repo); buildWorkbench(window, repo);
window.show(); window.show();

View File

@ -10,9 +10,11 @@
#include <vtkActor.h> #include <vtkActor.h>
#include <vtkNew.h> #include <vtkNew.h>
#include <vtkOpenGLRenderWindow.h>
#include <vtkPNGWriter.h> #include <vtkPNGWriter.h>
#include <vtkRenderWindow.h> #include <vtkRenderWindow.h>
#include <vtkRenderer.h> #include <vtkRenderer.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkWindowToImageFilter.h> #include <vtkWindowToImageFilter.h>
#include "CameraPreset.hpp" #include "CameraPreset.hpp"
@ -148,6 +150,18 @@ int main() {
if (vr.valid()) vr.image->GetDimensions(dims); if (vr.valid()) vr.image->GetDimensions(dims);
std::printf("VOXEL valid=%d dims=%dx%dx%d pts=%zu\n", vr.valid() ? 1 : 0, std::printf("VOXEL valid=%d dims=%dx%dx%d pts=%zu\n", vr.valid() ? 1 : 0,
dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size()); dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size());
// [实测诊断] 体绘制实际渲染模式 + OpenGL 渲染器(查是否软件渲染/CPU 回退)。
if (vr.valid()) {
vtkNew<vtkRenderer> rd; rd->AddVolume(vr.volume);
vtkNew<vtkRenderWindow> rwd; rwd->SetOffScreenRendering(1);
rwd->AddRenderer(rd); rwd->SetSize(400, 400); rwd->Render();
auto* svm = vtkSmartVolumeMapper::SafeDownCast(vr.volume->GetMapper());
const int mode = svm ? svm->GetLastUsedRenderMode() : -99;
std::printf("VOXEL_RENDER_MODE=%d (0=Default,1=RayCast/CPU,2=GPU,3=OSPRay)\n", mode);
auto* gl = vtkOpenGLRenderWindow::SafeDownCast(rwd.Get());
if (gl) std::printf("GL_CAPS:\n%s\n", gl->ReportCapabilities());
}
} }
// 7) DEM 地形 + 影像贴图 — GDAL 读 + 重投影到世界系 + warp 面 + 纹理 // 7) DEM 地形 + 影像贴图 — GDAL 读 + 重投影到世界系 + warp 面 + 纹理