diff --git a/docs/ENV_SETUP_Windows.md b/docs/ENV_SETUP_Windows.md index e797233..391290d 100644 --- a/docs/ENV_SETUP_Windows.md +++ b/docs/ENV_SETUP_Windows.md @@ -67,9 +67,10 @@ setx VCPKG_ROOT "C:\dev\vcpkg" # 永久(新开终端生效) **用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用): -1. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。 -2. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。 -3. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。 +1. `.\qt-online-installer-windows-x64-4.11.0.exe --mirror https://ftp.jaist.ac.jp/pub/qtproject` +2. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。 +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**。 diff --git a/docs/superpowers/specs/geopro-desktop-m1-design.md b/docs/superpowers/specs/geopro-desktop-m1-design.md new file mode 100644 index 0000000..34643dd --- /dev/null +++ b/docs/superpowers/specs/geopro-desktop-m1-design.md @@ -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 loadProject(id); + QFuture> loadStructure(projectId); // GS/TM 树 +} +IDatasetRepository { + QFuture> listDatasets(tmObjectId, PageReq); // 分页 + QFuture loadScatter(dsId); + QFuture loadGrid(dsId); + QFuture loadColorScale(dsId); + QFuture> loadAnomalies(dsId); + QFuture 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 `(不透明会话令牌,非 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 "`,存 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。* diff --git a/docs/需求调研-20260608.md b/docs/需求调研-20260608.md new file mode 100644 index 0000000..ad89652 --- /dev/null +++ b/docs/需求调研-20260608.md @@ -0,0 +1,25 @@ +对象:项目/gs/tm对象,鼠标右键,tm导入ds,编辑在右侧面板直接做;gs也有导入ds功能; + +数据集:单击更新属性,双击时打开或选中数据详情,选中三维视图;文件列表,双击按支持的文件格式预览,不支持的文件格式提示+下载; + +二维三维视图:对象视图中选中的TM,需要使用树形列表展示可渲染图形的tm/ds树,并支持选中或取消某个tm/ds,决定是否渲染;展示异常;鼠标点选图形时,如果在垂直屏幕方向有多个图形叠加,需要把多个图形的信息列出来,方便用户选中被遮住的图形; + +数据详情: + +异常:gs选择后,展示下面的合集;异常体不渲染;通过类型或状态过滤;支持多选异常创建异常体,支持将异常拖入异常体; + + + +项目列表:新增项目,最后访问时间倒序;项目列表可切换地图模式; + + + +分析视图、大屏视图(web):按项目类型设置绑定视图,客户端可切换,分析视图默认; + + + +个人资料:显示基本信息、修改邮箱、修改密码; + + + +大文件上传(ds中的地形文件)需要走独立的接口; \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 915d1d7..dff8680 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -11,9 +11,14 @@ find_package(VTK REQUIRED COMPONENTS FiltersModeling ) find_package(nlohmann_json CONFIG REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Svg) add_executable(geopro_desktop WIN32 main.cpp + Theme.cpp + TopBar.cpp + Glyphs.cpp + PanelHeader.cpp login/LoginWindow.cpp panels/AnomalyListPanel.cpp panels/DatasetListPanel.cpp) @@ -21,7 +26,7 @@ add_executable(geopro_desktop WIN32 target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_desktop PRIVATE - Qt6::Core Qt6::Gui Qt6::Widgets + Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg ${VTK_LIBRARIES} ads::qt6advanceddocking nlohmann_json::nlohmann_json diff --git a/src/app/Glyphs.cpp b/src/app/Glyphs.cpp new file mode 100644 index 0000000..24255ad --- /dev/null +++ b/src/app/Glyphs.cpp @@ -0,0 +1,153 @@ +#include "Glyphs.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace geopro::app { + +namespace { + +// 每个图标的 SVG 内容(线性风格,24x24 视窗,描边由外层注入颜色)。 +// 路径取自 Lucide 图标集(MIT 许可,业界通用),保证专业、清晰、语义正确。 +QString svgPathFor(Glyph t) +{ + switch (t) { + case Glyph::Tree: + return QStringLiteral( + "" + ""); + case Glyph::Dataset: + return QStringLiteral( + "" + ""); + case Glyph::Map: + return QStringLiteral( + "" + "" + ""); + case Glyph::Detail: + return QStringLiteral( + ""); + case Glyph::Anomaly: + return QStringLiteral( + ""); + case Glyph::Property: + return QStringLiteral( + ""); + case Glyph::Plus: + return QStringLiteral(""); + case Glyph::Filter: + return QStringLiteral( + ""); + case Glyph::Upload: + return QStringLiteral( + "" + ""); + case Glyph::Download: + return QStringLiteral( + "" + ""); + case Glyph::Collapse: + return QStringLiteral(""); + case Glyph::Workspace: + return QStringLiteral( + "" + "" + "" + ""); + case Glyph::Folder: + return QStringLiteral( + ""); + case Glyph::Help: + return QStringLiteral( + "" + ""); + case Glyph::Bell: + return QStringLiteral( + "" + ""); + case Glyph::Gear: + return QStringLiteral( + ""); + } + return QString(); +} + +} // namespace + +QIcon makeGlyph(Glyph type, const QColor& color, int px) +{ + const QString svg = + QStringLiteral("%2") + .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 diff --git a/src/app/Glyphs.hpp b/src/app/Glyphs.hpp new file mode 100644 index 0000000..9ac1347 --- /dev/null +++ b/src/app/Glyphs.hpp @@ -0,0 +1,41 @@ +#pragma once + +// 程序绘制的扁平线性图标(无需图片资源):用于面板表头与表头操作按钮, +// 风格对齐原型(细线、圆角端点、单色随主题)。仅外观,无交互逻辑。 + +#include +#include +#include + +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 diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp new file mode 100644 index 0000000..a561de6 --- /dev/null +++ b/src/app/PanelHeader.cpp @@ -0,0 +1,151 @@ +#include "PanelHeader.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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& 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& tabs, const QVector& 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 diff --git a/src/app/PanelHeader.hpp b/src/app/PanelHeader.hpp new file mode 100644 index 0000000..2309127 --- /dev/null +++ b/src/app/PanelHeader.hpp @@ -0,0 +1,46 @@ +#pragma once + +// 面板自绘表头(对齐原型): +// - buildPanelHeader:图标 + 标题 + (可选数量徽标) + 右侧操作按钮(标准面板)。 +// - buildTabbedPanel:表头为若干 Tab(图标+标题+可选徽标)+ 右侧操作按钮,下方堆叠对应内容。 +// 由调用方放在面板内容顶部,并隐藏 ADS 自带标题栏,从而完全掌控表头外观。 +// 操作按钮当前为静态占位(tooltip 标注),后续接真实功能。 + +#include +#include +#include + +#include "Glyphs.hpp" + +class QWidget; +class QLabel; + +namespace geopro::app { + +// 一个表头操作项:图标 + 提示文案。 +using HeaderAction = QPair; + +// 标准面板表头。标题 QLabel objectName="panelTitle"、徽标 QLabel objectName="panelBadge" +// (默认隐藏),调用方可经 container->findChild 动态更新。 +QWidget* buildPanelHeader(Glyph icon, const QString& title, + const QVector& actions = {}); + +// 一个 Tab 规格:图标 + 标题 + 内容 + 是否带数量徽标。 +struct PanelTab { + Glyph icon; + QString title; + QWidget* content; + bool hasBadge; +}; + +// 带 Tab 表头的面板构建结果:容器 + 各 Tab 的徽标标签(无徽标处为 nullptr,供动态更新)。 +struct TabbedPanel { + QWidget* container; + QVector badges; +}; + +// 构建带 Tab 表头的面板(首个 Tab 默认激活;点击 Tab 切换下方堆叠内容)。 +TabbedPanel buildTabbedPanel(const QVector& tabs, + const QVector& actions = {}); + +} // namespace geopro::app diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp new file mode 100644 index 0000000..3497124 --- /dev/null +++ b/src/app/Theme.cpp @@ -0,0 +1,428 @@ +#include "Theme.hpp" + +#include +#include +#include +#include +#include + +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 diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp new file mode 100644 index 0000000..d6734a8 --- /dev/null +++ b/src/app/Theme.hpp @@ -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 diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp new file mode 100644 index 0000000..57e08d9 --- /dev/null +++ b/src/app/TopBar.cpp @@ -0,0 +1,238 @@ +#include "TopBar.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp new file mode 100644 index 0000000..6f4a3e0 --- /dev/null +++ b/src/app/TopBar.hpp @@ -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 diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index 3b6fcea..ac85dd2 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -12,6 +11,7 @@ #include #include #include +#include #include "AuthService.hpp" @@ -77,83 +77,134 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) : QDialog(parent), auth_(auth) { setWindowTitle(QStringLiteral("Geopro 3.0 登录")); - setFixedSize(380, 320); - // 显式样式:QLineEdit 在所有状态都白底深字+边框(否则失焦时文字色取调色板默认、与背景相近不可见)。 + setFixedSize(400, 500); + + // 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。 + // QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。 setStyleSheet(QStringLiteral( - "QDialog { background: #F5F7FD; }" - "QLabel { color: #2B3A55; }" + "QDialog { background: #F4F6FA; }" + "#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 {" " background: #FFFFFF; color: #1F2A3D;" - " border: 1px solid #C7D2E0; border-radius: 5px; padding: 6px 9px;" - " selection-background-color: #3A6EA5; selection-color: #FFFFFF; }" - "QLineEdit:focus { border: 1px solid #3A6EA5; }" - "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }")); + " border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;" + " selection-background-color: #2D6CB5; selection-color: #FFFFFF; }" + "QLineEdit:focus { border: 1px solid #2D6CB5; }" + "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }" + "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }")); auto* root = new QVBoxLayout(this); - root->setContentsMargins(30, 24, 30, 24); - root->setSpacing(14); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); - auto* title = new QLabel(QStringLiteral("Geopro 3.0 登录"), this); - QFont titleFont = title->font(); - titleFont.setPointSize(15); - titleFont.setBold(true); - title->setFont(titleFont); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet(QStringLiteral("color: #2B3A55;")); - root->addWidget(title); + // ── 品牌头部:强调色横幅 + 产品名 + 副标题(建立产品身份与视觉锚点)── + auto* headerBand = new QWidget(this); + headerBand->setObjectName(QStringLiteral("headerBand")); + headerBand->setFixedHeight(108); + auto* headerLayout = new QVBoxLayout(headerBand); + headerLayout->setContentsMargins(32, 0, 32, 0); + headerLayout->setSpacing(4); + 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); - form->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); + // ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)── + auto* body = new QWidget(this); + auto* form = new QVBoxLayout(body); + form->setContentsMargins(32, 24, 32, 26); + form->setSpacing(6); - userEdit_ = new QLineEdit(QStringLiteral("sydk"), this); - pwdEdit_ = new QLineEdit(QStringLiteral("123456"), this); + // 统一字段构造:小号muted标签 + 40px 高输入框 + 字段间距。 + 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); - form->addRow(QStringLiteral("用户名"), userEdit_); - form->addRow(QStringLiteral("密码"), pwdEdit_); + pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码")); + addField(QStringLiteral("密码"), pwdEdit_); + + // 验证码:标签 + 一行(输入框占主,验证码图固定在右)。 + auto* codeLbl = new QLabel(QStringLiteral("验证码"), body); + codeLbl->setObjectName(QStringLiteral("fieldLabel")); + form->addWidget(codeLbl); - // 验证码行:图 + 输入框 + 刷新 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_->setFrameShape(QFrame::StyledPanel); - codeEdit_ = new QLineEdit(this); - codeEdit_->setPlaceholderText(QStringLiteral("验证码")); - captchaRow->addWidget(captchaLabel_); + captchaLabel_->setAlignment(Qt::AlignCenter); 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_->setCursor(Qt::PointingHandCursor); - refreshBtn_->setStyleSheet(QStringLiteral("color: #3A6EA5; border: none; text-align: right;")); - form->addRow(QString(), refreshBtn_); + refreshBtn_->setStyleSheet(QStringLiteral( + "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(this); - errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B;")); + // 错误提示:固定占位高度,避免出现时整体布局跳动。 + errorLabel_ = new QLabel(body); + errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;")); errorLabel_->setWordWrap(true); - errorLabel_->setMinimumHeight(16); - root->addWidget(errorLabel_); + errorLabel_->setMinimumHeight(18); + form->addWidget(errorLabel_); - loginBtn_ = new QPushButton(QStringLiteral("立即登录"), this); - loginBtn_->setMinimumHeight(34); + form->addStretch(); + + // 主操作:满宽强调主按钮(von Restorff:唯一高强调元素引导主流程)。 + loginBtn_ = new QPushButton(QStringLiteral("登 录"), body); + loginBtn_->setMinimumHeight(44); loginBtn_->setCursor(Qt::PointingHandCursor); loginBtn_->setStyleSheet(QStringLiteral( - "QPushButton { background: #3A6EA5; color: white; border: none; border-radius: 4px; " - "font-weight: bold; }" - "QPushButton:hover { background: #325E8C; }" + "QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; " + "font-size: 15px; font-weight: 600; }" + "QPushButton:hover { background: #2862A6; }" + "QPushButton:pressed { background: #234F87; }" "QPushButton:disabled { background: #9FB4CC; }")); loginBtn_->setDefault(true); - root->addWidget(loginBtn_); + form->addWidget(loginBtn_); + + root->addWidget(body, 1); connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha); connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin); connect(codeEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin); connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin); - refreshCaptcha(); // 打开即拉一张验证码 + refreshCaptcha(); // 打开即拉一张验证码 + userEdit_->setFocus(); // 焦点落在第一个待填字段 } void LoginWindow::refreshCaptcha() diff --git a/src/app/main.cpp b/src/app/main.cpp index 1260151..e77585c 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -17,6 +17,7 @@ // 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。 #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +45,8 @@ #include #include +#include +#include #include #include @@ -51,6 +56,10 @@ #include "ApiClient.hpp" #include "AuthService.hpp" +#include "Glyphs.hpp" +#include "PanelHeader.hpp" +#include "Theme.hpp" +#include "TopBar.hpp" #include "login/LoginWindow.hpp" #include "panels/AnomalyListPanel.hpp" #include "panels/DatasetListPanel.hpp" @@ -199,9 +208,41 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re 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); 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& 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 视图。 auto* centerWidget = new QWidget(); auto* centerLayout = new QVBoxLayout(centerWidget); @@ -227,13 +268,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* layerPanel = new QFrame(centerWidget); layerPanel->setFrameShape(QFrame::StyledPanel); layerPanel->setStyleSheet( - QStringLiteral("QFrame{background:rgba(255,255,255,0.92);border:1px solid #b0b4bb;" - "border-radius:6px;} QCheckBox{padding:1px;}")); + QStringLiteral("QFrame{background:rgba(255,255,255,0.96);border:1px solid #D5DBE5;" + "border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}" + "QCheckBox:disabled{color:#9AA6B6;}")); auto* layerLayout = new QVBoxLayout(layerPanel); - layerLayout->setContentsMargins(10, 8, 12, 8); - layerLayout->setSpacing(4); + layerLayout->setContentsMargins(13, 10, 15, 11); + layerLayout->setSpacing(6); 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("帘面(断面墙)")); chkCurtain->setChecked(true); auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)")); @@ -301,19 +344,37 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailLayout->addWidget(detailWidget, 1); 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); // 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。 auto structure = std::make_shared>(repo.loadStructure()); - // 左上 dock:对象树(GS→TM,测线复选)。 + // 左上 dock:对象树(GS→TM,测线复选)。表头交给自绘 PanelHeader,隐藏树自带列头(避免双标题)。 auto* tree = new QTreeWidget(); - tree->setHeaderLabel(QStringLiteral("对象显示栏")); + tree->setHeaderHidden(true); 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("对象显示栏")); - leftDock->setWidget(tree); + leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"), tree, + {{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}})); auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 @@ -322,27 +383,61 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re datasetList->setAlternatingRowColors(true); datasetTabs->addTab(datasetList, QStringLiteral("数据")); 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("文件")); 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(QStringLiteral("panelTitle")); dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea); - // 右上 dock:异常列表(对齐原型;颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。 + // 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 auto* anomalyList = new QListWidget(); anomalyList->setAlternatingRowColors(true); - auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表")); - anomalyDock->setWidget(anomalyList); - auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock); + auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); + objAttrLabel->setWordWrap(true); + 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("(单击左侧数据集查看属性与平面剖面)")); propLabel->setWordWrap(true); propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); propLabel->setMargin(8); auto* propDock = new ads::CDockWidget(QStringLiteral("属性")); - propDock->setWidget(propLabel); + propDock->setWidget( + wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel)); 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。 // 二维地图 = buildSurveyLine(红线俯视,浅底背景)+ applyTop2D。 @@ -451,13 +546,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re rebuildCentral(); }); - // 单击测线(TM) → 左下数据列表填充其采集批次(数据集)。 + // 单击测线(TM) → 左下数据列表填充其采集批次(数据集) + 动态标题 + 数据 Tab 数量。 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(); if (tmId.isEmpty()) return; // GS 节点无数据集 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(tm->dss.size()))); }); // ── 数据详情共享状态 + 重建 ────────────────────────────────────────── @@ -527,8 +627,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 - auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms]( - const QString& dsId, const QString& name) { + auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms, + anomalyBadge](const QString& dsId, const QString& name) { if (dsId.isEmpty()) return; *currentDsId = dsId; @@ -539,6 +639,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽 geopro::app::populateAnomalyList(anomalyList, anomalies); } + // 异常列表 Tab 数量徽标。 + if (anomalyBadge) { + anomalyBadge->setText(QString::number(anomalies.size())); + anomalyBadge->setVisible(!anomalies.empty()); + } rebuildDetail(); @@ -664,6 +769,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } if (!picked) continue; 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(picked->dss.size()))); for (const auto& ds : picked->dss) if (ds.ddType == "dd_section") { loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name)); @@ -671,16 +781,43 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } 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 int main(int argc, char* argv[]) { + // 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。 + // 必须在 QApplication 构造前设置。 + QApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + // QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。 QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); QApplication app(argc, argv); + // 浅色专业主题(Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。 + geopro::app::applyTheme(app); + // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量; // 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。 if (qEnvironmentVariableIsEmpty("PROJ_DATA")) { @@ -717,6 +854,7 @@ int main(int argc, char* argv[]) QMainWindow window; window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); window.resize(1280, 800); + window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸 buildWorkbench(window, repo); window.show(); diff --git a/tests/spike/render_verify.cpp b/tests/spike/render_verify.cpp index 8fde7b2..538ef02 100644 --- a/tests/spike/render_verify.cpp +++ b/tests/spike/render_verify.cpp @@ -10,9 +10,11 @@ #include #include +#include #include #include #include +#include #include #include "CameraPreset.hpp" @@ -148,6 +150,18 @@ int main() { if (vr.valid()) vr.image->GetDimensions(dims); 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()); + + // [实测诊断] 体绘制实际渲染模式 + OpenGL 渲染器(查是否软件渲染/CPU 回退)。 + if (vr.valid()) { + vtkNew rd; rd->AddVolume(vr.volume); + vtkNew 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 面 + 纹理