feat(ui): 工作台与登录 UI/UX 整体重构,对齐 Web 原型
- 登录窗品牌化重设计(头部横幅+纵向字段),移除默认账号密码 - 全局浅色主题精致化:基准字体 10pt、去线框留白、下划线标签页、扁平分隔条、High-DPI 直通、压平 Fusion 3D 立体描边 - 顶部:菜单栏(视图/项目管理/业务工具/设备 多级菜单)+ 工具条(工作空间切换/项目选择/帮助·通知·设置/用户) - ADS 子窗口去除关闭·浮动·拖动等操作并隐藏自带标题栏;改用自绘面板表头(图标+标题+操作按钮) - 右侧异常列表/对象属性合并为 Tab 面板,属性独立面板;数据集动态标题+数量徽标 - 矢量图标体系(Lucide SVG via QSvgRenderer);状态栏常驻坐标系/世界系原点 - 对象树修复:单标题、保留折叠箭头、选中高亮不覆盖缩进列
This commit is contained in:
parent
e27a93073a
commit
72761fc05f
|
|
@ -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**。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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基准+轴向)、CrsTransform(PROJ 封装,多 CRS)
|
||||||
|
│ │ └─ algo/ # IInterpolator 接口 + IdwInterpolator(返回 core 的 ScalarVolume,绝不含 VTK)
|
||||||
|
│ ├─ data/ # 数据访问层(异步契约)
|
||||||
|
│ │ ├─ repo/ # IProjectRepository, IDatasetRepository(QFuture/回调 + 取消 + 分页)
|
||||||
|
│ │ ├─ local/ # LocalSampleRepository(QtConcurrent 线程池跑解析)+ 各格式解析器
|
||||||
|
│ │ ├─ api/ # ApiRepository(M1 骨架,签名对齐)
|
||||||
|
│ │ └─ dto/ # 后端 JSON DTO + → model 映射
|
||||||
|
│ ├─ net/ # ApiClient(QtNetwork)/ AuthService(验证码+RSA+login2)/ Credential(QtKeychain)
|
||||||
|
│ ├─ render/ # VTK 渲染层(独占 vtkRenderWindow,统一管理所有 actor)
|
||||||
|
│ │ ├─ Scene # 场景图、世界坐标空间、可见性;持有 RenderWindow
|
||||||
|
│ │ ├─ actors/ # ScatterActor, GridContourActor, VoxelVolumeActor, AnomalyActor, TerrainActor
|
||||||
|
│ │ ├─ color/ # ColorLutBuilder(colorBar → vtkLookupTable 离散阶梯), ScalarBar
|
||||||
|
│ │ ├─ camera/ # CameraPreset(Top2D / Free3D)
|
||||||
|
│ │ ├─ interact/ # InteractionManager + InteractionTool(MeasureTool/SliceTool/PickSelectTool)
|
||||||
|
│ │ └─ ground/ # IGroundLayer + DemImageGroundLayer(M1);TileGroundLayer(M1.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 / MainWindow(ADS 布局、主题)/ AppContext(DI 根)
|
||||||
|
├─ resources/ # QSS 主题、QtAwesome、登录页素材
|
||||||
|
├─ tests/ # gtest(core/data/algo)+ Qt Test(view/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-9,QOpenGLWidget 系,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+spacing)→(z 抬升用 `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` 0–100 → 归一 [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:3857(Web 墨卡托)**,与剖面**不同投影**。
|
||||||
|
- 网格另带 `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. 数据层:Repository(K-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 量纲按色阶来源文件类型判定**(网格色阶 0–255、LVL 色阶 0–1),解析器入口带 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)
|
||||||
|
|
||||||
|
- **停靠框架**:ADS(LGPL,规约 §6.2)。**VTK 面板默认不可浮动**(或浮动时占位、停靠回重建),缓解 reparent 上下文问题(spike 验证,M-4)。
|
||||||
|
- **三区布局**(还原原型):左(对象树 + 数据集列表)/ 中(2D-3D 视图 + 数据详情)/ 右(异常-对象属性 + 属性)。
|
||||||
|
- **主题**:QSS + QDarkStyleSheet 打底 + QtAwesome 图标。
|
||||||
|
- **布局持久化**:ADS 透视图 + 窗口几何存 QSettings(Windows 强制 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 数组),绝不含 VTK(M-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.1(msvc2022_64,预编译) | 官方安装器(MSVC kit) | UI/网络/SQL/并发 | LGPLv3(动态)⚠️ 商务 D-2 |
|
||||||
|
| VTK 9.3([qt,opengl] + gdal/proj 可选) | **源码编→install 前缀** | 三维渲染 + QVTK widget | BSD ✅ |
|
||||||
|
| ADS(Qt-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 入 CI;Debug 启用 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**:全 vcpkg(qtbase + vtk[qt] 共用一份 Qt)配置、编译、出 exe、单一链路部署、无双 Qt 冲突。
|
||||||
|
2. **UI 上下文 spike**:ADS(vcpkg 或 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`,去 VTK(M-2 arch)。
|
||||||
|
- 交互:模态工具抽象 + 拾取回流 + 切片改 `vtkResliceCursorWidget`(B-3 arch、M-2 code)。
|
||||||
|
- widget 改 `QVTKOpenGLStereoWidget` + VTK 面板不可浮动 + spike(M-4 code)。
|
||||||
|
- 构建改全 vcpkg(已核 vtk[qt]→qtbase 依赖)、删 PCL(M-5 code、m-1)。
|
||||||
|
- 登录 refresh/token 生命周期降为前置确认(M-5 arch)。
|
||||||
|
- 色阶离散 LUT + under/over/NaN + alpha 按来源 + lineType 以配置为准(m-1/m-2/m-4 各)。
|
||||||
|
- controller 拆 Selection/RenderSync/DetailSync(m-5 arch)。
|
||||||
|
- 新增 §15 spike 门槛(K-2)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*v2 经双专家评审 + 数据核验修订。下一步:spike 预研 → writing-plans。*
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
对象:项目/gs/tm对象,鼠标右键,tm导入ds,编辑在右侧面板直接做;gs也有导入ds功能;
|
||||||
|
|
||||||
|
数据集:单击更新属性,双击时打开或选中数据详情,选中三维视图;文件列表,双击按支持的文件格式预览,不支持的文件格式提示+下载;
|
||||||
|
|
||||||
|
二维三维视图:对象视图中选中的TM,需要使用树形列表展示可渲染图形的tm/ds树,并支持选中或取消某个tm/ds,决定是否渲染;展示异常;鼠标点选图形时,如果在垂直屏幕方向有多个图形叠加,需要把多个图形的信息列出来,方便用户选中被遮住的图形;
|
||||||
|
|
||||||
|
数据详情:
|
||||||
|
|
||||||
|
异常:gs选择后,展示下面的合集;异常体不渲染;通过类型或状态过滤;支持多选异常创建异常体,支持将异常拖入异常体;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
项目列表:新增项目,最后访问时间倒序;项目列表可切换地图模式;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
分析视图、大屏视图(web):按项目类型设置绑定视图,客户端可切换,分析视图默认;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
个人资料:显示基本信息、修改邮箱、修改密码;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
大文件上传(ds中的地形文件)需要走独立的接口;
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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——一旦改写 branch,Qt 会停止绘制
|
||||||
|
默认的展开/折叠箭头(与 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,83 +77,134 @@ 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);
|
||||||
connect(codeEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
connect(codeEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
||||||
connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
||||||
|
|
||||||
refreshCaptcha(); // 打开即拉一张验证码
|
refreshCaptcha(); // 打开即拉一张验证码
|
||||||
|
userEdit_->setFocus(); // 焦点落在第一个待填字段
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoginWindow::refreshCaptcha()
|
void LoginWindow::refreshCaptcha()
|
||||||
|
|
|
||||||
180
src/app/main.cpp
180
src/app/main.cpp
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 面 + 纹理
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue