Merge pull request 'refactor/pure-qt-ui' (#3) from refactor/pure-qt-ui into main
Reviewed-on: https://gitea.geomative.cn/gaozheng/geopro/pulls/3
This commit is contained in:
commit
439737a457
|
|
@ -0,0 +1,65 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||||
|
|
||||||
|
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||||
|
|
||||||
|
## 1. Think Before Coding
|
||||||
|
|
||||||
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them - don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
## 2. Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||||
|
|
||||||
|
## 3. Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, mention it - don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
The test: Every changed line should trace directly to the user's request.
|
||||||
|
|
||||||
|
## 4. Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
|
```
|
||||||
|
1. [Step] → verify: [check]
|
||||||
|
2. [Step] → verify: [check]
|
||||||
|
3. [Step] → verify: [check]
|
||||||
|
```
|
||||||
|
|
||||||
|
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 第三方组件与许可证 (Third-Party Notices)
|
||||||
|
|
||||||
|
本项目(geopro / Geopro 3.0 桌面端)使用了以下第三方开源组件。各组件版权归其各自作者所有,
|
||||||
|
按其各自许可证条款分发。
|
||||||
|
|
||||||
|
| 组件 | 用途 | 许可证 | 来源 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Qt 6** (6.11.x) | GUI 框架(Widgets/Gui/Core/Network/Svg/OpenGL 等) | LGPL-3.0(亦提供商业许可) | https://www.qt.io |
|
||||||
|
| **VTK** (9.x) | 三维/二维可视化渲染 | BSD-3-Clause | https://vtk.org |
|
||||||
|
| **Qt-Advanced-Docking-System** (4.3.1) | 停靠面板布局 | LGPL-2.1 | https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System |
|
||||||
|
| **QtKeychain** (0.14.0) | 凭证安全存取("记住登录") | BSD-3-Clause | https://github.com/frankosterfeld/qtkeychain |
|
||||||
|
|
||||||
|
> UI 为标准 Qt Widgets(Fusion 风格 + 项目自带 QSS 主题),未使用第三方 UI 控件库。
|
||||||
|
|
||||||
|
## 许可证要点
|
||||||
|
|
||||||
|
- **Qt 6 / Qt-Advanced-Docking-System(LGPL)**:本项目以动态链接方式使用 Qt 与 ADS,符合 LGPL
|
||||||
|
对动态链接的要求;如需替换这些库,最终用户可自行替换对应动态库。若改为静态链接或分发修改版,
|
||||||
|
需遵循 LGPL 的相应义务(提供目标代码/重新链接能力)。
|
||||||
|
- **VTK / QtKeychain(BSD-3-Clause)**:需在分发物中保留版权声明与许可证文本,不得用作者名背书。
|
||||||
|
|
||||||
|
> 各组件完整许可证文本随其源码分发(FetchContent 拉取于 build 目录的 `_deps/<组件>-src/`,
|
||||||
|
> 或随 Qt/VTK 安装目录)。如需在发行包中附带完整 LICENSE 文本,请从对应来源复制。
|
||||||
52
README.md
52
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
Qt 6.8 LTS(QtWidgets)+ VTK 9.3+ · CMake + vcpkg(全量,含 Qt)· MSVC 2022 / C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
|
Qt 6.11(QtWidgets)+ VTK 9.6 · CMake + Ninja · 官方 MSVC 预编译 Qt + vcpkg(仅非 Qt 依赖)· MSVC(VS 2022/2026)/ C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
|
||||||
|
|
||||||
## 目录(设计 §3)
|
## 目录(设计 §3)
|
||||||
|
|
||||||
|
|
@ -25,19 +25,51 @@ tools/ 离线验证脚本(validate_samples.py)
|
||||||
docs/ 规约、API、样本数据、设计文档
|
docs/ 规约、API、样本数据、设计文档
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 构建与运行
|
||||||
|
|
||||||
前置:VS2022(C++ 桌面开发)、Git、vcpkg(设 `VCPKG_ROOT`)。详见 ENV_SETUP_Windows.md。
|
前置:**Visual Studio 2022/2026**(勾选「使用 C++ 的桌面开发」工作负载,自带 CMake + Ninja)、Git、**vcpkg** 并设环境变量 `VCPKG_ROOT`。构建方案②:单一官方 MSVC 预编译 Qt(`CMAKE_PREFIX_PATH` → `D:/Qt/6.11.1/msvc2022_64`)、VTK 预编译于 `external/vtk-install`、ADS/QtKeychain 经 FetchContent 对接同一份 Qt、仅非 Qt 依赖(GDAL/PROJ/OpenSSL/…)走 vcpkg。详见 [docs/ENV_SETUP_Windows.md](docs/ENV_SETUP_Windows.md)。
|
||||||
|
|
||||||
```powershell
|
> ⚠️ 本机 `cmake` / `ninja` / `cl` **默认不在 PATH**,必须在已激活 MSVC 环境的终端里构建。下面三种方式都已处理好这一点。
|
||||||
# x64 Native Tools 命令行,项目根
|
|
||||||
vcpkg x-update-baseline --add-initial-baseline # 锁依赖版本
|
### 方式一:一键脚本(推荐)
|
||||||
cmake --preset msvc-debug # 首次编译 Qt+VTK,较久
|
|
||||||
cmake --build build/debug
|
项目根的 `build.bat` 自动用 vswhere 定位 VS、激活 MSVC 环境、按需配置并编译。在 **cmd** 里于项目根执行 `build <命令>`:
|
||||||
.\build\debug\src\app\geopro_desktop.exe # spike 冒烟:应显示一个锥体
|
|
||||||
ctest --test-dir build/debug # 运行单测
|
| 命令 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `build`(或 `build app`) | 编译主程序 `geopro_desktop`(默认) |
|
||||||
|
| `build run` | 编译并运行主程序 |
|
||||||
|
| `build test` | 编译并跑单元测试(ctest) |
|
||||||
|
| `build all` | 编译全部目标 |
|
||||||
|
| `build configure` | 改了 CMakeLists / 新增源文件后,强制重新配置 |
|
||||||
|
|
||||||
|
### 方式二:Visual Studio 打开文件夹
|
||||||
|
|
||||||
|
VS →「打开本地文件夹」→ 选仓库根 → 自动识别 `CMakePresets.json` → 选配置 **MSVC Release** → 菜单「生成 → 全部生成」;运行/调试目标选 `geopro_desktop`。
|
||||||
|
|
||||||
|
### 方式三:手动命令行
|
||||||
|
|
||||||
|
开始菜单打开「**x64 Native Tools Command Prompt for VS**」(已带 MSVC 环境),在仓库根(CMake 用 VS 自带的全路径,因其不在 PATH):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
set CMAKE="%VSINSTALLDIR%Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||||
|
%CMAKE% --preset msvc-release :: 配置(首次 / 改 CMakeLists 后)
|
||||||
|
%CMAKE% --build build/release --target geopro_desktop :: 编译主程序
|
||||||
|
build\release\src\app\geopro_desktop.exe :: 运行
|
||||||
|
%CMAKE% --build build/release --target geopro_tests :: 编译测试
|
||||||
|
ctest --test-dir build/release --output-on-failure :: 跑测试
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(Debug 用 `--preset msvc-debug`,对应 `build/debug`。`%VSINSTALLDIR%` 在 Native Tools 提示符里已设好。)
|
||||||
|
|
||||||
|
### 构建目标与产物
|
||||||
|
|
||||||
|
- `geopro_desktop` — 主程序,产物 `build/release/src/app/geopro_desktop.exe`
|
||||||
|
- `geopro_tests` — 单元测试,配 `ctest`
|
||||||
|
- 不带 `--target` 编译全部
|
||||||
|
|
||||||
|
> 改了已有 `.cpp/.hpp` 直接 `--build`(Ninja 增量);改了 `CMakeLists.txt` 或新增源文件需先 `--preset` / `build configure`。链接报 **LNK1104**(文件被占用)时,先关掉运行中的 `geopro_desktop.exe`。
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
M1 设计完成(v2,经双专家评审)。进入 **spike 预研**(设计 §15):① 全 vcpkg 构建/部署 ② ADS + QVTKOpenGLStereoWidget 停靠稳定 ③ 真实样本跑通 banded contour。spike 通过后展开完整实现计划。
|
M1 设计完成(v2,经双专家评审)。进入 **spike 预研**(设计 §15):① 全 vcpkg 构建/部署 ② ADS + QVTKOpenGLStereoWidget 停靠稳定 ③ 真实样本跑通 banded contour。spike 通过后展开完整实现计划。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM geopro build helper (Windows / MSVC + Ninja, CMake presets)
|
||||||
|
REM
|
||||||
|
REM Usage: build [app | all | test | run | configure]
|
||||||
|
REM app (default) build target geopro_desktop
|
||||||
|
REM all build all targets
|
||||||
|
REM test build + run unit tests via ctest
|
||||||
|
REM run build + launch geopro_desktop
|
||||||
|
REM configure force re-run CMake configure (after CMakeLists changes)
|
||||||
|
REM
|
||||||
|
REM Requires: Visual Studio 2022/2026 (Desktop C++ workload, ships
|
||||||
|
REM CMake + Ninja) and the VCPKG_ROOT environment variable.
|
||||||
|
REM Note: cmake/ninja/cl are NOT on PATH on this machine; this script
|
||||||
|
REM locates VS via vswhere and activates the MSVC env itself.
|
||||||
|
REM ============================================================
|
||||||
|
setlocal
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
set "BUILDDIR=%ROOT%build\release"
|
||||||
|
set "PRESET=msvc-release"
|
||||||
|
|
||||||
|
REM --- locate Visual Studio (vswhere lives at a fixed path) ---
|
||||||
|
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||||
|
if not exist "%VSWHERE%" (
|
||||||
|
echo [build] vswhere not found. Open "x64 Native Tools Command Prompt for VS" and build manually.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||||
|
if not defined VSPATH ( echo [build] Visual Studio not found. & exit /b 1 )
|
||||||
|
|
||||||
|
set "VCVARS=%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
set "CMAKE=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||||
|
set "CTEST=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe"
|
||||||
|
if not exist "%VCVARS%" ( echo [build] vcvars64.bat not found: %VCVARS% & exit /b 1 )
|
||||||
|
if not exist "%CMAKE%" ( echo [build] cmake not found: %CMAKE% & exit /b 1 )
|
||||||
|
|
||||||
|
REM --- activate MSVC environment (cl / link / include / lib) ---
|
||||||
|
call "%VCVARS%" >nul
|
||||||
|
|
||||||
|
set "CMD=%~1"
|
||||||
|
if "%CMD%"=="" set "CMD=app"
|
||||||
|
|
||||||
|
if /i "%CMD%"=="configure" goto :configure
|
||||||
|
if /i "%CMD%"=="app" goto :app
|
||||||
|
if /i "%CMD%"=="all" goto :all
|
||||||
|
if /i "%CMD%"=="test" goto :test
|
||||||
|
if /i "%CMD%"=="run" goto :run
|
||||||
|
echo [build] unknown command "%CMD%". Use: app ^| all ^| test ^| run ^| configure
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:ensure
|
||||||
|
if not exist "%BUILDDIR%\CMakeCache.txt" "%CMAKE%" --preset %PRESET%
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:configure
|
||||||
|
"%CMAKE%" --preset %PRESET%
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
||||||
|
:app
|
||||||
|
call :ensure
|
||||||
|
"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
||||||
|
:all
|
||||||
|
call :ensure
|
||||||
|
"%CMAKE%" --build "%BUILDDIR%"
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
||||||
|
:test
|
||||||
|
call :ensure
|
||||||
|
"%CMAKE%" --build "%BUILDDIR%" --target geopro_tests || exit /b 1
|
||||||
|
"%CTEST%" --test-dir "%BUILDDIR%" --output-on-failure
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
||||||
|
:run
|
||||||
|
call :ensure
|
||||||
|
"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop || exit /b 1
|
||||||
|
"%BUILDDIR%\src\app\geopro_desktop.exe"
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
@ -0,0 +1,669 @@
|
||||||
|
# Geopro 3.0 桌面客户端 — 视觉设计规范(Design System)
|
||||||
|
|
||||||
|
**版本 v1.0** · 适用范围:Geopro 3.0 桌面客户端全部界面
|
||||||
|
**技术载体**:Qt 6(QtWidgets)+ Fusion QStyle + QSS + QPalette + QtAwesome + VTK
|
||||||
|
**模式**:Light(依据原型还原)/ Dark(同一设计语言派生)
|
||||||
|
|
||||||
|
> 本规范是客户端视觉的**单一事实来源**。Claude Code 在实现任何界面时,颜色、间距、字号、圆角、控件尺寸、状态表达一律引用本文档的 token,禁止在各 widget 中即兴硬编色值。所有 token 应集中定义在一个主题模块(见 §13),全局通过主题对象访问。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 设计原则
|
||||||
|
|
||||||
|
1. **数据为主,UI 退后**:中间的 2D/3D 视图与剖面图是视觉焦点,外围面板(树、列表、属性)使用克制的中性色,不与数据争夺注意力。
|
||||||
|
2. **浅色为默认,深色为派生**:原型为浅色界面,Light 为基准模式;Dark 按相同色相、相同层级关系派生,保证两套是「同一语言的明暗版本」。
|
||||||
|
3. **信息密度优先**:勘探软件信息密集,控件紧凑、间距节制、对齐严谨,而非消费级 App 的宽松留白。
|
||||||
|
4. **强调色克制**:主强调色(科技蓝)只用于可交互的主操作、选中态、链接、聚焦;状态色只用于状态表达,不作装饰。
|
||||||
|
5. **视图区永远深色**:无论 Light/Dark 模式,中间的 2D 地图 / 3D 视图 / 剖面图画布**始终是深色衬底**(原型即如此),让色阶数据更突出。这意味着「模式切换」主要影响外围 UI,视图画布的深色基调保持稳定。
|
||||||
|
6. **双模式同构**:同一组件在两种模式下结构、间距、字号完全一致,仅颜色 token 取值不同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 色彩系统(Color Tokens)
|
||||||
|
|
||||||
|
色彩采用**语义化分层**:原始色板(Primitives)→ 语义 token(Semantic)。组件只引用语义 token,不直接引用原始色板,便于换肤。
|
||||||
|
|
||||||
|
### 1.1 原始色板 · 主强调色(Brand / Primary)
|
||||||
|
|
||||||
|
科技蓝,取自原型导航高亮、主按钮、链接、选中态。
|
||||||
|
|
||||||
|
| Token | 色值 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--primary-50` | `#EFF5FF` | 最浅,选中行背景、hover 底 |
|
||||||
|
| `--primary-100` | `#DBE8FE` | 浅强调背景 |
|
||||||
|
| `--primary-200` | `#BFD4FD` | |
|
||||||
|
| `--primary-300` | `#93B4FA` | |
|
||||||
|
| `--primary-400` | `#5E8DF5` | |
|
||||||
|
| `--primary-500` | `#3B73EC` | **主强调色(Light 主按钮、链接)** |
|
||||||
|
| `--primary-600` | `#2B5FD9` | 主按钮 hover |
|
||||||
|
| `--primary-700` | `#2450B8` | 主按钮 pressed |
|
||||||
|
| `--primary-800` | `#21478F` | |
|
||||||
|
| `--primary-900` | `#1B3A6E` | |
|
||||||
|
|
||||||
|
> Dark 模式主强调略微提亮以保证深底对比度:Dark 主强调用 `--primary-400` (`#5E8DF5`),hover 用 `--primary-300`。
|
||||||
|
|
||||||
|
### 1.2 原始色板 · 中性灰阶(Neutral)
|
||||||
|
|
||||||
|
界面骨架色,决定整体气质。
|
||||||
|
|
||||||
|
| Token | 色值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--neutral-0` | `#FFFFFF` | 纯白 |
|
||||||
|
| `--neutral-25` | `#FCFCFD` | 面板底 |
|
||||||
|
| `--neutral-50` | `#F7F8FA` | 应用背景(Light 工作区底) |
|
||||||
|
| `--neutral-100` | `#EFF1F4` | 次级背景、表头底、斑马纹 |
|
||||||
|
| `--neutral-200` | `#E3E6EB` | 边框、分隔线 |
|
||||||
|
| `--neutral-300` | `#CDD2DA` | 输入框边框、禁用边框 |
|
||||||
|
| `--neutral-400` | `#A8AFBC` | 占位文字、禁用文字 |
|
||||||
|
| `--neutral-500` | `#7C8493` | 次要文字、图标默认 |
|
||||||
|
| `--neutral-600` | `#5A626F` | 正文次级 |
|
||||||
|
| `--neutral-700` | `#3E4551` | 正文 |
|
||||||
|
| `--neutral-800` | `#272C35` | 标题文字 |
|
||||||
|
| `--neutral-900` | `#161A20` | 最深文字、Dark 面板底 |
|
||||||
|
| `--neutral-950` | `#0E1116` | Dark 应用背景 |
|
||||||
|
|
||||||
|
### 1.3 原始色板 · 视图画布专用深色(Canvas)
|
||||||
|
|
||||||
|
2D/3D/剖面画布的衬底,两种模式通用。
|
||||||
|
|
||||||
|
| Token | 色值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--canvas-bg` | `#0B1320` | 视图画布主背景(原型深蓝黑) |
|
||||||
|
| `--canvas-bg-soft` | `#111B2D` | 画布内浮层(如「列表显示栏」浮窗)底 |
|
||||||
|
| `--canvas-grid` | `#1E2A3D` | 画布网格线、坐标轴 |
|
||||||
|
| `--canvas-overlay` | `rgba(10,17,28,0.82)` | 画布上的标签底(如 ERT1 标注牌) |
|
||||||
|
| `--canvas-text` | `#E6ECF5` | 画布上文字 |
|
||||||
|
| `--canvas-text-dim` | `#8A97AC` | 画布上次要文字(坐标、比例尺) |
|
||||||
|
|
||||||
|
### 1.4 语义色 · 状态色(Status)
|
||||||
|
|
||||||
|
来自原型异常分级(红=高/低阻异常、橙=中等、蓝=边界/信息)与通用反馈。每个状态含 `主色 / 浅底 / 边框` 三档。
|
||||||
|
|
||||||
|
| 语义 | 主色(Light) | 主色(Dark) | 浅底(Light) | 浅底(Dark) | 用途 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **Danger / 高(红)** | `#E5484D` | `#FF6166` | `#FDECEC` | `#3A1D1F` | 高等级异常、错误、删除 |
|
||||||
|
| **Warning / 中(橙)** | `#E08A1E` | `#F5A623` | `#FBF0DD` | `#3A2C12` | 中等异常、警告 |
|
||||||
|
| **Success(绿)** | `#2E9E5B` | `#46C07A` | `#E7F6ED` | `#16301F` | 成功、在线、就绪 |
|
||||||
|
| **Info / 低(蓝)** | `#3B73EC` | `#5E8DF5` | `#EFF5FF` | `#16243F` | 信息、低等级、边界过渡 |
|
||||||
|
| **Neutral / 离线(灰)** | `#7C8493` | `#8A93A3` | `#F0F1F4` | `#23282F` | 离线、停用、未知 |
|
||||||
|
|
||||||
|
> **异常分级专用**(原型异常列表左侧圆点 + 标签「高/中/低」):高=Danger、中=Warning、低=Info,停用/隐藏=Neutral。三维视图与剖面图中的异常标注牌也用同一组色。
|
||||||
|
|
||||||
|
### 1.5 语义 token 映射表(组件取此层)
|
||||||
|
|
||||||
|
| 语义 token | Light 取值 | Dark 取值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `bg/app` | `neutral-50` | `neutral-950` |
|
||||||
|
| `bg/panel` | `neutral-0` | `neutral-900` |
|
||||||
|
| `bg/panel-subtle` | `neutral-25` | `#161B22` |
|
||||||
|
| `bg/header` | `neutral-0` | `#12161C` |
|
||||||
|
| `bg/hover` | `neutral-100` | `#1B2129` |
|
||||||
|
| `bg/selected` | `primary-50` | `#16243F` |
|
||||||
|
| `bg/canvas` | `canvas-bg` | `canvas-bg` |
|
||||||
|
| `border/default` | `neutral-200` | `#262C35` |
|
||||||
|
| `border/strong` | `neutral-300` | `#333B45` |
|
||||||
|
| `border/focus` | `primary-500` | `primary-400` |
|
||||||
|
| `text/primary` | `neutral-800` | `#E6E9EF` |
|
||||||
|
| `text/secondary` | `neutral-600` | `#A4ADBB` |
|
||||||
|
| `text/tertiary` | `neutral-500` | `#7A8494` |
|
||||||
|
| `text/disabled` | `neutral-400` | `#5A626F` |
|
||||||
|
| `text/link` | `primary-500` | `primary-400` |
|
||||||
|
| `text/on-primary` | `neutral-0` | `neutral-0` |
|
||||||
|
| `accent/primary` | `primary-500` | `primary-400` |
|
||||||
|
| `accent/primary-hover` | `primary-600` | `primary-300` |
|
||||||
|
| `accent/primary-pressed` | `primary-700` | `primary-500` |
|
||||||
|
| `divider` | `neutral-200` | `#22272F` |
|
||||||
|
| `scrollbar/thumb` | `neutral-300` | `#3A424D` |
|
||||||
|
| `scrollbar/thumb-hover` | `neutral-400` | `#4A535F` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 字体与排版(Typography)
|
||||||
|
|
||||||
|
### 2.1 字族
|
||||||
|
|
||||||
|
| 用途 | 字体栈 |
|
||||||
|
|---|---|
|
||||||
|
| 中文 UI | `"Microsoft YaHei UI", "PingFang SC", "Source Han Sans SC", "Noto Sans CJK SC", sans-serif` |
|
||||||
|
| 西文/数字 | `"Segoe UI", "Inter", "Helvetica Neue", Arial, sans-serif` |
|
||||||
|
| 等宽(坐标/数值/编号/日志) | `"Cascadia Code", "JetBrains Mono", "Consolas", monospace` |
|
||||||
|
|
||||||
|
> macOS 优先 PingFang SC + SF Pro。数值、坐标(如 `103.85°E · 36.72°N · z=1.0x`)、批次号、深度刻度一律用等宽字体,保证对齐。
|
||||||
|
|
||||||
|
### 2.2 字号阶梯(pt,桌面端基准)
|
||||||
|
|
||||||
|
| Token | 字号 | 字重 | 行高 | 用途 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `text/display` | 18 | 600 | 26 | 空状态大标题(少用) |
|
||||||
|
| `text/title` | 14 | 600 | 22 | 对话框标题、首选项分组标题 |
|
||||||
|
| `text/heading` | 12.5 | 600 | 20 | 面板标题栏(「对象显示栏」「属性」等) |
|
||||||
|
| `text/body` | 11.5 | 400 | 18 | 正文、列表项、表单值 |
|
||||||
|
| `text/body-strong` | 11.5 | 600 | 18 | 强调正文、选中项 |
|
||||||
|
| `text/label` | 11 | 400 | 16 | 表单标签、次要说明 |
|
||||||
|
| `text/caption` | 10 | 400 | 14 | 辅助信息(日期、计数、单位) |
|
||||||
|
| `text/mono` | 11 | 400 | 16 | 数值/坐标/编号 |
|
||||||
|
|
||||||
|
> 桌面端字号偏小(信息密度优先)。如需适配高 DPI,按系统缩放因子整体放大,不单独改 token。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 间距、圆角、阴影、边框(Spacing / Radius / Elevation)
|
||||||
|
|
||||||
|
### 3.1 间距阶梯(px,4 的倍数节奏)
|
||||||
|
|
||||||
|
| Token | 值 | 典型用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `space/3xs` | 2 | 图标与文字微距 |
|
||||||
|
| `space/2xs` | 4 | 紧凑控件内边距 |
|
||||||
|
| `space/xs` | 6 | 列表项上下内边距 |
|
||||||
|
| `space/sm` | 8 | 控件内边距、小间隔 |
|
||||||
|
| `space/md` | 12 | 面板内边距、表单行距 |
|
||||||
|
| `space/lg` | 16 | 分组间距 |
|
||||||
|
| `space/xl` | 24 | 对话框内边距 |
|
||||||
|
| `space/2xl` | 32 | 大区块分隔 |
|
||||||
|
|
||||||
|
### 3.2 圆角
|
||||||
|
|
||||||
|
| Token | 值 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `radius/none` | 0 | 表格、树(贴合密集布局) |
|
||||||
|
| `radius/sm` | 4 | 按钮、输入框、标签 |
|
||||||
|
| `radius/md` | 6 | 卡片、列表项、浮层 |
|
||||||
|
| `radius/lg` | 8 | 对话框、画布浮窗 |
|
||||||
|
| `radius/pill` | 999 | 胶囊标签、开关、计数徽标 |
|
||||||
|
|
||||||
|
### 3.3 边框
|
||||||
|
|
||||||
|
- 默认边框宽度 `1px`,颜色 `border/default`。
|
||||||
|
- 聚焦态边框 `1px` `border/focus` + 外发光 `0 0 0 2px primary-100`(Light)/ `primary-900 透明度` 描边(Dark)。
|
||||||
|
- 分隔线用 `divider`,`1px`。
|
||||||
|
|
||||||
|
### 3.4 阴影(仅浮层使用,界面整体扁平)
|
||||||
|
|
||||||
|
| Token | Light | Dark |
|
||||||
|
|---|---|---|
|
||||||
|
| `shadow/popover` | `0 4px 16px rgba(20,30,50,0.12)` | `0 4px 16px rgba(0,0,0,0.5)` |
|
||||||
|
| `shadow/dialog` | `0 12px 40px rgba(20,30,50,0.18)` | `0 12px 40px rgba(0,0,0,0.6)` |
|
||||||
|
| `shadow/dropdown` | `0 2px 10px rgba(20,30,50,0.10)` | `0 2px 10px rgba(0,0,0,0.45)` |
|
||||||
|
|
||||||
|
> 树、列表、表格、面板**不使用阴影**(扁平、紧凑)。仅菜单、下拉、tooltip、对话框、画布浮窗使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 布局框架(依据原型还原)
|
||||||
|
|
||||||
|
原型即客户端「项目分析视图」工作台,整体为 **顶栏 + 三栏主体 + 中列上下分割** 的多面板结构(用 ADS 实现停靠)。
|
||||||
|
|
||||||
|
### 4.1 整体栅格
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ 顶栏 TopBar (高 48px) │
|
||||||
|
├────────────┬───────────────────────────────┬─────────────────┤
|
||||||
|
│ 左栏 │ 中列上:2D/3D 视图画布 │ 右栏上:异常列表 │
|
||||||
|
│ 对象显示栏 │ (深色 canvas,含浮窗/工具) │ /对象属性 (Tab) │
|
||||||
|
│ (树) ├───────────────────────────────┤ │
|
||||||
|
│ │ 中列下:数据详情 ├─────────────────┤
|
||||||
|
│ 数据集显示栏 │ (Tab + 工具条 + 剖面图) │ 右栏下:属性 │
|
||||||
|
│ (列表) │ │ (键值表) │
|
||||||
|
└────────────┴───────────────────────────────┴─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 各区尺寸建议
|
||||||
|
|
||||||
|
| 区域 | 尺寸 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 顶栏 TopBar | 高 `48px` | 固定 |
|
||||||
|
| 左栏 | 默认宽 `280px`,可拖拽 `220–400px` | 上下两段:对象树 + 数据集列表 |
|
||||||
|
| 右栏 | 默认宽 `340px`,可拖拽 `280–460px` | 上下两段:异常/属性 Tab + 属性键值表 |
|
||||||
|
| 中列 | 自适应填充 | 上下分割:视图画布(占比大)+ 数据详情 |
|
||||||
|
| 面板标题栏 | 高 `36px` | 含图标 + 标题 + 右侧动作按钮 |
|
||||||
|
| 面板间分隔条(splitter) | `4px` 命中区,视觉 `1px` | hover 显示 `accent/primary` |
|
||||||
|
|
||||||
|
### 4.3 面板标题栏规范(所有可停靠面板统一)
|
||||||
|
|
||||||
|
- 左:`14px` 图标(QtAwesome)+ `text/heading` 标题,可带计数(如「异常列表 2/3」)。
|
||||||
|
- 右:动作按钮区(如 筛选漏斗、+、展开/折叠、全屏),图标按钮 `24×24px`,hover 显示 `bg/hover` 底。
|
||||||
|
- 底部 `1px` `divider`。
|
||||||
|
- 背景 `bg/panel`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 顶栏 TopBar(依据原型)
|
||||||
|
|
||||||
|
从左到右的元素与规范:
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| **Logo** | 左起,高 `24px`,含下拉箭头(切换工作空间/企业) |
|
||||||
|
| **项目名 + 区域** | 图标 + 项目名(`text/body-strong`)+ 区域下拉胶囊(`primary` 文字 + 浅底)|
|
||||||
|
| **主导航**(视图分析/项目管理/业务工具) | 文字 tab,含图标;当前项 `accent/primary` + 底部 `2px` 高亮条;业务工具带下拉箭头 |
|
||||||
|
| **右侧工具组** | 设备(主按钮样式,`accent/primary` 填充)、帮助(?)、通知(铃铛,含红点徽标)、设置(齿轮)、用户头像+姓名+角色 |
|
||||||
|
| 背景 | `bg/header`,底部 `1px` `divider` |
|
||||||
|
| 图标按钮尺寸 | `32×32px`,hover `bg/hover` |
|
||||||
|
| 通知红点 | `8px` 圆点,`Danger` 色,右上角 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 核心组件规范
|
||||||
|
|
||||||
|
### 6.1 对象树(左栏上 · 对象显示栏)
|
||||||
|
|
||||||
|
原型特征:多级树,每行含「复选框 + 状态圆点 + 类型图标 + 名称 + 右侧计数」。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 行高 | `28px` |
|
||||||
|
| 缩进 | 每级 `16px`,展开箭头 `12px` |
|
||||||
|
| 复选框 | `14px`,选中 `accent/primary` 填充 + 白勾;三态(半选)用横线 |
|
||||||
|
| 状态圆点 | `8px` 实心圆,颜色映射对象状态/数据集色(蓝/绿/橙/红,与右侧异常色一致)|
|
||||||
|
| 类型图标 | `14px` QtAwesome,`text/secondary` 色 |
|
||||||
|
| 名称 | `text/body`;选中行 `text/body-strong` |
|
||||||
|
| 计数徽标 | 右对齐,`text/caption`,`text/tertiary` 色;可加 `pill` 浅底 |
|
||||||
|
| 选中行 | 底 `bg/selected`,左侧 `2px` `accent/primary` 竖条 |
|
||||||
|
| hover 行 | 底 `bg/hover` |
|
||||||
|
| 分组节点(GS) | 名称略强,可加定位图标 |
|
||||||
|
|
||||||
|
### 6.2 数据集列表(左栏下 · 数据集显示栏)
|
||||||
|
|
||||||
|
原型特征:顶部 Tab(数据 / 文件,带计数)+ 列表项(含状态点、标题、日期·通道数)。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 段 Tab | 「数据 2 / 文件 3」,胶囊式分段控件(见 6.9)|
|
||||||
|
| 列表项高 | `52px`(双行:标题 + 元信息)|
|
||||||
|
| 标题 | `text/body`,含前置状态圆点 |
|
||||||
|
| 元信息行 | `text/caption` `text/tertiary`,如「2026-03-15 09:21 · 64 道」|
|
||||||
|
| 选中项 | `bg/selected` + 左 `2px` 竖条 + `radius/md` |
|
||||||
|
| 右上工具 | 筛选漏斗、导出图标 |
|
||||||
|
|
||||||
|
### 6.3 异常列表(右栏上 Tab1)
|
||||||
|
|
||||||
|
原型特征:每条含左侧状态竖条 + 圆点 + 名称 + 等级标签 + 多行属性 + 右侧眼睛(显隐)。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 卡片项 | `radius/md`,左 `3px` 状态色竖条,内距 `space/sm` |
|
||||||
|
| 卡片底 | 对应状态浅底(高=Danger 浅底、中=Warning 浅底、低=Info 浅底)|
|
||||||
|
| 名称 | `text/body-strong` + 圆点 |
|
||||||
|
| 等级标签 | 胶囊标签,状态色(见 6.8)|
|
||||||
|
| 属性行 | `text/caption`,如「140m · 18m / 32 Ω·m」,数值用等宽 |
|
||||||
|
| 显隐开关 | 右侧眼睛图标,开=`text/secondary`,关=`text/disabled` + 斜杠眼 |
|
||||||
|
| 标题计数 | 「异常列表 2/3」=可见/总数 |
|
||||||
|
|
||||||
|
### 6.4 属性键值表(右栏下 · 属性)
|
||||||
|
|
||||||
|
原型特征:两列键值,左键右值,多组。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 行高 | `28px` |
|
||||||
|
| 键 | 左列,`text/label` `text/secondary`,定宽约 `72px` |
|
||||||
|
| 值 | 右列,`text/body` `text/primary`,数值/日期用等宽;可右对齐 |
|
||||||
|
| 分组标题 | `text/heading`,上留 `space/md` |
|
||||||
|
| 可编辑值 | hover 显示编辑图标;进入编辑变为内联输入框 |
|
||||||
|
| 链接值 | 指向其他数据集的属性显示为 `text/link`,点击跳转新建详情页 |
|
||||||
|
|
||||||
|
### 6.5 视图画布(中列上 · 2D/3D)
|
||||||
|
|
||||||
|
**始终深色**(`bg/canvas`),不随模式切换变浅。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 视图切换 | 左上「二维地图 / 三维视图」分段 tab,深色版分段控件 |
|
||||||
|
| 画布浮窗(列表显示栏) | 左上浮层,底 `canvas-bg-soft` + `radius/lg` + `shadow/popover`,半透明 |
|
||||||
|
| 画布内文字 | `canvas-text` / `canvas-text-dim` |
|
||||||
|
| 标注牌(ERT1 等) | 底 `canvas-overlay`,白字,`radius/sm` |
|
||||||
|
| 右上控件 | 底图切换下拉(天地图等)、缩放 +/-,深色按钮 |
|
||||||
|
| 右下状态条 | 坐标/比例尺,`canvas-text-dim`,等宽字体 |
|
||||||
|
| 缩放按钮 | `28×28px`,深色半透明底,hover 提亮 |
|
||||||
|
| 顶部状态徽标 | 如「3/4 测线可见」,圆点 + `text/caption` |
|
||||||
|
|
||||||
|
### 6.6 数据详情区(中列下 · 剖面图)
|
||||||
|
|
||||||
|
原型特征:Tab(采集批次)+ 二级 Tab(原数据/网格数据)+ 工具条 + 剖面图 + 色阶条。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 标题栏 | 图标 +「数据详情」+ 右侧设置/下载图标 |
|
||||||
|
| 批次 Tab | 可关闭的多 Tab(数据集详情以 Tab 呈现,见 6.10)|
|
||||||
|
| 二级 Tab | 「原数据 / 网格数据」文字 tab |
|
||||||
|
| 工具条 | 一排工具按钮(异常标注/色阶配置/白化/滤波处理)= 次按钮(描边)样式,带图标;右侧为复选框组(显示异常/等值线等)+ 滑块(简化容差)+ 右端图标按钮(网格/另存为)|
|
||||||
|
| 复选框组 | 行内排列,`14px` 复选框 + `text/label` |
|
||||||
|
| 滑块 | 见 6.13 |
|
||||||
|
| 剖面图画布 | 深色衬底;色阶填充用数据色阶(见 §8)|
|
||||||
|
| 色阶条(legend) | 底部水平色带 + 刻度(等宽数字)+ 单位标签 |
|
||||||
|
| 深度/距离轴 | 轴标题 `text/caption`,刻度等宽 |
|
||||||
|
|
||||||
|
### 6.7 按钮(Button)
|
||||||
|
|
||||||
|
| 类型 | Light | Dark | 用途 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Primary(主)** | 底 `accent/primary` 白字,hover `accent/primary-hover` | 同(用 Dark 强调取值) | 设备、确定、主操作;每个区域≤1个 |
|
||||||
|
| **Secondary(次/描边)** | 描边 `border/strong` + `text/primary`,hover `bg/hover` | 同 | 工具条按钮、取消 |
|
||||||
|
| **Tertiary(文字/幽灵)** | 无边无底 `text/secondary`,hover `bg/hover` | 同 | 次要动作、图标按钮 |
|
||||||
|
| **Danger** | 底/描边 `Danger` | 同 | 删除等破坏性操作 |
|
||||||
|
| **Link** | `text/link` 无底 | 同 | 内联跳转 |
|
||||||
|
|
||||||
|
- 高度:标准 `28px`,紧凑(工具条)`26px`,大(对话框主操作)`32px`。
|
||||||
|
- 内距:水平 `space/md`,图标与文字间距 `space/xs`。
|
||||||
|
- 圆角 `radius/sm`。禁用态:`text/disabled` + 不变底色 + 光标禁用。
|
||||||
|
|
||||||
|
### 6.8 标签 / 徽标(Tag / Badge)
|
||||||
|
|
||||||
|
| 类型 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 状态标签(高/中/低) | 胶囊 `radius/pill`,状态浅底 + 状态主色文字,`text/caption`,内距 `space/2xs space/sm` |
|
||||||
|
| 计数徽标 | 圆形/胶囊,`text/tertiary` + `neutral-100` 底;通知红点为纯 `Danger` 圆点 |
|
||||||
|
| 类型徽标 | 中性,`neutral-100` 底 + `text/secondary` |
|
||||||
|
|
||||||
|
### 6.9 分段控件(Segmented / 视图切换、数据/文件 Tab)
|
||||||
|
|
||||||
|
- 容器:`neutral-100` 底(Dark `#1B2129`)+ `radius/md`,内距 `2px`。
|
||||||
|
- 选中段:`bg/panel` 底 + `text/primary` + 轻阴影(浮起感)。
|
||||||
|
- 未选段:透明 + `text/secondary`,hover `text/primary`。
|
||||||
|
- 深色画布上的分段控件(视图切换)用深色版:容器半透明深底,选中段 `accent/primary` 文字。
|
||||||
|
|
||||||
|
### 6.10 标签页(Tabs · 数据集详情多 Tab)
|
||||||
|
|
||||||
|
- 标签:`text/body`,当前项 `text/primary` + 底部 `2px` `accent/primary`;非当前 `text/secondary`。
|
||||||
|
- 可关闭:每个 Tab 右侧 `×`(hover 显示),底色 hover `bg/hover`。
|
||||||
|
- 新建详情页 = 新增 Tab;列表选中与 Tab 双向联动(选中 Tab 高亮)。
|
||||||
|
- 溢出:超出宽度显示左右滚动箭头或下拉列出全部 Tab。
|
||||||
|
|
||||||
|
### 6.11 大视图模式(剖面/属性页全屏)
|
||||||
|
|
||||||
|
- 触发后该详情页覆盖「标题菜单以下区域」,其余面板隐藏。
|
||||||
|
- 右上角显示「按 Esc 退出大视图」提示条(`canvas-overlay` 底,`text/caption`,2 秒后淡出)。
|
||||||
|
|
||||||
|
### 6.12 复选框 / 单选 / 开关
|
||||||
|
|
||||||
|
| 控件 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 复选框 | `14px`,未选描边 `border/strong`;选中 `accent/primary` 填充 + 白勾;半选横线;禁用降透明 |
|
||||||
|
| 单选框 | `14px` 圆,选中 `accent/primary` 圆环 + 实心点 |
|
||||||
|
| 开关 Switch | 宽 `36px` 高 `20px` 胶囊,关=`neutral-300` 底,开=`accent/primary` 底,滑块白色 `16px` |
|
||||||
|
|
||||||
|
### 6.13 滑块(Slider · 如简化容差)
|
||||||
|
|
||||||
|
- 轨道 `4px` `neutral-200`(Dark `#2A313B`),已填充段 `accent/primary`。
|
||||||
|
- 滑块手柄 `14px` 白圆 + `1px` `border/strong` + 轻阴影;hover 放大到 `16px`。
|
||||||
|
- 当前值标签:右侧等宽数字。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 表单、表格、对话框等通用组件(补充 · 原型未全部出现但客户端必备)
|
||||||
|
|
||||||
|
### 7.1 输入框(Text Input)
|
||||||
|
|
||||||
|
| 状态 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 默认 | 高 `28px`,底 `bg/panel`,描边 `1px border/default`,`radius/sm`,内距 `space/sm`,`text/body` |
|
||||||
|
| hover | 描边 `border/strong` |
|
||||||
|
| focus | 描边 `border/focus` + 外发光 |
|
||||||
|
| 禁用 | 底 `neutral-50`(Dark `#1A1F26`)+ `text/disabled` |
|
||||||
|
| 错误 | 描边 `Danger` + 下方 `text/caption` `Danger` 错误说明 |
|
||||||
|
| 占位符 | `text/disabled` |
|
||||||
|
| 前/后缀 | 单位、图标置于框内两端,`text/tertiary` |
|
||||||
|
|
||||||
|
### 7.2 下拉选择(ComboBox / Select)
|
||||||
|
|
||||||
|
- 外观同输入框 + 右侧 `12px` 下拉箭头。
|
||||||
|
- 展开菜单:`bg/panel` + `radius/md` + `shadow/dropdown`;项高 `28px`,hover `bg/hover`,选中项 `bg/selected` + 勾。
|
||||||
|
- 可搜索下拉:顶部带搜索输入框。
|
||||||
|
- 多选下拉:选中项以 6.8 标签形式回填到框内。
|
||||||
|
|
||||||
|
### 7.3 数字输入 / 步进器(SpinBox)
|
||||||
|
|
||||||
|
- 输入框 + 右侧上下步进按钮(各 `14px` 高);数值等宽右对齐;支持单位后缀。
|
||||||
|
|
||||||
|
### 7.4 表格(Table / DataGrid)
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 表头 | 底 `neutral-100`(Dark `#161B22`),`text/label` `text/secondary`,高 `32px`,可排序列带箭头 |
|
||||||
|
| 行高 | 标准 `32px`,紧凑 `28px` |
|
||||||
|
| 斑马纹 | 偶数行 `bg/panel-subtle`(可关闭) |
|
||||||
|
| 行 hover | `bg/hover` |
|
||||||
|
| 行选中 | `bg/selected` + 左 `2px` 竖条(多选用复选框列) |
|
||||||
|
| 单元格 | 内距 `space/sm`,文本 `text/body`,数值等宽右对齐 |
|
||||||
|
| 边框 | 仅横向 `1px divider`(无竖线,保持轻盈);密集模式可加竖线 |
|
||||||
|
| 固定列/表头 | 横向滚动时首列与表头固定 |
|
||||||
|
| 空状态 | 居中插画/图标 + `text/secondary` 说明 + 可选操作按钮 |
|
||||||
|
| 分页/加载 | 底部分页器或无限滚动;加载中显示骨架行 |
|
||||||
|
|
||||||
|
### 7.5 对话框(Dialog / Modal)
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 遮罩 | 全屏 `rgba(10,16,26,0.45)`(Dark `rgba(0,0,0,0.6)`) |
|
||||||
|
| 容器 | `bg/panel` + `radius/lg` + `shadow/dialog`;宽按内容(小 `420px` / 中 `560px` / 大 `720px`) |
|
||||||
|
| 标题栏 | `text/title` + 右上 `×`;底 `1px divider`;内距 `space/xl` |
|
||||||
|
| 内容区 | 内距 `space/xl`,可滚动 |
|
||||||
|
| 底部操作栏 | 右对齐:次按钮(取消)+ 主按钮(确定);破坏性操作主按钮用 Danger;内距 `space/lg space/xl` |
|
||||||
|
| 进入动画 | 淡入 + 轻微上移(120ms) |
|
||||||
|
|
||||||
|
### 7.6 确认 / 警示框(Confirm / Alert)
|
||||||
|
|
||||||
|
- 小对话框,左侧状态图标(Danger/Warning/Info 圆形图标)+ 标题 + 说明文 + 操作按钮。
|
||||||
|
- 删除类:图标 Danger,主按钮 Danger,文案明确后果。
|
||||||
|
|
||||||
|
### 7.7 提示框(Toast / Notification)
|
||||||
|
|
||||||
|
| 类型 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| Toast(瞬时) | 右上或底部居中浮出,`bg/panel` + 状态色左竖条 + 图标 + 文案;`radius/md` + `shadow/popover`;3–4s 自动消失;可手动关闭 |
|
||||||
|
| 行内提示(Inline alert) | 块状,状态浅底 + 状态色左竖条 + 图标 + 文案;用于表单顶部、面板内提示 |
|
||||||
|
| 通知中心(铃铛) | 顶栏铃铛下拉面板,列表项含图标/标题/时间/已读态;未读左侧圆点 |
|
||||||
|
|
||||||
|
### 7.8 Tooltip / Popover
|
||||||
|
|
||||||
|
- Tooltip:深色小气泡(Light 也用深色 `neutral-800` 底 + 白字),`text/caption`,`radius/sm`,延迟 `400ms` 显示。
|
||||||
|
- Popover:`bg/panel` + `radius/md` + `shadow/popover`,可含富内容(如对象快速属性 tip)。
|
||||||
|
|
||||||
|
### 7.9 菜单(右键菜单 / 下拉菜单)
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 容器 | `bg/panel` + `radius/md` + `shadow/dropdown`,内距 `space/2xs` |
|
||||||
|
| 项 | 高 `28px`,左图标 `14px` + 文字 + 右侧快捷键(`text/tertiary` 等宽)|
|
||||||
|
| hover | `bg/hover` |
|
||||||
|
| 分隔 | `1px divider`,上下 `space/2xs` |
|
||||||
|
| 危险项 | `Danger` 文字(如删除)|
|
||||||
|
| 子菜单 | 右侧箭头,hover 展开 |
|
||||||
|
| 禁用项 | `text/disabled` |
|
||||||
|
|
||||||
|
> 对象树/数据集右键菜单按规约功能:显示/隐藏、定位、属性、异常详情、编辑、新建 GS/TM、导入数据集、删除等。
|
||||||
|
|
||||||
|
### 7.10 首选项 / 设置(Preferences)
|
||||||
|
|
||||||
|
原型顶栏齿轮入口,客户端必备完整设置界面。
|
||||||
|
|
||||||
|
| 元素 | 规范 |
|
||||||
|
|---|---|
|
||||||
|
| 结构 | 左侧分类导航(垂直列表)+ 右侧设置项面板(主从布局)|
|
||||||
|
| 分类项 | 高 `32px`,图标 + 名称,选中 `bg/selected` + 左竖条 |
|
||||||
|
| 设置项行 | 左:标题(`text/body`)+ 说明(`text/caption text/tertiary`);右:控件(开关/下拉/输入)|
|
||||||
|
| 分组 | 分组标题 `text/heading` + `divider` |
|
||||||
|
| 建议分类 | 外观(**主题:跟随系统/浅色/深色**、字体、字号缩放)、启动画面与停留时间、默认大屏、语言(中/英)、坐标系默认、底图与缓存、更新(检查/通道)、账户、关于(版本/许可/Qt 源码声明)|
|
||||||
|
|
||||||
|
### 7.11 空状态 / 加载 / 骨架屏
|
||||||
|
|
||||||
|
- **空状态**:居中 `48px` 灰度图标 + `text/body secondary` 主文案 + `text/caption` 辅助 + 可选主操作按钮。
|
||||||
|
- **加载**:局部用旋转 spinner(`accent/primary`);区块用骨架屏(`neutral-100`/`#1B2129` 矩形微动画)。
|
||||||
|
- **进度**:长任务(如在线更新、VTK 大数据加载)用进度条 `accent/primary` + 百分比等宽数字。
|
||||||
|
|
||||||
|
### 7.12 滚动条
|
||||||
|
|
||||||
|
- 细滚动条:宽 `8px`,thumb `scrollbar/thumb` + `radius/pill`,hover `scrollbar/thumb-hover`,轨道透明;overlay 模式(悬停才显)。
|
||||||
|
|
||||||
|
### 7.13 树/列表筛选器(漏斗)
|
||||||
|
|
||||||
|
- 面板标题栏漏斗图标 → 弹出 Popover:含搜索框 + 多选类型复选组 + 日期范围 + 「应用/重置」。
|
||||||
|
- 激活筛选时漏斗图标显示 `accent/primary` + 角标计数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. VTK 视图配色(与 UI 对齐)
|
||||||
|
|
||||||
|
> **关键约束**:QSS/QPalette 不作用于 VTK 渲染窗口。VTK 画布配色须通过 VTK API 单独设置,并**与 §1.3 画布 token 手动对齐**,避免「UI 深色、视图另一种灰」的割裂。VTK 画布在两种模式下**都用深色**。
|
||||||
|
|
||||||
|
### 8.1 渲染窗口基础
|
||||||
|
|
||||||
|
| VTK 元素 | 取值(对齐 token) |
|
||||||
|
|---|---|
|
||||||
|
| 渲染器背景(renderer background) | `canvas-bg` `#0B1320`;可用上下渐变到 `#0E1626` |
|
||||||
|
| 三维网格/地面网格 | `canvas-grid` `#1E2A3D` |
|
||||||
|
| 坐标轴(axes actor) | X/Y/Z 用低饱和 红/绿/蓝,标签 `canvas-text-dim` |
|
||||||
|
| 文字标注(vtkTextActor) | `canvas-text` `#E6ECF5`,等宽字体 |
|
||||||
|
| 比例尺/方位 | `canvas-text-dim` |
|
||||||
|
| 拾取高亮 | `accent/primary` `#5E8DF5` 描边 |
|
||||||
|
| 选中对象包围盒 | `accent/primary` 虚线框 |
|
||||||
|
|
||||||
|
### 8.2 数据色阶(Colormap / Lookup Table)
|
||||||
|
|
||||||
|
原型剖面图色阶为**电阻率经典彩虹色阶**(紫蓝→青绿→黄→橙红→深红,对数刻度 5–1000 Ω·m)。规范:
|
||||||
|
|
||||||
|
| 色阶用途 | 推荐 colormap | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 电阻率/极化率(默认) | 彩虹/Jet 类(紫→蓝→青→绿→黄→橙→红) | 与原型一致;对数刻度;端点可标注异常 |
|
||||||
|
| 连续物理量(通用) | Viridis / Turbo | 感知均匀,科学制图推荐 |
|
||||||
|
| 发散量(正负、相对基线) | Coolwarm(蓝-白-红) | 如电位差、变化量 |
|
||||||
|
| 地层/分类(离散) | 离散调色板(命名色板) | 类别明确,高区分度 |
|
||||||
|
| 单色强度 | 单色渐变(如蓝系) | 单一属性强度 |
|
||||||
|
|
||||||
|
- 色阶须**可配置、可命名保存**(规约「色阶定义工具」),用户调整后视图实时刷新。
|
||||||
|
- 色阶条(legend)渲染在 UI 侧或画布侧均可,但**刻度数字用等宽字体**,单位明确。
|
||||||
|
- 等值线(contour):默认 `canvas-text-dim` 细线;标注牌用 `canvas-overlay` 底。
|
||||||
|
|
||||||
|
### 8.3 异常标注(三维视图 + 剖面图)
|
||||||
|
|
||||||
|
- 异常圈/标注牌颜色 = §1.4 异常分级色(高=Danger、中=Warning、低=Info),与右栏异常列表**严格一致**。
|
||||||
|
- 三维视图中的测线:可见用亮色(红/绿区分极性或测线编号),不可见降透明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 图标系统(QtAwesome)
|
||||||
|
|
||||||
|
- **统一用 QtAwesome**(图标字体),全局禁止混用位图图标,保证矢量、可染色、随主题变色。
|
||||||
|
- 图标字体集:优先 **Material Design Icons (mdi)** 或 **Font Awesome**,全项目统一一套。
|
||||||
|
- 标准尺寸:行内 `14px`,按钮内 `16px`,面板标题 `14px`,空状态 `48px`。
|
||||||
|
- 默认色 `text/secondary`;激活/选中 `accent/primary`;禁用 `text/disabled`。
|
||||||
|
- 图标语义映射(建议固定,供 Claude Code 一致引用):
|
||||||
|
|
||||||
|
| 语义 | 图标(mdi 名参考) |
|
||||||
|
|---|---|
|
||||||
|
| 对象树/层级 | `file-tree` / `sitemap` |
|
||||||
|
| GS 分组/定位 | `map-marker` |
|
||||||
|
| 数据集 | `database` / `chart-line` |
|
||||||
|
| 文件 | `file-document` |
|
||||||
|
| 二维地图 | `map` |
|
||||||
|
| 三维视图 | `cube-outline` |
|
||||||
|
| 异常 | `alert-circle` |
|
||||||
|
| 属性 | `information-outline` |
|
||||||
|
| 筛选 | `filter-variant` |
|
||||||
|
| 新增 | `plus` |
|
||||||
|
| 导出/下载 | `download` / `export` |
|
||||||
|
| 显隐 | `eye` / `eye-off` |
|
||||||
|
| 全屏 | `fullscreen` / `fullscreen-exit` |
|
||||||
|
| 设置 | `cog` |
|
||||||
|
| 通知 | `bell` |
|
||||||
|
| 帮助 | `help-circle` |
|
||||||
|
| 设备 | `access-point` / `chip` |
|
||||||
|
| 色阶配置 | `palette` |
|
||||||
|
| 白化/滤波 | `blur` / `wave` |
|
||||||
|
| 网格 | `grid` |
|
||||||
|
| 缩放 | `magnify-plus` / `magnify-minus` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 状态与交互反馈
|
||||||
|
|
||||||
|
| 交互态 | 表现 |
|
||||||
|
|---|---|
|
||||||
|
| hover | 背景 `bg/hover` 或控件提亮,`120ms` 过渡 |
|
||||||
|
| pressed | 加深一档 |
|
||||||
|
| focus(键盘) | `border/focus` + 外发光,**键盘可达性必须保留** |
|
||||||
|
| selected | `bg/selected` + 左竖条 |
|
||||||
|
| disabled | `text/disabled` + 降透明 + 禁用光标 |
|
||||||
|
| loading | spinner / 骨架 / 进度条 |
|
||||||
|
| drag(异常合并、图层排序) | 拖拽项半透明跟随 + 目标位置 `accent/primary` 插入线 |
|
||||||
|
| 实时刷新(色阶/图例调整) | 视图即时重绘,无整页闪烁 |
|
||||||
|
|
||||||
|
> 动效克制:仅用 `120–200ms` 的 hover/展开/淡入过渡,专业工具避免花哨动画。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 双模式切换规范
|
||||||
|
|
||||||
|
- 三种选项:**跟随系统 / 浅色 / 深色**(首选项 §7.10 + 顶栏快捷切换可选)。
|
||||||
|
- 切换时:外围 UI(顶栏、面板、树、列表、表单、对话框)整体换肤;**视图画布与剖面图保持深色基调不变**(仅 UI 边框等微调)。
|
||||||
|
- 切换应即时、无重启;通过统一主题对象广播刷新所有 widget 的 QSS + QPalette。
|
||||||
|
- VTK 渲染器背景在两模式下均深色,不参与切换(仅当用户在首选项单独设置画布背景时才改)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 可访问性与适配
|
||||||
|
|
||||||
|
- **对比度**:正文文字与背景对比度 ≥ 4.5:1,大字 ≥ 3:1(两种模式都须校验,Dark 模式尤其注意状态浅底上的文字)。
|
||||||
|
- **不以颜色为唯一信息**:异常分级除颜色外必须带文字标签(高/中/低)或图标,照顾色觉障碍。
|
||||||
|
- **高 DPI**:所有尺寸用逻辑像素,随系统缩放因子整体放大;图标用矢量字体。
|
||||||
|
- **键盘导航**:Tab 焦点环必须可见;树/列表/表格支持方向键。
|
||||||
|
- **最小命中区**:可点击图标按钮命中区 ≥ `24×24px`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 实现约定(给 Claude Code)
|
||||||
|
|
||||||
|
1. **集中定义 token**:建一个主题模块(如 `src/ui/theme/`),包含 `Theme` 对象,集中持有所有颜色/间距/字号 token 的两套取值(light/dark)。所有 widget 通过 `theme.color("bg/panel")` 一类接口取值,**禁止散落硬编色值**。
|
||||||
|
2. **QSS 模板化**:QSS 写成带占位符的模板,运行时用当前 token 填充生成最终 QSS 字符串并 `qApp->setStyleSheet()`;切换模式时重新生成并应用。
|
||||||
|
3. **QPalette 同步**:除 QSS 外**必须同步设置 QPalette**(Window/Base/Text/Highlight/ButtonText 等角色),否则原生绘制控件(菜单、tooltip、部分原生项)颜色不统一。两者取值来自同一 token。
|
||||||
|
4. **Fusion 为底座**:`QApplication::setStyle("Fusion")`,在其上叠加 QSS + QPalette,不替换为第三方 QStyle。
|
||||||
|
5. **VTK 配色独立设置**:VTK 渲染器背景、坐标轴、文字、色阶用 VTK API 设置,取值引用 §8 / §1.3,与 UI token 对齐;模式切换时画布保持深色。
|
||||||
|
6. **图标统一走 QtAwesome**:封装一个图标工具函数 `icon(name, role)`,role 决定染色(默认/激活/禁用/状态色),全局复用 §9 映射。
|
||||||
|
7. **主题切换广播**:主题切换时通过信号通知所有顶层窗口重应用 QSS/QPalette 并刷新 QtAwesome 图标颜色。
|
||||||
|
8. **可视化校验**:实现后用 light/dark 各截图核对本规范(对比度、状态色一致性、视图画布与 UI 的衔接)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 A:核心语义色速查(开发对照)
|
||||||
|
|
||||||
|
### Light
|
||||||
|
|
||||||
|
| 用途 | 色值 |
|
||||||
|
|---|---|
|
||||||
|
| 应用背景 | `#F7F8FA` |
|
||||||
|
| 面板背景 | `#FFFFFF` |
|
||||||
|
| hover | `#EFF1F4` |
|
||||||
|
| 选中 | `#EFF5FF` |
|
||||||
|
| 边框 | `#E3E6EB` |
|
||||||
|
| 主文字 | `#272C35` |
|
||||||
|
| 次文字 | `#5A626F` |
|
||||||
|
| 主强调 | `#3B73EC` |
|
||||||
|
| 视图画布 | `#0B1320` |
|
||||||
|
| 高/Danger | `#E5484D` |
|
||||||
|
| 中/Warning | `#E08A1E` |
|
||||||
|
| 低/Info | `#3B73EC` |
|
||||||
|
| 成功 | `#2E9E5B` |
|
||||||
|
|
||||||
|
### Dark
|
||||||
|
|
||||||
|
| 用途 | 色值 |
|
||||||
|
|---|---|
|
||||||
|
| 应用背景 | `#0E1116` |
|
||||||
|
| 面板背景 | `#161A20` |
|
||||||
|
| hover | `#1B2129` |
|
||||||
|
| 选中 | `#16243F` |
|
||||||
|
| 边框 | `#262C35` |
|
||||||
|
| 主文字 | `#E6E9EF` |
|
||||||
|
| 次文字 | `#A4ADBB` |
|
||||||
|
| 主强调 | `#5E8DF5` |
|
||||||
|
| 视图画布 | `#0B1320`(同 Light) |
|
||||||
|
| 高/Danger | `#FF6166` |
|
||||||
|
| 中/Warning | `#F5A623` |
|
||||||
|
| 低/Info | `#5E8DF5` |
|
||||||
|
| 成功 | `#46C07A` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本规范依据客户提供的 Web 原型(浅色)提取并派生深色模式,覆盖项目分析视图工作台及客户端通用组件。色值为初始建议值,落地后应结合实机截图微调对比度与一致性,并随设计迭代维护版本。*
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# geopro_desktop → ElaWidgetTools (Fluent) 迁移计划
|
||||||
|
|
||||||
|
**分支**:`feat/elawidgettools` **日期**:2026-06-09 **决策**:全面 Ela 化(最彻底),支持明/暗主题。
|
||||||
|
**前置评估已完成**:spike(`spike/ela/`) 证明 ElaWidgetTools(RainbowCandyX fork) 可用官方 Qt 6.11.1 + MSVC 经 FetchContent 构建,ElaWindow + ADS 内嵌 + QVTK 渲染均可行;ElaTheme 明暗切换可用,但**只自动覆盖 Ela\* 控件与 ElaWindow 外壳**,标准 QWidget/ADS/VTK 需手工主题联动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 硬前提(动手前必须满足)
|
||||||
|
|
||||||
|
- **P0-a 清理 C: 盘**:当前 C: 0 GB 可用,链接器写 `%TEMP%` 失败(`LNK1108`)。迁移需大量构建验证。需用户清理 C:;我同时把 `TEMP/TMP→D:` 兜底固化进 `build.bat`,避免反复手动重定向。
|
||||||
|
- **P0-b 验证靠用户**:每阶段我构建通过后,用户运行 + 截图,我据反馈迭代(登录门槛 + GUI,我无法目视)。
|
||||||
|
- **P0-c 回退保障**:全程保留 `GEOPRO_UI_SHELL=classic|ela` 环境变量开关,可在「现 QMainWindow 壳」与「ElaWindow 壳」间切换,便于 A/B 与回退;迁移稳定后再移除。
|
||||||
|
|
||||||
|
## 1. 依赖固化(P0 工程)
|
||||||
|
|
||||||
|
- ElaWidgetTools 从 spike 提升为**正式依赖**:`FetchContent` 的 `GIT_TAG` 由 `main` **钉到具体 commit**(可复现)。
|
||||||
|
- **静/动态**:先静态(MIT,省 DLL);若遇静态资源(字体/SVG 图标 .qrc)被剥离导致图标缺失,改动态(`ELAWIDGETTOOLS_BUILD_STATIC_LIB OFF` + DLL 随 `TARGET_RUNTIME_DLLS` 拷贝)。
|
||||||
|
- **插件部署**:把 `platforms / styles / imageformats / iconengines`(含 SVG 图标用的 `qsvg`/`qsvgicon`)部署接进 `geopro_desktop` 的 post-build(今天 spike 是手动拷的,要正式化;windeployqt 会被 ADS 的 DLL 依赖卡住,改为显式 copy Qt plugins)。
|
||||||
|
|
||||||
|
## 2. P1 — 换壳(带开关)
|
||||||
|
|
||||||
|
- 新建 `ElaShellWindow`(继承 `ElaWindow`)或在 `main()` 分支构造。把现有 `buildWorkbench()` 产出的中心内容(ADS `CDockManager` + 工具条)作为 ElaWindow 的一个 page/central content 挂入(`addPageNode` / `setCentralCustomWidget`)。
|
||||||
|
- `eApp->init()` 在 `QApplication` 后调用;保留高 DPI 与 QVTK surface format 设置顺序。
|
||||||
|
- **dock 持久化注意**:ElaWindow 接管后,`restoreState` 后重隐藏 ADS 标题栏的时序修复(已在主分支)需在新壳下复核。
|
||||||
|
- **验收**:`GEOPRO_UI_SHELL=ela` 启动 → 登录 → 工作台;ADS 停靠可拖动;中央/详情 VTK 正常;导航/标题栏 Fluent 外观。截图确认。
|
||||||
|
|
||||||
|
## 3. P2 — 主题桥(明/暗覆盖所有非 Ela 面)
|
||||||
|
|
||||||
|
- 新建 `ThemeBridge`:监听 `ElaTheme::themeModeChanged`,把明/暗同步到:
|
||||||
|
1. **全局 QSS**:把 `Theme.cpp` 的 `kStyleSheet` 拆成「明」与「暗」两版(用已有 `type/space/radius/semantic` 令牌派生暗色盘),按主题切换。
|
||||||
|
2. **ADS 停靠区**:`CDockManager::setStyleSheet` 明/暗两套。
|
||||||
|
3. **VTK 背景**:中央 + 详情 renderer 背景随主题切深/浅底并 `Render()`。
|
||||||
|
4. **内联样式面板**:PanelHeader / TopBar / LoginWindow / main 浮层 的内联 QSS 改为「跟随主题」(去硬编码色,引用桥提供的明/暗令牌)。
|
||||||
|
- **暗色盘设计**:在 `Theme.hpp` 增加暗色语义(surface/ink/border/accent 的暗版),保持品牌蓝在暗底的可读性与对比度(≥4.5:1)。
|
||||||
|
- **验收**:明/暗一键切换,全界面(外壳+停靠+面板+VTK)协调一致、无残留亮/暗块;对比度达标。截图明、暗各一。
|
||||||
|
|
||||||
|
## 4. P3 — 全面控件 Ela 化(工作量主体)
|
||||||
|
|
||||||
|
逐面替换标准控件为 `Ela*` 等价物,"白嫖"明暗与 Fluent 观感。映射(精确 Ela 类名在实施时按头文件确认):
|
||||||
|
|
||||||
|
| 现状 | → Ela 等价 | 所在 |
|
||||||
|
|---|---|---|
|
||||||
|
| QPushButton | ElaPushButton | LoginWindow / 各处 |
|
||||||
|
| QLineEdit | ElaLineEdit | LoginWindow / ProjectListDialog 过滤 |
|
||||||
|
| QCheckBox | ElaCheckBox | LoginWindow / 图层浮层 / 异常列表 |
|
||||||
|
| QComboBox | ElaComboBox | ProjectListDialog / 全局 |
|
||||||
|
| QLabel(文本) | ElaText | 各处文本/标题 |
|
||||||
|
| QToolButton(Tab/操作) | ElaToolButton / ElaIconButton | PanelHeader / TopBar |
|
||||||
|
| QMenuBar / QMenu | ElaMenuBar / ElaMenu | TopBar |
|
||||||
|
| QTreeWidget | ElaTreeView(+model) 或保留+主题联动 | ObjectTreePanel |
|
||||||
|
| QListWidget | ElaListView(+model) 或保留+联动 | Dataset/Anomaly 面板 |
|
||||||
|
| QTableWidget | ElaTableView 或保留+联动 | ProjectListDialog |
|
||||||
|
| QProgressBar | ElaProgressBar | 全局 |
|
||||||
|
| QStatusBar | ElaStatusBar 或保留+联动 | main |
|
||||||
|
| QTabWidget/分段 | ElaTabWidget / ElaToggleSwitch | PanelHeader 数据/文件 |
|
||||||
|
| QDialog(登录) | ElaWidget/ElaWindow 风格弹窗 | LoginWindow |
|
||||||
|
| **保留(无替代)** | QVTKOpenGLStereoWidget、ADS CDockManager | 中央/详情/停靠 |
|
||||||
|
|
||||||
|
- 树/列表/表若用 Ela 的 View 需改 model(成本高),可分两步:先保留 widget 版做主题联动,后续再评估换 View。
|
||||||
|
- 登录窗重做为 Fluent 风格(沿用现有令牌与文案)。
|
||||||
|
|
||||||
|
## 5. P4 — 收尾
|
||||||
|
|
||||||
|
- 插件部署正式化、ElaWidgetTools 版本锁定、静态资源核验。
|
||||||
|
- 去掉 `GEOPRO_UI_SHELL` 过渡开关(确认稳定后)。
|
||||||
|
- 开源声明:ElaWidgetTools(MIT)、ADS(LGPLv2.1)、Qt(LGPL) NOTICE 归集。
|
||||||
|
- 回归:登录、项目切换、对象树、数据集/文件分页、异常、VTK 各视图、dock 持久化。
|
||||||
|
|
||||||
|
## 风险登记
|
||||||
|
|
||||||
|
| 风险 | 缓解 |
|
||||||
|
|---|---|
|
||||||
|
| Qt 6.11 Windows Popup 渲染(作者红旗) | spike 已初验;P1 重点复核菜单/下拉/提示;必要时打 fork 的条件补丁 |
|
||||||
|
| ADS 在 ElaWindow 内主题/交互异常 | spike 已验内嵌;P2 专门做 ADS 明暗 QSS |
|
||||||
|
| Ela View 需 model 重写(树/列表/表) | 分步:先 widget 版主题联动,再评估换 View |
|
||||||
|
| 静态库资源剥离(图标/字体缺失) | 改动态库 |
|
||||||
|
| 我无法目视 | 每阶段用户运行+截图验收 |
|
||||||
|
| C: 满导致构建反复失败 | 清理 C: + TEMP→D: 固化进 build.bat |
|
||||||
|
| 大重构回归 | 全程 env 开关可回退;主分支零影响 |
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
P0 → P1(验收)→ P2(验收)→ P3(按面板分批,每批验收)→ P4。每步构建通过后由用户运行+截图确认再进下一步。
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# 设计规范落地 —— 基线与有意偏离记录
|
||||||
|
|
||||||
|
**基线(改动前):** `build.bat app` 通过;`build/release/src/app/geopro_desktop.exe` 现行可执行(ninja: no work to do = 源码与产物同步)。分支 `refactor/pure-qt-ui`。
|
||||||
|
|
||||||
|
## 有意偏离规范的三点(经用户确认的范围裁剪)
|
||||||
|
|
||||||
|
1. **字号**:保留现有 px 字号缩放体系(`Theme.hpp` `type::` 命名空间 + `scaledPx`),不切规范 §2.2 的 pt。理由:用户已投入字号缩放设置,切 pt 会破坏现有缩放与持久化。
|
||||||
|
2. **图标**:保留自有 `Glyphs`(程序绘制矢量、随主题着色),不引入 QtAwesome。理由:已满足规范 §9「矢量 + 可染色 + 随主题」的意图,引入新依赖收益低。
|
||||||
|
3. **本轮不做**:表格 / 对话框 / Toast / Tooltip 富组件、VTK colormap(§8.2)。留待后续独立计划。
|
||||||
|
|
||||||
|
## 构建说明(供实现者)
|
||||||
|
|
||||||
|
- 命令:项目根目录执行 `build.bat app`(MSVC + Ninja,preset `msvc-release`)。
|
||||||
|
- 在 PowerShell 下 `& .\build.bat app` 会打印一行 `vswhere.exe is not recognized` 的 stderr 噪声,但 ninja 仍会运行——以最终的 ninja/cl 输出与 exit code 为准,不要被该行误导。
|
||||||
|
|
@ -0,0 +1,533 @@
|
||||||
|
# Geopro 3.0 视觉设计规范落地 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
> **每个 Task 末尾的「规范一致性校验」步骤是硬性门禁** —— 必须派 subagent 逐值核对改动与 `docs/Geopro3.0_视觉设计规范.md`,PASS 才能进入下一个 Task。
|
||||||
|
|
||||||
|
**Goal:** 把 `docs/Geopro3.0_视觉设计规范.md` 落成代码层的单一事实来源:集中语义令牌(light/dark 双值)→ 模板化 QSS → 同步 QPalette → 数据画布常深 → 关键列表面板卡片化,消除当前「裸 hex 散落 + 暗色靠字符串替换」的脆弱结构。
|
||||||
|
|
||||||
|
**Architecture:** 在 `Theme.cpp` 建一张语义令牌表(`name → {lightHex, darkHex}`,取值严格来自规范 §1.5 + 附录 A + §1.3 画布色)。新增 `token()/tokenColor()/fillTokens()/applyTokenizedStyleSheet()` API:QSS 模板用 `{{token}}` 占位,运行时按当前明暗填充;`QPalette` 同一令牌表构建;主题切换时广播重填。逐步把内联 QSS 调用方迁移到令牌,最后删除遗留的 `kDarkMap` 字符串替换路径。
|
||||||
|
|
||||||
|
**Tech Stack:** C++17 / Qt 6 (QtWidgets) / Fusion QStyle + QSS + QPalette / VTK / ADS。构建:`build.bat app`(MSVC + Ninja,CMake preset `msvc-release`)。
|
||||||
|
|
||||||
|
**Scope(明确边界,YAGNI):**
|
||||||
|
- ✅ 本计划覆盖:令牌基础设施、数据画布常深、全局标准控件重着色到规范色值、内联 QSS 调用方迁移、异常列表 + 数据集列表两个面板卡片化。
|
||||||
|
- ❌ 本计划**不**覆盖(留待后续独立计划):表格/对话框/Toast/Tooltip 富组件、VTK colormap(§8.2)、字号从 px 切 pt(§2.2,当前 px 字号缩放体系刻意保留)、图标改 QtAwesome(当前自有 `Glyphs` 已矢量+随主题,满足 §9 意图,刻意保留)。这三点在 Task 0 记为「有意偏离」。
|
||||||
|
|
||||||
|
**Verify 形态说明(本领域无 QSS 单测,刻意不套 TDD):** 每个 Task 的验证 = (a) `build.bat app` 通过;(b) 规范一致性校验 subagent 返回 PASS;(c) 标注 `[截图检查点]` 的 Task 由用户看真机截图拍板。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| 文件 | 职责 | 本计划改动 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/Theme.hpp` | 主题公共 API + 排版/间距/圆角令牌 | 新增令牌 API 声明 |
|
||||||
|
| `src/app/Theme.cpp` | 令牌表、QSS 模板、QPalette、主题管理器 | 重写核心:令牌表 + 模板化 + palette 从令牌构建;最后删 `kDarkMap` |
|
||||||
|
| `src/app/CentralScene.cpp` | VTK 场景重建(含背景色) | 画布背景改常深(间接,经 `vtkBackground()`) |
|
||||||
|
| `src/app/TopBar.cpp` | 顶栏 chrome 内联 QSS | 迁移到 `{{token}}` |
|
||||||
|
| `src/app/PanelHeader.cpp` | 面板表头内联 QSS | 迁移到 `{{token}}` |
|
||||||
|
| `src/app/panels/ObjectTreePanel.cpp` | 对象树(内联复选框/选中色) | 迁移到令牌 |
|
||||||
|
| `src/app/panels/AnomalyListPanel.cpp` | 异常列表 | 卡片化(自绘 item delegate / 富 item) |
|
||||||
|
| `src/app/panels/DatasetListPanel.cpp` | 数据集列表 | 卡片化(双行 + 选中竖条) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 0: 基线快照 + 偏离记录(无代码改动)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/superpowers/plans/2026-06-10-design-baseline.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 记录当前 git 状态与构建基线**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功(建立「改动前可编译」基线)。若失败,先停下来报告,不要继续。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 写下有意偏离规范的三点**
|
||||||
|
|
||||||
|
在 `docs/superpowers/plans/2026-06-10-design-baseline.md` 写入:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# 设计规范落地 —— 有意偏离记录(经用户确认的范围裁剪)
|
||||||
|
1. 字号:保留现有 px 字号缩放体系(Theme.hpp type:: 命名空间 + scaledPx),不切规范 §2.2 的 pt。理由:用户已投入字号缩放设置,切 pt 会破坏现有缩放。
|
||||||
|
2. 图标:保留自有 Glyphs(程序绘制矢量、随主题着色),不引入 QtAwesome。理由:已满足规范 §9「矢量+可染色+随主题」的意图,引入新依赖收益低。
|
||||||
|
3. 本轮不做:表格/对话框/Toast/Tooltip 富组件、VTK colormap(§8.2)。留待后续计划。
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/superpowers/plans/
|
||||||
|
git commit -m "docs: 设计规范落地计划 + 基线与偏离记录"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: 语义令牌基础设施(additive,无视觉变化)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/Theme.hpp`(新增 API 声明)
|
||||||
|
- Modify: `src/app/Theme.cpp`(新增令牌表 + 实现,暂不替换现有 QSS)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 `Theme.hpp` 声明令牌 API**
|
||||||
|
|
||||||
|
在 `namespace geopro::app {` 内、`applyThemedStyleSheet` 声明附近加入:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens;规范 §1.5 + 附录 A + §1.3)──
|
||||||
|
// 组件只引语义 token,禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。
|
||||||
|
QString token(const char* name); // 当前明暗下的 hex(未知名返回品红 "#FF00FF" 以便一眼发现漏配)
|
||||||
|
QColor tokenColor(const char* name); // 同上,QColor 形式
|
||||||
|
|
||||||
|
// 把 QSS 模板里的 {{token}} 占位替换为当前明暗的 hex 后返回。
|
||||||
|
QString fillTokens(const QString& tmpl);
|
||||||
|
|
||||||
|
// 应用一段 {{token}} 模板 QSS 到 widget,并随主题切换自动重填。
|
||||||
|
// 迁移内联 QSS 调用方的目标接口(取代 applyThemedStyleSheet 的浅色 hex 写法)。
|
||||||
|
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 `Theme.cpp` 匿名 namespace 顶部加入令牌表**
|
||||||
|
|
||||||
|
放在 `kStyleSheet` 之前。**取值逐一对照规范,禁止改动**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ── 语义令牌表(全 UI 唯一颜色来源)。改色只改这一处。 ──────────────────
|
||||||
|
// 取值来源:规范 §1.5 语义映射 + 附录 A 速查 + §1.3 画布专用色。
|
||||||
|
// 画布(canvas/*)与 bg/canvas 两模式同值——规范 §0.5「视图区永远深色」。
|
||||||
|
struct Token { const char* name; const char* light; const char* dark; };
|
||||||
|
const Token kTokens[] = {
|
||||||
|
// 背景
|
||||||
|
{"bg/app", "#F7F8FA", "#0E1116"},
|
||||||
|
{"bg/panel", "#FFFFFF", "#161A20"},
|
||||||
|
{"bg/panel-subtle", "#FCFCFD", "#161B22"},
|
||||||
|
{"bg/header", "#FFFFFF", "#12161C"},
|
||||||
|
{"bg/hover", "#EFF1F4", "#1B2129"},
|
||||||
|
{"bg/selected", "#EFF5FF", "#16243F"},
|
||||||
|
{"bg/canvas", "#0B1320", "#0B1320"},
|
||||||
|
// 边框
|
||||||
|
{"border/default", "#E3E6EB", "#262C35"},
|
||||||
|
{"border/strong", "#CDD2DA", "#333B45"},
|
||||||
|
{"border/focus", "#3B73EC", "#5E8DF5"},
|
||||||
|
// 文字
|
||||||
|
{"text/primary", "#272C35", "#E6E9EF"},
|
||||||
|
{"text/secondary", "#5A626F", "#A4ADBB"},
|
||||||
|
{"text/tertiary", "#7C8493", "#7A8494"},
|
||||||
|
{"text/disabled", "#A8AFBC", "#5A626F"},
|
||||||
|
{"text/link", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"text/on-primary", "#FFFFFF", "#FFFFFF"},
|
||||||
|
// 强调
|
||||||
|
{"accent/primary", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"accent/primary-hover", "#2B5FD9", "#93B4FA"},
|
||||||
|
{"accent/primary-pressed","#2450B8", "#3B73EC"},
|
||||||
|
// 其他
|
||||||
|
{"divider", "#E3E6EB", "#22272F"},
|
||||||
|
{"scrollbar/thumb", "#CDD2DA", "#3A424D"},
|
||||||
|
{"scrollbar/thumb-hover", "#A8AFBC", "#4A535F"},
|
||||||
|
// 状态色(主色 + 浅底)规范 §1.4
|
||||||
|
{"status/danger", "#E5484D", "#FF6166"},
|
||||||
|
{"status/danger-bg", "#FDECEC", "#3A1D1F"},
|
||||||
|
{"status/warning", "#E08A1E", "#F5A623"},
|
||||||
|
{"status/warning-bg", "#FBF0DD", "#3A2C12"},
|
||||||
|
{"status/success", "#2E9E5B", "#46C07A"},
|
||||||
|
{"status/success-bg", "#E7F6ED", "#16301F"},
|
||||||
|
{"status/info", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"status/info-bg", "#EFF5FF", "#16243F"},
|
||||||
|
{"status/neutral", "#7C8493", "#8A93A3"},
|
||||||
|
// 画布专用(两模式同值)规范 §1.3
|
||||||
|
{"canvas/bg", "#0B1320", "#0B1320"},
|
||||||
|
{"canvas/bg-soft", "#111B2D", "#111B2D"},
|
||||||
|
{"canvas/grid", "#1E2A3D", "#1E2A3D"},
|
||||||
|
{"canvas/text", "#E6ECF5", "#E6ECF5"},
|
||||||
|
{"canvas/text-dim", "#8A97AC", "#8A97AC"},
|
||||||
|
};
|
||||||
|
|
||||||
|
QString tokenHex(const char* name, bool dark)
|
||||||
|
{
|
||||||
|
for (const auto& t : kTokens)
|
||||||
|
if (qstrcmp(t.name, name) == 0) return QString::fromLatin1(dark ? t.dark : t.light);
|
||||||
|
return QStringLiteral("#FF00FF"); // 漏配的令牌显眼品红,便于一眼发现
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 `Theme.cpp` 实现公共 API(文件末尾 `namespace geopro::app` 内)**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QString token(const char* name) { return tokenHex(name, isDarkTheme()); }
|
||||||
|
|
||||||
|
QColor tokenColor(const char* name) { return QColor(token(name)); }
|
||||||
|
|
||||||
|
QString fillTokens(const QString& tmpl)
|
||||||
|
{
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
QString s = tmpl;
|
||||||
|
for (const auto& t : kTokens)
|
||||||
|
s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)),
|
||||||
|
QString::fromLatin1(dark ? t.dark : t.light));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl)
|
||||||
|
{
|
||||||
|
if (!w) return;
|
||||||
|
w->setStyleSheet(fillTokens(tmpl));
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
|
||||||
|
[w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:`kTokens`/`tokenHex` 在匿名 namespace;`token/fillTokens` 等在 `geopro::app` 内调用 `tokenHex` 没问题(同 TU)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功,**界面无任何变化**(新 API 尚无调用方)。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent(opus)执行:读取 `docs/Geopro3.0_视觉设计规范.md` 的 §1.1–§1.5 + §1.3 + 附录 A,与 `Theme.cpp` 的 `kTokens` 表**逐 token 逐 hex 比对**。输出 PASS/FAIL 清单:每个 token 标注「规范值 vs 代码值」是否一致;列出规范有但表里缺的、表里有但规范无依据的。FAIL 则修正后重校,PASS 才继续。
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/Theme.hpp src/app/Theme.cpp
|
||||||
|
git commit -m "feat(theme): 语义令牌基础设施(令牌表+token/fillTokens API,规范§1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: 数据画布常深(规范 §0.5 / §5 / §11)[截图检查点]
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/Theme.cpp:612-618`(`vtkBackground` 实现)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 改 `vtkBackground` 返回画布令牌,不随主题**
|
||||||
|
|
||||||
|
把现有实现:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void vtkBackground(double& r, double& g, double& b)
|
||||||
|
{
|
||||||
|
const QColor c = roleColor(isDarkTheme(), "#F4F6FA");
|
||||||
|
r = c.redF();
|
||||||
|
g = c.greenF();
|
||||||
|
b = c.blueF();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void vtkBackground(double& r, double& g, double& b)
|
||||||
|
{
|
||||||
|
// 规范 §0.5/§11:数据画布永远深色,不随明暗切换。取 canvas/bg。
|
||||||
|
const QColor c = tokenColor("canvas/bg"); // #0B1320
|
||||||
|
r = c.redF();
|
||||||
|
g = c.greenF();
|
||||||
|
b = c.blueF();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 更新 `CentralScene.cpp:23` 的注释(避免注释腐烂)**
|
||||||
|
|
||||||
|
把 `// 背景随主题(取 ElaTheme 窗口底色),暗色下不再是刺眼白底。`
|
||||||
|
改为 `// 背景永远深色(规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。`
|
||||||
|
|
||||||
|
- [ ] **Step 3: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent:确认 (a) `vtkBackground` 返回 `canvas/bg` = `#0B1320`;(b) 两种主题模式下该函数返回值相同(不分支 `isDarkTheme()` 影响结果);(c) 符合规范 §11「VTK 渲染器背景在两模式下均深色,不参与切换」。PASS/FAIL。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 用户截图检查点**
|
||||||
|
|
||||||
|
请用户运行 `build.bat run`,截图浅色 + 深色两模式下中间视图,确认画布均为深蓝黑 `#0B1320`、与外围 UI 衔接自然。用户确认后继续。
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/Theme.cpp src/app/CentralScene.cpp
|
||||||
|
git commit -m "feat(canvas): 数据画布常深 #0B1320(规范§0.5/§11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 全局标准控件重着色到规范色值(模板化 QSS + palette 从令牌)[截图检查点]
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/Theme.cpp`(重写 `kStyleSheet` 为 `{{token}}` 模板、`buildPalette` 从令牌、`styleSheetForMode` 走 `fillTokens`)
|
||||||
|
|
||||||
|
**说明:** 这是观感变化最大的一步(强调蓝 `#2D6CB5`→`#3B73EC`,中性灰换规范阶)。遗留的 `kDarkMap`/`themedQss`/`applyThemedStyleSheet` 暂保留,供尚未迁移的内联调用方(TopBar/面板)在本步后仍可用;Task 4–5 迁移完后于 Task 5 删除。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 把 `kStyleSheet` 内所有裸 hex 换成 `{{token}}` 占位**
|
||||||
|
|
||||||
|
逐段替换(映射规则固定,便于校验):
|
||||||
|
|
||||||
|
| 原裸 hex | 语义 → 占位 |
|
||||||
|
|---|---|
|
||||||
|
| `#F4F6FA`(QMainWindow/QDialog 底、ADS 区底) | `{{bg/app}}` |
|
||||||
|
| `#FFFFFF`(面板/树/列表/工具条/状态栏/菜单底) | `{{bg/panel}}` |
|
||||||
|
| `#1F2A3D`(主文字) | `{{text/primary}}` |
|
||||||
|
| `#5A6B85`(次文字) | `{{text/secondary}}` |
|
||||||
|
| `#3A475C`(表头/分组标题文字) | `{{text/secondary}}` |
|
||||||
|
| `#9AA6B6` / `#8A93A3`(禁用/占位文字) | `{{text/disabled}}` |
|
||||||
|
| `#EDF1F7`(表头/抬升/ADS 标题底) | `{{bg/hover}}` |
|
||||||
|
| `#EFF1F4`/`#EEF3FB`/`#EAF1FB`/`#EEF2FB`(hover 底) | `{{bg/hover}}` |
|
||||||
|
| `#DCE9F8`/`#DCE6F4`(选中/按下底) | `{{bg/selected}}` |
|
||||||
|
| `#1B3D67`(选中文字) | `{{accent/primary-pressed}}`(深蓝,深底对比)|
|
||||||
|
| `#E3E6EB`/`#D5DBE5`/`#EAEEF4`/`#E1E6EE`/`#EEF1F5`/`#E6EBF3`/`#E6EAF1`(分隔/边框/轨道) | `{{divider}}` 或 `{{border/default}}`(边框用 border/default,分隔线/轨道用 divider)|
|
||||||
|
| `#C2CCDA`/`#C7D2E0`(输入/按钮强边框、滚动条 thumb) | 边框→`{{border/strong}}`;滚动条 thumb→`{{scrollbar/thumb}}` |
|
||||||
|
| `#A7B4C7`(滚动条 hover thumb) | `{{scrollbar/thumb-hover}}` |
|
||||||
|
| `#2D6CB5`(强调) | `{{accent/primary}}` |
|
||||||
|
| `#2862A6`(强调 hover) | `{{accent/primary-hover}}` |
|
||||||
|
| `#234F87`(强调 pressed) | `{{accent/primary-pressed}}` |
|
||||||
|
| `#F0F2F6`/`#F0F1F4`(禁用底) | `{{bg/app}}` |
|
||||||
|
|
||||||
|
> QToolTip 段维持「不写 QSS」现状(用原生)。`QSplitter::handle:hover`/ADS splitter hover 用 `{{accent/primary}}`(规范 §4.2「splitter hover 显示 accent/primary」)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: `styleSheetForMode` 改走 `fillTokens`**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QString styleSheetForMode(bool /*dark*/)
|
||||||
|
{
|
||||||
|
return fillTokens(QString::fromUtf8(kStyleSheet));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(`fillTokens` 内部已按 `isDarkTheme()` 取值;保留参数签名以免动调用方,但实现以当前模式为准。若调用点传入与当前模式不一致的 dark,需在 Step 3 校验中确认 `applyThemeMode` 调用时 `ThemeManager` 状态已就绪。)
|
||||||
|
|
||||||
|
> ⚠️ 依赖检查:`applyThemeMode(app, dark)` 在 `main` 中调用时机须保证 `ThemeManager::instance()` 的 `dark_` 已与传入 `dark` 一致。若不一致,改为让 `applyThemeMode` 内 `setStyleSheet(fillTokens(...))` 前不依赖参数、统一以 `ThemeManager` 为准;并在本步明确 `applyThemeMode` 的 `dark` 参数仅用于 palette。**实现者须先 grep `applyThemeMode(` 的所有调用点确认。**
|
||||||
|
|
||||||
|
- [ ] **Step 3: `buildPalette` 从令牌构建**
|
||||||
|
|
||||||
|
把 `buildPalette` 内的 `roleColor(dark, "#xxxxxx")` 调用改为 `QColor(tokenHex(name, dark))`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QPalette buildPalette(bool dark)
|
||||||
|
{
|
||||||
|
QPalette p;
|
||||||
|
const QColor shell = QColor(tokenHex("bg/app", dark));
|
||||||
|
const QColor panel = QColor(tokenHex("bg/panel", dark));
|
||||||
|
const QColor text = QColor(tokenHex("text/primary", dark));
|
||||||
|
const QColor muted = QColor(tokenHex("text/secondary", dark));
|
||||||
|
const QColor accent = QColor(tokenHex("accent/primary", dark));
|
||||||
|
const QColor border = QColor(tokenHex("border/default", dark));
|
||||||
|
const QColor disabled = QColor(tokenHex("text/disabled", dark));
|
||||||
|
const QColor hoverBg = QColor(tokenHex("bg/hover", dark));
|
||||||
|
|
||||||
|
p.setColor(QPalette::Window, shell);
|
||||||
|
p.setColor(QPalette::WindowText, text);
|
||||||
|
p.setColor(QPalette::Base, panel);
|
||||||
|
p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark)));
|
||||||
|
p.setColor(QPalette::Text, text);
|
||||||
|
p.setColor(QPalette::Button, hoverBg);
|
||||||
|
p.setColor(QPalette::ButtonText, text);
|
||||||
|
p.setColor(QPalette::ToolTipBase, text);
|
||||||
|
p.setColor(QPalette::ToolTipText, panel);
|
||||||
|
p.setColor(QPalette::Highlight, accent);
|
||||||
|
p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark)));
|
||||||
|
p.setColor(QPalette::PlaceholderText, muted);
|
||||||
|
p.setColor(QPalette::Link, accent);
|
||||||
|
p.setColor(QPalette::Light, panel);
|
||||||
|
p.setColor(QPalette::Midlight, border);
|
||||||
|
p.setColor(QPalette::Mid, border);
|
||||||
|
p.setColor(QPalette::Dark, border);
|
||||||
|
p.setColor(QPalette::Shadow, border);
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent:(a) 确认 `kStyleSheet` 内**不再有任何裸 `#` hex**(QToolTip 注释除外),全部为 `{{token}}`;(b) 抽查每条 `{{token}}` 的语义角色与规范 §3/§6/§7 控件描述一致(如选中行 = `bg/selected`、splitter hover = `accent/primary`);(c) `buildPalette` 各 QPalette 角色取的令牌合理。输出 PASS/FAIL。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 用户截图检查点**
|
||||||
|
|
||||||
|
请用户 `build.bat run`,截图浅/深两模式工作台全貌。重点确认强调蓝、中性灰、选中/hover、边框层级符合规范气质。**色板方向由用户拍板**(这是审美决策)。用户确认后继续。
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/Theme.cpp
|
||||||
|
git commit -m "feat(theme): 全局 QSS 模板化 + palette 从令牌,标准控件对齐规范色值(§1/§3/§6/§7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 顶栏 + 面板表头内联 QSS 迁移到令牌
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/TopBar.cpp:135-154`
|
||||||
|
- Modify: `src/app/PanelHeader.cpp`(全文件内联 QSS)
|
||||||
|
|
||||||
|
- [ ] **Step 1: TopBar 内联 QSS 改 `{{token}}` + `applyTokenizedStyleSheet`**
|
||||||
|
|
||||||
|
把 `applyThemedStyleSheet(this, QStringLiteral("...裸 hex..."))` 改为 `applyTokenizedStyleSheet(this, QStringLiteral("...{{token}}..."))`,裸 hex 按 Task 3 同一映射替换:
|
||||||
|
- `#FFFFFF`→`{{bg/header}}`(顶栏用 header 底)、`#E1E6EE`→`{{divider}}`、`#1F2A3D`→`{{text/primary}}`、`#EEF3FB`→`{{bg/hover}}`、`#2D6CB5`→`{{accent/primary}}`、`#8A93A3`→`{{text/tertiary}}`。
|
||||||
|
- `#avatar` 背景用 `{{accent/primary}}`,`color:white` 保持字面 `white`(头像白字恒白,规范 §5 主按钮 on-primary)。
|
||||||
|
- 字号 `.arg(scaledPx(...))` 占位(`%1`…`%6`)保持不变——令牌只替换颜色,不碰字号。
|
||||||
|
|
||||||
|
> 注意:`{{token}}` 与 `.arg()` 的 `%N` 共存无冲突(`fillTokens` 只替换 `{{...}}`,`.arg` 只替换 `%N`)。先 `.arg()` 拼好字符串,再交给 `applyTokenizedStyleSheet` 填 `{{}}`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: PanelHeader 同法迁移**
|
||||||
|
|
||||||
|
读 `PanelHeader.cpp` 全文,将其内联 QSS 的裸 hex 按 Task 3 映射改为 `{{token}}`,调用改 `applyTokenizedStyleSheet`。表头底用 `{{bg/panel}}`、底部分隔 `{{divider}}`、标题文字 `{{text/primary}}`、图标按钮 hover `{{bg/hover}}`(规范 §4.3 面板标题栏:背景 bg/panel + 底部 1px divider)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功,顶栏/表头外观与 Task 3 后的全局风格一致(同一套令牌)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent:确认 TopBar.cpp / PanelHeader.cpp 内联 QSS **无裸 hex**(`white` 关键字允许,记为「on-primary 恒白」);表头结构符合规范 §4.3 / §5。PASS/FAIL。
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/TopBar.cpp src/app/PanelHeader.cpp
|
||||||
|
git commit -m "refactor(theme): TopBar/PanelHeader 内联 QSS 迁移到语义令牌(§4.3/§5)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: 对象树面板迁移 + 删除遗留 kDarkMap 路径
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/ObjectTreePanel.cpp:45-71`
|
||||||
|
- Modify: `src/app/Theme.cpp`(删除 `kDarkMap`/`darkOf`/`roleColor`/`themedQss`/`themed`/`applyThemedStyleSheet`/`themed` 声明与实现——**仅当全仓无调用方时**)
|
||||||
|
|
||||||
|
- [ ] **Step 1: ObjectTreePanel 复选框/选中色改令牌**
|
||||||
|
|
||||||
|
把 `applyCheckboxStyle` lambda 内的硬编码 `QColor(0x..)` 与 `selBg/selFg` 字面 hex 改为 `tokenColor(...)`:
|
||||||
|
- `border` → `tokenColor("border/strong")`
|
||||||
|
- `boxBg` → `tokenColor("bg/panel")`
|
||||||
|
- `accent` → `tokenColor("accent/primary")`
|
||||||
|
- `selBg` → `token("bg/selected")`
|
||||||
|
- `selFg` → `token("accent/primary-pressed")`(深底对比的深蓝字)
|
||||||
|
|
||||||
|
`hint_` 的 `color:#9AA6B6` → `applyTokenizedStyleSheet(hint_, "color:{{text/disabled}}; padding:16px;")`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: grep 确认遗留路径已无调用方**
|
||||||
|
|
||||||
|
Run: `grep -rn "applyThemedStyleSheet\|themedQss\|\bthemed(\|roleColor\|kDarkMap\|darkOf" src/`
|
||||||
|
Expected: 仅 `Theme.cpp` 定义处命中,无其他调用方。**若仍有调用方(如 DatasetListPanel/AnomalyListPanel 尚在 Task 6 前用到),跳过 Step 3,把删除挪到 Task 6 末尾。**
|
||||||
|
|
||||||
|
- [ ] **Step 3: 删除遗留 kDarkMap 字符串替换路径(仅当 Step 2 确认无调用方)**
|
||||||
|
|
||||||
|
从 `Theme.cpp` 删除:`struct DarkPair` + `kDarkMap[]` + `darkOf` + `roleColor` + `themedQss` + `themed` + `applyThemedStyleSheet`;从 `Theme.hpp` 删除 `themed` / `applyThemedStyleSheet` 声明。`styleSheetForMode` 已在 Task 3 改走 `fillTokens`,不依赖它们。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent:(a) ObjectTreePanel 无裸 hex;(b) 若执行了 Step 3,确认 `Theme.cpp` 已无字符串替换 dark 路径、暗色完全由 `kTokens` 双值驱动(规范 §13.1「集中 token」)。PASS/FAIL。
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/ObjectTreePanel.cpp src/app/Theme.hpp src/app/Theme.cpp
|
||||||
|
git commit -m "refactor(theme): 对象树迁移令牌 + 移除遗留 kDarkMap 字符串替换路径(§13.1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: 异常列表 + 数据集列表卡片化(规范 §6.2 / §6.3)[截图检查点]
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/AnomalyListPanel.cpp`(异常卡片:左状态竖条 + 圆点 + 名称 + 等级胶囊 + 属性行 + 显隐眼睛)
|
||||||
|
- Modify: `src/app/panels/DatasetListPanel.cpp`(双行列表项 + 状态圆点 + 选中竖条)
|
||||||
|
|
||||||
|
**实现取向:** 用 `QStyledItemDelegate` 自绘(保留 `QListWidget` 数据模型),颜色全取 `tokenColor(...)`。避免 `\n` 拼字符串那种朴素渲染。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 异常列表 —— 定义等级→状态令牌映射**
|
||||||
|
|
||||||
|
在 `AnomalyListPanel.cpp` 匿名 namespace 加:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 规范 §1.4:异常分级 高=Danger 中=Warning 低=Info,停用=Neutral。
|
||||||
|
// 返回 {主色 token, 浅底 token}。
|
||||||
|
struct LevelTokens { const char* main; const char* bg; };
|
||||||
|
LevelTokens levelTokens(int level) // 0=高 1=中 2=低 其他=停用
|
||||||
|
{
|
||||||
|
switch (level) {
|
||||||
|
case 0: return {"status/danger", "status/danger-bg"};
|
||||||
|
case 1: return {"status/warning", "status/warning-bg"};
|
||||||
|
case 2: return {"status/info", "status/info-bg"};
|
||||||
|
default: return {"status/neutral", "bg/hover"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 实现者须先确认 `geopro::core::Anomaly` 是否已有「等级」字段;若无,本步先用现有 `lineColor` 决定竖条色、等级胶囊暂以「—」占位,并在 baseline 文档记一条 TODO(不阻塞卡片结构)。**先读 `AnomalyListPanel.hpp` + Anomaly 定义确认。**
|
||||||
|
|
||||||
|
- [ ] **Step 2: 异常列表 —— 写 `QStyledItemDelegate` 子类自绘卡片**
|
||||||
|
|
||||||
|
卡片规范(§6.3):`radius/md`(8) 圆角、左 3px 状态色竖条、底为状态浅底、名称 `text/primary-strong`、右侧等级胶囊(`radius/pill`、状态浅底 + 状态主色字)、属性行 `text/caption` `text/secondary` 等宽数值、右侧显隐眼睛(开=`text/secondary`,关=`text/disabled`)。`sizeHint` 高度容纳双行(约 56px)。所有颜色 `tokenColor(...)`;主题切换时 `viewport()->update()`。
|
||||||
|
|
||||||
|
> 实现细节(cardpaint):`paint()` 内 `painter->setRenderHint(QPainter::Antialiasing)`;用 `QPainterPath::addRoundedRect` 画卡底;竖条 `fillRect`;胶囊 `drawRoundedRect`;眼睛图标复用 `makeGlyph`(若 `Glyph` 无 eye,先用文字「●/○」占位并记 TODO)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 数据集列表 —— 双行项 delegate**
|
||||||
|
|
||||||
|
规范 §6.2:项高 52px、标题 `text/body` + 前置状态圆点、元信息行 `text/caption` `text/tertiary`(如「2026-03-15 09:21 · 64 道」)、选中项 `bg/selected` + 左 2px 竖条 + `radius/md`。同样 `QStyledItemDelegate` 自绘,颜色 `tokenColor`。
|
||||||
|
|
||||||
|
> 先读 `DatasetListPanel.cpp/.hpp` 确认现有数据字段(标题/日期/通道数来源)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建**
|
||||||
|
|
||||||
|
Run: `build.bat app`
|
||||||
|
Expected: 构建成功。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 若 Task 5 Step 3 当时被跳过,现在删除遗留 kDarkMap 路径**
|
||||||
|
|
||||||
|
重跑 Task 5 的 grep 确认无调用方后,执行 Task 5 Step 3 的删除并构建。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 规范一致性校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent:对照规范 §6.2 / §6.3 / §1.4,核对:竖条宽度(异常 3px / 数据集 2px)、圆角档(md/pill)、状态色映射(高=danger…)、字号角色、显隐态颜色、所有颜色经 `tokenColor` 无裸 hex。PASS/FAIL。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 用户截图检查点**
|
||||||
|
|
||||||
|
用户 `build.bat run`,截图异常列表 + 数据集列表(浅/深)。确认卡片层级、状态色、胶囊、选中竖条符合规范且美观。
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/AnomalyListPanel.cpp src/app/panels/DatasetListPanel.cpp src/app/Theme.hpp src/app/Theme.cpp
|
||||||
|
git commit -m "feat(panels): 异常/数据集列表卡片化,状态色对齐规范(§6.2/§6.3/§1.4)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 收尾:整体一致性复核
|
||||||
|
|
||||||
|
- [ ] **Step 1: 全仓裸 hex 扫描**
|
||||||
|
|
||||||
|
Run: `grep -rn "#[0-9A-Fa-f]\{6\}" src/app/ --include=*.cpp --include=*.hpp`
|
||||||
|
Expected: 仅 `Theme.cpp` 的 `kTokens` 表命中(唯一颜色来源)。其余文件命中即为漏迁移,逐个修。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 终版规范一致性总校验(门禁)**
|
||||||
|
|
||||||
|
派 subagent(opus)做一次全量复核:以 `docs/Geopro3.0_视觉设计规范.md` 为准,遍历本计划范围内的所有改动文件,产出一份「规范条款 → 实现位置 → 一致/偏离」对照表。偏离项须落在 Task 0 记录的三条「有意偏离」内,否则修正。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 用户最终验收截图**(浅/深 × 工作台全貌 + 各面板)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit 复核报告**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/superpowers/plans/
|
||||||
|
git commit -m "docs: 设计规范落地终版一致性复核报告"
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# ElaWidgetTools 评估 spike(隔离 demo,独立 exe)。仅 feat/elawidgettools 分支评估用,
|
||||||
|
# 与产品 geopro_desktop 完全解耦:链 ElaWidgetTools(Fluent 库) + ADS + VTK,验证可行性与观感。
|
||||||
|
add_executable(geopro_ela_spike WIN32 main.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(geopro_ela_spike PRIVATE
|
||||||
|
Qt6::Core Qt6::Gui Qt6::Widgets
|
||||||
|
ElaWidgetTools
|
||||||
|
ads::qt6advanceddocking
|
||||||
|
${VTK_LIBRARIES})
|
||||||
|
|
||||||
|
vtk_module_autoinit(TARGETS geopro_ela_spike MODULES ${VTK_LIBRARIES})
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
add_custom_command(TARGET geopro_ela_spike POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_RUNTIME_DLLS:geopro_ela_spike> $<TARGET_FILE_DIR:geopro_ela_spike>
|
||||||
|
COMMAND_EXPAND_LISTS)
|
||||||
|
endif()
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
// ElaWidgetTools 评估 spike(隔离 demo,不属于产品 app,仅 feat/elawidgettools 分支评估用)。
|
||||||
|
// 一锤定音验证四件事:
|
||||||
|
// ① 用你们官方 Qt 6.11.1 + MSVC 经 FetchContent 能否构建 ElaWidgetTools(RainbowCandyX fork);
|
||||||
|
// ② ElaWindow 的 Fluent 观感在你们机器上渲染是否正常(重点看 Qt6.11 的 Popup/弹窗);
|
||||||
|
// ③ Qt Advanced Docking System(ADS) 能否内嵌进 ElaWindow;
|
||||||
|
// ④ QVTKOpenGLStereoWidget 视口在 ElaWindow + ADS 内能否正常渲染。
|
||||||
|
// 结论决定是否值得对真实 app 做外壳重构。
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSurfaceFormat>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "ElaApplication.h"
|
||||||
|
#include "ElaDef.h"
|
||||||
|
#include "ElaPushButton.h"
|
||||||
|
#include "ElaText.h"
|
||||||
|
#include "ElaTheme.h"
|
||||||
|
#include "ElaWindow.h"
|
||||||
|
|
||||||
|
#include <DockManager.h>
|
||||||
|
#include <DockWidget.h>
|
||||||
|
|
||||||
|
#include <QVTKOpenGLStereoWidget.h>
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkConeSource.h>
|
||||||
|
#include <vtkGenericOpenGLRenderWindow.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
|
#include <vtkProperty.h>
|
||||||
|
#include <vtkRenderer.h>
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||||
|
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||||||
|
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
eApp->init(); // ElaApplication 初始化(Fluent 主题/字体/动画基建)
|
||||||
|
|
||||||
|
ElaWindow window;
|
||||||
|
window.setWindowTitle(QStringLiteral("ElaWidgetTools Spike — Fluent + ADS + VTK"));
|
||||||
|
window.resize(1200, 760);
|
||||||
|
|
||||||
|
// 「工作台」页内嵌 ADS 停靠管理器:验证 ADS 能否在 ElaWindow 内正常工作。
|
||||||
|
auto* dockHost = new QWidget;
|
||||||
|
auto* hostLay = new QVBoxLayout(dockHost);
|
||||||
|
hostLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
auto* dockManager = new ads::CDockManager(dockHost);
|
||||||
|
hostLay->addWidget(dockManager);
|
||||||
|
|
||||||
|
// dock1:Fluent 控件样例(看观感:ElaText / ElaPushButton 与标准控件对照)。
|
||||||
|
auto* sample = new QWidget;
|
||||||
|
sample->setObjectName(QStringLiteral("sampleHost"));
|
||||||
|
auto* sLay = new QVBoxLayout(sample);
|
||||||
|
sLay->setContentsMargins(16, 16, 16, 16);
|
||||||
|
sLay->setSpacing(12);
|
||||||
|
sLay->addWidget(new ElaText(QStringLiteral("ElaText —— Fluent 文本"), sample));
|
||||||
|
sLay->addWidget(new ElaPushButton(QStringLiteral("ElaPushButton 主操作"), sample));
|
||||||
|
sLay->addWidget(new QLabel(QStringLiteral("(对照)标准 QLabel"), sample));
|
||||||
|
|
||||||
|
// 浅/深主题切换:ElaWidgetTools 内置 ElaTheme,运行期一键切换整套 Fluent 主题。
|
||||||
|
auto* themeBtn = new ElaPushButton(QStringLiteral("切换 浅色 / 深色"), sample);
|
||||||
|
QObject::connect(themeBtn, &QPushButton::clicked, themeBtn, [] {
|
||||||
|
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark
|
||||||
|
: ElaThemeType::Light);
|
||||||
|
});
|
||||||
|
sLay->addWidget(themeBtn);
|
||||||
|
sLay->addStretch();
|
||||||
|
auto* d1 = new ads::CDockWidget(QStringLiteral("Fluent 控件"));
|
||||||
|
d1->setWidget(sample);
|
||||||
|
dockManager->addDockWidget(ads::LeftDockWidgetArea, d1);
|
||||||
|
|
||||||
|
// dock2:QVTK 视口(蓝色锥体),验证 VTK 在 ElaWindow + ADS 内渲染。
|
||||||
|
auto* vtkWidget = new QVTKOpenGLStereoWidget;
|
||||||
|
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
|
||||||
|
vtkNew<vtkRenderer> renderer;
|
||||||
|
renderer->SetBackground(1.0, 1.0, 1.0);
|
||||||
|
vtkWidget->setRenderWindow(renderWindow);
|
||||||
|
renderWindow->AddRenderer(renderer);
|
||||||
|
vtkNew<vtkConeSource> cone;
|
||||||
|
cone->SetResolution(48);
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputConnection(cone->GetOutputPort());
|
||||||
|
vtkNew<vtkActor> actor;
|
||||||
|
actor->SetMapper(mapper);
|
||||||
|
actor->GetProperty()->SetColor(0.18, 0.42, 0.71); // 品牌蓝 #2D6CB5 近似
|
||||||
|
renderer->AddActor(actor);
|
||||||
|
renderer->ResetCamera();
|
||||||
|
auto* d2 = new ads::CDockWidget(QStringLiteral("VTK 视口(锥体)"));
|
||||||
|
d2->setWidget(vtkWidget);
|
||||||
|
dockManager->addDockWidget(ads::RightDockWidgetArea, d2);
|
||||||
|
|
||||||
|
// 关键发现演示:ADS 停靠区与普通 QWidget 不会自动跟随 ElaTheme(只有 Ela* 控件与
|
||||||
|
// ElaWindow 外壳跟随)。这里手动把「停靠区背景 + 普通容器 + VTK 背景」同步到当前主题,
|
||||||
|
// 并监听 ElaTheme::themeModeChanged。这段「同步」正是真集成时要为每个非 Ela 面板付出的成本。
|
||||||
|
auto* rendererPtr = renderer.Get();
|
||||||
|
auto* rwPtr = renderWindow.Get();
|
||||||
|
auto applyContentTheme = [dockManager, sample, rendererPtr, rwPtr](ElaThemeType::ThemeMode mode) {
|
||||||
|
const bool dark = (mode == ElaThemeType::Dark);
|
||||||
|
const QString bg = dark ? QStringLiteral("#1E1F22") : QStringLiteral("#FFFFFF");
|
||||||
|
const QString fg = dark ? QStringLiteral("#E3E3E3") : QStringLiteral("#1F2A3D");
|
||||||
|
dockManager->setStyleSheet(
|
||||||
|
QStringLiteral("ads--CDockAreaWidget, ads--CDockContainerWidget { background:%1; }")
|
||||||
|
.arg(bg));
|
||||||
|
sample->setStyleSheet(
|
||||||
|
QStringLiteral("#sampleHost { background:%1; } #sampleHost QLabel { color:%2; }")
|
||||||
|
.arg(bg, fg));
|
||||||
|
rendererPtr->SetBackground(dark ? 0.11 : 1.0, dark ? 0.12 : 1.0, dark ? 0.14 : 1.0);
|
||||||
|
rwPtr->Render();
|
||||||
|
};
|
||||||
|
QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager,
|
||||||
|
[applyContentTheme](ElaThemeType::ThemeMode m) { applyContentTheme(m); });
|
||||||
|
applyContentTheme(eTheme->getThemeMode()); // 初始同步当前主题
|
||||||
|
|
||||||
|
window.addPageNode(QStringLiteral("工作台"), dockHost);
|
||||||
|
window.show();
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,8 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/DatasetListPanel.cpp
|
panels/DatasetListPanel.cpp
|
||||||
panels/ObjectTreePanel.cpp
|
panels/ObjectTreePanel.cpp
|
||||||
CentralScene.cpp
|
CentralScene.cpp
|
||||||
ProjectListDialog.cpp)
|
ProjectListDialog.cpp
|
||||||
|
SettingsDialog.cpp)
|
||||||
|
|
||||||
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
# QtKeychain 经 FetchContent 接入,头文件不随 target 传播,显式加源/构建目录(含生成的 export 头)。
|
# QtKeychain 经 FetchContent 接入,头文件不随 target 传播,显式加源/构建目录(含生成的 export 头)。
|
||||||
|
|
@ -51,4 +52,14 @@ if(WIN32)
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
$<TARGET_RUNTIME_DLLS:geopro_desktop> $<TARGET_FILE_DIR:geopro_desktop>
|
$<TARGET_RUNTIME_DLLS:geopro_desktop> $<TARGET_FILE_DIR:geopro_desktop>
|
||||||
COMMAND_EXPAND_LISTS)
|
COMMAND_EXPAND_LISTS)
|
||||||
|
|
||||||
|
# 运行期 Qt 插件部署:platforms 必需;imageformats/iconengines 供 ElaWidgetTools 的 SVG 图标;
|
||||||
|
# styles 备用。从链接的 Qt6::Core 推导 Qt 安装的 plugins 目录,拷到 exe 旁(windeployqt 会被
|
||||||
|
# ADS 的非 Qt DLL 依赖卡住,故改为显式 copy)。
|
||||||
|
foreach(_pl platforms styles imageformats iconengines)
|
||||||
|
add_custom_command(TARGET geopro_desktop POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"$<TARGET_FILE_DIR:Qt6::Core>/../plugins/${_pl}"
|
||||||
|
"$<TARGET_FILE_DIR:geopro_desktop>/${_pl}")
|
||||||
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include "CameraPreset.hpp"
|
#include "CameraPreset.hpp"
|
||||||
#include "Scene.hpp"
|
#include "Scene.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
#include "actors/CurtainActor.hpp"
|
#include "actors/CurtainActor.hpp"
|
||||||
#include "actors/MapLineActor.hpp"
|
#include "actors/MapLineActor.hpp"
|
||||||
#include "geo/GeoLocalFrame.hpp"
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
|
|
@ -18,7 +19,11 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
|
||||||
const geopro::core::GeoLocalFrame& frame, double verticalExaggeration) {
|
const geopro::core::GeoLocalFrame& frame, double verticalExaggeration) {
|
||||||
scene.clear();
|
scene.clear();
|
||||||
const bool is2D = (mode == ViewMode::Map2D);
|
const bool is2D = (mode == ViewMode::Map2D);
|
||||||
renderer->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
|
(void)is2D;
|
||||||
|
// 背景永远深色(规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。
|
||||||
|
double bgR, bgG, bgB;
|
||||||
|
geopro::app::vtkBackground(bgR, bgG, bgB);
|
||||||
|
renderer->SetBackground(bgR, bgG, bgB);
|
||||||
|
|
||||||
for (const auto& s : sections) {
|
for (const auto& s : sections) {
|
||||||
if (is2D) {
|
if (is2D) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QObject>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QSvgRenderer>
|
#include <QSvgRenderer>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -93,7 +99,7 @@ QString svgPathFor(Glyph t)
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
QIcon makeGlyph(Glyph type, const QColor& color, int px)
|
QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight)
|
||||||
{
|
{
|
||||||
const QString svg =
|
const QString svg =
|
||||||
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
|
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
|
||||||
|
|
@ -105,12 +111,13 @@ QIcon makeGlyph(Glyph type, const QColor& color, int px)
|
||||||
|
|
||||||
// 以 3x 超采样渲染再设 devicePixelRatio,保证在任意缩放/DPI 下都清晰。
|
// 以 3x 超采样渲染再设 devicePixelRatio,保证在任意缩放/DPI 下都清晰。
|
||||||
constexpr qreal kSuper = 3.0;
|
constexpr qreal kSuper = 3.0;
|
||||||
const int dim = qRound(px * kSuper);
|
const int dim = qRound(px * kSuper); // 图标本体边长(方形)
|
||||||
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
|
const int dimW = qRound((px + padRight) * kSuper); // 含右透明内边距的画布宽
|
||||||
|
QImage img(dimW, dim, QImage::Format_ARGB32_Premultiplied);
|
||||||
img.fill(Qt::transparent);
|
img.fill(Qt::transparent);
|
||||||
QPainter p(&img);
|
QPainter p(&img);
|
||||||
p.setRenderHint(QPainter::Antialiasing, true);
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
renderer.render(&p, QRectF(0, 0, dim, dim));
|
renderer.render(&p, QRectF(0, 0, dim, dim)); // 图标居左方形渲染,右侧 padRight 留透明
|
||||||
p.end();
|
p.end();
|
||||||
|
|
||||||
QPixmap pm = QPixmap::fromImage(img);
|
QPixmap pm = QPixmap::fromImage(img);
|
||||||
|
|
@ -150,4 +157,74 @@ QString writeChevronIcon(bool open, const QColor& color)
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString writeCheckboxIcon(bool checked, const QColor& border, const QColor& fill, const QColor& check,
|
||||||
|
const QString& tag)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
const double s = px * kScale;
|
||||||
|
const double inset = s * 0.14;
|
||||||
|
const QRectF box(inset, inset, s - 2 * inset, s - 2 * inset);
|
||||||
|
const double r = s * 0.14;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(fill);
|
||||||
|
p.drawRoundedRect(box, r, r);
|
||||||
|
QPen cpen(check, s * 0.12);
|
||||||
|
cpen.setCapStyle(Qt::RoundCap);
|
||||||
|
cpen.setJoinStyle(Qt::RoundJoin);
|
||||||
|
p.setPen(cpen);
|
||||||
|
p.drawLine(QPointF(s * 0.30, s * 0.52), QPointF(s * 0.44, s * 0.66));
|
||||||
|
p.drawLine(QPointF(s * 0.44, s * 0.66), QPointF(s * 0.70, s * 0.34));
|
||||||
|
} else {
|
||||||
|
QPen bpen(border, s * 0.085);
|
||||||
|
p.setPen(bpen);
|
||||||
|
p.setBrush(fill);
|
||||||
|
p.drawRoundedRect(box, r, r);
|
||||||
|
}
|
||||||
|
p.end();
|
||||||
|
|
||||||
|
const QString path = QDir(QDir::tempPath())
|
||||||
|
.filePath(QStringLiteral("geopro_chk_%1_%2.png")
|
||||||
|
.arg(tag, checked ? QStringLiteral("on")
|
||||||
|
: QStringLiteral("off")));
|
||||||
|
pm.save(path, "PNG");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 当前主题下的 chrome 图标色:取主题主文本色(暗=浅、亮=深),保证两种模式都清晰。
|
||||||
|
QColor themedIconColor()
|
||||||
|
{
|
||||||
|
return isDarkTheme() ? QColor(0xE6, 0xE8, 0xEB) : QColor(0x1F, 0x2A, 0x3D); // 主文字明/暗
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void setThemedGlyph(QLabel* label, Glyph type, int px)
|
||||||
|
{
|
||||||
|
if (!label) return;
|
||||||
|
auto apply = [label, type, px]() {
|
||||||
|
label->setPixmap(makeGlyph(type, themedIconColor(), px).pixmap(px, px));
|
||||||
|
};
|
||||||
|
apply();
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, label, [apply]() { apply(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight)
|
||||||
|
{
|
||||||
|
if (!button) return;
|
||||||
|
auto apply = [button, type, px, padRight]() {
|
||||||
|
button->setIcon(makeGlyph(type, themedIconColor(), px, padRight));
|
||||||
|
if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推
|
||||||
|
};
|
||||||
|
apply();
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QAbstractButton;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
enum class Glyph {
|
enum class Glyph {
|
||||||
|
|
@ -31,11 +34,27 @@ enum class Glyph {
|
||||||
Gear, // 设置(齿轮)
|
Gear, // 设置(齿轮)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 2x 绘制保证清晰)。
|
// 「图标+文字」按钮的图标→文字间距补丁:Fusion 内置约 4px,本值补到规范 §6.7 的 6px。
|
||||||
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16);
|
// 做法:把图标渲染进“px 宽图标 + padRight 透明右边距”的画布,文字被这段透明区右推。
|
||||||
|
inline constexpr int kGlyphTextGapPad = 2;
|
||||||
|
|
||||||
|
// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 3x 绘制保证清晰)。
|
||||||
|
// padRight>0 时图标画布右侧留透明内边距(用于「图标+文字」按钮统一间距),图标本体仍为 px×px 居左。
|
||||||
|
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16, int padRight = 0);
|
||||||
|
|
||||||
|
// 随主题明暗自动着色的 glyph(取主题文本色:暗色用浅色、亮色用深色),主题切换时自动重绘。
|
||||||
|
// 用于面板表头/页签等 chrome 图标,避免固定色在暗色下看不清。
|
||||||
|
// 按钮版的 padRight:见 kGlyphTextGapPad;>0 时本函数同时把 iconSize 设为 (px+padRight)×px。
|
||||||
|
void setThemedGlyph(QLabel* label, Glyph type, int px);
|
||||||
|
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight = 0);
|
||||||
|
|
||||||
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
|
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
|
||||||
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。
|
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。
|
||||||
QString writeChevronIcon(bool open, const QColor& color);
|
QString writeChevronIcon(bool open, const QColor& color);
|
||||||
|
|
||||||
|
// 生成清晰复选框 PNG(供 QTreeView::indicator 的 QSS `image:url(...)`):
|
||||||
|
// 未选=明显边框空心方框;选中=填充色方框 + 白色对勾。明暗各传一套色,tag 区分文件名。
|
||||||
|
QString writeCheckboxIcon(bool checked, const QColor& border, const QColor& fill, const QColor& check,
|
||||||
|
const QString& tag);
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "PanelHeader.hpp"
|
#include "PanelHeader.hpp"
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
|
@ -20,19 +22,33 @@ constexpr int kTitleIcon = 20; // 表头标题图标
|
||||||
constexpr int kActionIcon = 19; // 表头操作按钮图标
|
constexpr int kActionIcon = 19; // 表头操作按钮图标
|
||||||
constexpr int kTabIcon = 19; // Tab 图标
|
constexpr int kTabIcon = 19; // Tab 图标
|
||||||
|
|
||||||
// 表头统一样式(标准表头 + Tab 表头共用)。
|
// 表头统一样式(标准表头 + Tab 表头共用)。字号/字重引用 Theme 排版令牌:
|
||||||
const char* kHeaderQss =
|
// 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。
|
||||||
"#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }"
|
// #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色),
|
||||||
"#panelTitle { color:#1F2A3D; font-size:14px; font-weight:600; }"
|
// 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。
|
||||||
"#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;"
|
// 表头底/标题/徽标 + 页签样式。页签选中态 = 强调色文字 + 2px 强调色下划线,
|
||||||
" padding:1px 7px; font-size:12px; font-weight:600; }"
|
// 与视图/详情工具条完全一致(全 UI 切换控件统一这一套)。操作按钮(ElaIconButton)自绘 Fluent。
|
||||||
|
QString headerQss()
|
||||||
|
{
|
||||||
|
return QStringLiteral(
|
||||||
|
"#panelHeader { background:{{bg/panel}}; border-bottom:1px solid {{divider}}; }"
|
||||||
|
"#panelTitle { color:{{text/primary}}; font-size:%1px; font-weight:%3; }"
|
||||||
|
"#panelBadge { background:{{bg/hover}}; color:{{text/secondary}}; border-radius:9px;"
|
||||||
|
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
|
||||||
|
"#panelBadgeWarn { background:{{status/warning-bg}}; color:{{status/warning}}; border-radius:9px;"
|
||||||
|
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
|
||||||
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
|
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
|
||||||
"QToolButton#panelAction:hover { background:#EEF3FB; }"
|
"QToolButton#panelAction:hover { background:{{bg/hover}}; }"
|
||||||
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
|
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:{{text/secondary}};"
|
||||||
" padding:8px 4px; font-size:14px; }"
|
" padding:8px 6px; font-size:%4px; }"
|
||||||
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
|
"QToolButton#tabBtn:hover { color:{{text/primary}}; }"
|
||||||
"QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:600;"
|
"QToolButton#tabBtn:checked { color:{{accent/primary}}; font-weight:%3;"
|
||||||
" border-bottom:2px solid #2D6CB5; }";
|
" border-bottom:2px solid {{accent/primary}}; }")
|
||||||
|
.arg(scaledPx(type::kTitle)) // %1 标题字号
|
||||||
|
.arg(scaledPx(type::kCaption)) // %2 徽标字号
|
||||||
|
.arg(type::kWeightSemibold) // %3 字重(多处)
|
||||||
|
.arg(scaledPx(type::kBody)); // %4 页签字号
|
||||||
|
}
|
||||||
|
|
||||||
// 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。
|
// 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。
|
||||||
QLabel* makeBadge(QWidget* parent)
|
QLabel* makeBadge(QWidget* parent)
|
||||||
|
|
@ -45,12 +61,12 @@ QLabel* makeBadge(QWidget* parent)
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表头操作按钮(静态占位)。
|
// 表头操作按钮(QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #panelAction QSS 给)。
|
||||||
QToolButton* makeActionButton(QWidget* parent, const HeaderAction& a)
|
QWidget* makeActionButton(QWidget* parent, const HeaderAction& a)
|
||||||
{
|
{
|
||||||
auto* btn = new QToolButton(parent);
|
auto* btn = new QToolButton(parent);
|
||||||
btn->setObjectName(QStringLiteral("panelAction"));
|
btn->setObjectName(QStringLiteral("panelAction"));
|
||||||
btn->setIcon(makeGlyph(a.first, QColor("#5A6B85"), kActionIcon));
|
setThemedGlyph(btn, a.first, kActionIcon);
|
||||||
btn->setIconSize(QSize(kActionIcon, kActionIcon));
|
btn->setIconSize(QSize(kActionIcon, kActionIcon));
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
btn->setToolTip(a.second + QStringLiteral("(占位)"));
|
btn->setToolTip(a.second + QStringLiteral("(占位)"));
|
||||||
|
|
@ -65,14 +81,14 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<Header
|
||||||
auto* header = new QWidget();
|
auto* header = new QWidget();
|
||||||
header->setObjectName(QStringLiteral("panelHeader"));
|
header->setObjectName(QStringLiteral("panelHeader"));
|
||||||
header->setFixedHeight(kHeaderHeight);
|
header->setFixedHeight(kHeaderHeight);
|
||||||
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
|
geopro::app::applyTokenizedStyleSheet(header, headerQss());
|
||||||
|
|
||||||
auto* lay = new QHBoxLayout(header);
|
auto* lay = new QHBoxLayout(header);
|
||||||
lay->setContentsMargins(12, 0, 8, 0);
|
lay->setContentsMargins(12, 0, 8, 0);
|
||||||
lay->setSpacing(8);
|
lay->setSpacing(geopro::app::space::kSm);
|
||||||
|
|
||||||
auto* iconLbl = new QLabel(header);
|
auto* iconLbl = new QLabel(header);
|
||||||
iconLbl->setPixmap(makeGlyph(icon, QColor("#44546B"), kTitleIcon).pixmap(kTitleIcon, kTitleIcon));
|
setThemedGlyph(iconLbl, icon, kTitleIcon); // 随主题着色(暗色下也清晰)
|
||||||
lay->addWidget(iconLbl);
|
lay->addWidget(iconLbl);
|
||||||
|
|
||||||
auto* titleLbl = new QLabel(title, header);
|
auto* titleLbl = new QLabel(title, header);
|
||||||
|
|
@ -98,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
|
||||||
auto* header = new QWidget(box);
|
auto* header = new QWidget(box);
|
||||||
header->setObjectName(QStringLiteral("panelHeader"));
|
header->setObjectName(QStringLiteral("panelHeader"));
|
||||||
header->setFixedHeight(kHeaderHeight);
|
header->setFixedHeight(kHeaderHeight);
|
||||||
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
|
geopro::app::applyTokenizedStyleSheet(header, headerQss());
|
||||||
auto* hlay = new QHBoxLayout(header);
|
auto* hlay = new QHBoxLayout(header);
|
||||||
hlay->setContentsMargins(10, 0, 8, 0);
|
hlay->setContentsMargins(10, 0, 8, 0);
|
||||||
hlay->setSpacing(2);
|
hlay->setSpacing(2);
|
||||||
|
|
@ -112,15 +128,13 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
|
||||||
|
|
||||||
for (int i = 0; i < tabs.size(); ++i) {
|
for (int i = 0; i < tabs.size(); ++i) {
|
||||||
const PanelTab& t = tabs[i];
|
const PanelTab& t = tabs[i];
|
||||||
auto* btn = new QToolButton(header);
|
auto* btn = new QToolButton(header); // 页签与工具条统一: QToolButton + 强调色下划线 QSS
|
||||||
btn->setObjectName(QStringLiteral("tabBtn"));
|
btn->setObjectName(QStringLiteral("tabBtn"));
|
||||||
btn->setText(t.title);
|
btn->setText(t.title);
|
||||||
btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), kTabIcon));
|
setThemedGlyph(btn, t.icon, kTabIcon, kGlyphTextGapPad); // 随主题着色 + 图标→文字6px(§6.7)
|
||||||
btn->setIconSize(QSize(kTabIcon, kTabIcon));
|
|
||||||
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
btn->setAutoRaise(true);
|
|
||||||
group->addButton(btn, i);
|
group->addButton(btn, i);
|
||||||
hlay->addWidget(btn);
|
hlay->addWidget(btn);
|
||||||
|
|
||||||
|
|
@ -148,4 +162,41 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SegmentedHeader buildSegmentedHeader(const QVector<QString>& segments,
|
||||||
|
const QVector<HeaderAction>& actions)
|
||||||
|
{
|
||||||
|
auto* header = new QWidget();
|
||||||
|
header->setObjectName(QStringLiteral("panelHeader"));
|
||||||
|
header->setFixedHeight(kHeaderHeight);
|
||||||
|
geopro::app::applyTokenizedStyleSheet(header, headerQss());
|
||||||
|
|
||||||
|
auto* hlay = new QHBoxLayout(header);
|
||||||
|
hlay->setContentsMargins(10, 0, 8, 0);
|
||||||
|
hlay->setSpacing(2);
|
||||||
|
|
||||||
|
auto* group = new QButtonGroup(header);
|
||||||
|
group->setExclusive(true);
|
||||||
|
|
||||||
|
SegmentedHeader result;
|
||||||
|
result.header = header;
|
||||||
|
|
||||||
|
for (int i = 0; i < segments.size(); ++i) {
|
||||||
|
auto* btn = new QToolButton(header); // 与异常/属性页签统一: tabBtn 样式 + 强调色下划线
|
||||||
|
btn->setObjectName(QStringLiteral("tabBtn"));
|
||||||
|
btn->setText(segments[i]);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
group->addButton(btn, i);
|
||||||
|
hlay->addWidget(btn);
|
||||||
|
hlay->addSpacing(10);
|
||||||
|
result.buttons.append(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
hlay->addStretch();
|
||||||
|
for (const auto& a : actions) hlay->addWidget(makeActionButton(header, a));
|
||||||
|
|
||||||
|
if (!result.buttons.isEmpty()) result.buttons[0]->setChecked(true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QToolButton;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -43,4 +44,16 @@ struct TabbedPanel {
|
||||||
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs,
|
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs,
|
||||||
const QVector<HeaderAction>& actions = {});
|
const QVector<HeaderAction>& actions = {});
|
||||||
|
|
||||||
|
// 分段切换表头构建结果:表头容器 + 各分段按钮(互斥,首个默认激活,供调用方接 clicked)。
|
||||||
|
struct SegmentedHeader {
|
||||||
|
QWidget* header;
|
||||||
|
QVector<QToolButton*> buttons;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建「分段切换表头」:一行 Tab 风格互斥按钮(与异常/属性页签同款:选中=强调色文字 + 2px
|
||||||
|
// 强调色下划线)+ 右侧操作按钮。表头底/高度/边框与 buildTabbedPanel 完全一致;内容由调用方
|
||||||
|
// 自行 addWidget 到表头下方(不建堆叠,因 2D/3D 共用同一画布部件)。
|
||||||
|
SegmentedHeader buildSegmentedHeader(const QVector<QString>& segments,
|
||||||
|
const QVector<HeaderAction>& actions = {});
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QFont>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -13,6 +14,9 @@
|
||||||
#include <QTableWidgetItem>
|
#include <QTableWidgetItem>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
namespace {
|
namespace {
|
||||||
QString statusText(int s) {
|
QString statusText(int s) {
|
||||||
|
|
@ -22,6 +26,15 @@ QString statusText(int s) {
|
||||||
default: return QString::number(s);
|
default: return QString::number(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。
|
||||||
|
QColor statusColor(int s) {
|
||||||
|
switch (s) {
|
||||||
|
case 1: return tokenColor("text/tertiary"); // 未开始:弱化
|
||||||
|
case 2: return tokenColor("accent/primary"); // 进行中:活动中
|
||||||
|
default: return tokenColor("text/secondary"); // 未知:中性
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent)
|
ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent)
|
||||||
|
|
@ -50,7 +63,7 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa
|
||||||
filter->addStretch();
|
filter->addStretch();
|
||||||
root->addLayout(filter);
|
root->addLayout(filter);
|
||||||
|
|
||||||
table_ = new QTableWidget(this);
|
table_ = new QTableWidget(this); // Ela item 版表格(继承 QTableWidget),直替
|
||||||
table_->setColumnCount(8);
|
table_->setColumnCount(8);
|
||||||
table_->setHorizontalHeaderLabels(QStringList{
|
table_->setHorizontalHeaderLabels(QStringList{
|
||||||
QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"),
|
QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"),
|
||||||
|
|
@ -142,10 +155,18 @@ void ProjectListDialog::query() {
|
||||||
set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1));
|
set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1));
|
||||||
auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name));
|
auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name));
|
||||||
nameItem->setData(Qt::UserRole, QString::fromStdString(p.id));
|
nameItem->setData(Qt::UserRole, QString::fromStdString(p.id));
|
||||||
nameItem->setForeground(QColor("#2D6CB5"));
|
nameItem->setForeground(tokenColor("accent/primary"));
|
||||||
table_->setItem(i, 1, nameItem);
|
table_->setItem(i, 1, nameItem);
|
||||||
set(2, QString::fromStdString(p.code));
|
set(2, QString::fromStdString(p.code));
|
||||||
set(3, statusText(p.status));
|
// 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。
|
||||||
|
auto* statusItem = new QTableWidgetItem(statusText(p.status));
|
||||||
|
statusItem->setForeground(statusColor(p.status));
|
||||||
|
if (p.status == 2) {
|
||||||
|
QFont f = statusItem->font();
|
||||||
|
f.setBold(true);
|
||||||
|
statusItem->setFont(f);
|
||||||
|
}
|
||||||
|
table_->setItem(i, 3, statusItem);
|
||||||
set(4, QString::fromStdString(p.typeName));
|
set(4, QString::fromStdString(p.typeName));
|
||||||
set(5, QString::fromStdString(p.ownerCompany));
|
set(5, QString::fromStdString(p.ownerCompany));
|
||||||
set(6, QString::fromStdString(p.responsiblePerson));
|
set(6, QString::fromStdString(p.responsiblePerson));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
#include "SettingsDialog.hpp"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QTextBrowser>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 「标签 + 控件」一行(标签定宽左对齐,控件右随)。
|
||||||
|
QWidget* makeRow(const QString& label, QWidget* control) {
|
||||||
|
auto* row = new QWidget();
|
||||||
|
auto* lay = new QHBoxLayout(row);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(12);
|
||||||
|
auto* lbl = new QLabel(label, row);
|
||||||
|
lbl->setMinimumWidth(96);
|
||||||
|
lay->addWidget(lbl);
|
||||||
|
lay->addWidget(control, 1);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 区段标题。
|
||||||
|
QLabel* sectionTitle(const QString& text, QWidget* parent) {
|
||||||
|
auto* t = new QLabel(text, parent);
|
||||||
|
t->setStyleSheet(QStringLiteral("font-size:%1px; font-weight:600;")
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kHeading)));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* buildAppearancePage() {
|
||||||
|
auto* page = new QWidget();
|
||||||
|
auto* v = new QVBoxLayout(page);
|
||||||
|
v->setContentsMargins(24, 20, 24, 20);
|
||||||
|
v->setSpacing(16);
|
||||||
|
v->addWidget(sectionTitle(QStringLiteral("外观"), page));
|
||||||
|
|
||||||
|
// 主题:跟随系统 / 浅色 / 深色(热切)。
|
||||||
|
auto* themeCombo = new QComboBox(page);
|
||||||
|
themeCombo->addItem(QStringLiteral("跟随系统"), QStringLiteral("system"));
|
||||||
|
themeCombo->addItem(QStringLiteral("浅色"), QStringLiteral("light"));
|
||||||
|
themeCombo->addItem(QStringLiteral("深色"), QStringLiteral("dark"));
|
||||||
|
const QString curTheme = geopro::app::themeModePreference();
|
||||||
|
themeCombo->setCurrentIndex(themeCombo->findData(curTheme) >= 0 ? themeCombo->findData(curTheme) : 0);
|
||||||
|
QObject::connect(themeCombo, &QComboBox::activated, page, [themeCombo](int) {
|
||||||
|
geopro::app::setThemeModePreference(themeCombo->currentData().toString());
|
||||||
|
});
|
||||||
|
v->addWidget(makeRow(QStringLiteral("主题"), themeCombo));
|
||||||
|
|
||||||
|
// 界面字号:小/标准/大/特大(重启生效)。
|
||||||
|
auto* fontCombo = new QComboBox(page);
|
||||||
|
fontCombo->addItem(QStringLiteral("小"), 90);
|
||||||
|
fontCombo->addItem(QStringLiteral("标准"), 100);
|
||||||
|
fontCombo->addItem(QStringLiteral("大"), 115);
|
||||||
|
fontCombo->addItem(QStringLiteral("特大"), 130);
|
||||||
|
const int curScale = geopro::app::fontScalePreference();
|
||||||
|
fontCombo->setCurrentIndex(fontCombo->findData(curScale) >= 0 ? fontCombo->findData(curScale) : 1);
|
||||||
|
v->addWidget(makeRow(QStringLiteral("界面字号"), fontCombo));
|
||||||
|
|
||||||
|
// 字号改动:持久化 + 提示重启(提供立即重启)。
|
||||||
|
auto* restartRow = new QWidget(page);
|
||||||
|
auto* rlay = new QHBoxLayout(restartRow);
|
||||||
|
rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐
|
||||||
|
rlay->setSpacing(10);
|
||||||
|
auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow);
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
hint, QStringLiteral("color:{{text/secondary}}; font-size:%1px;")
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kCaption)));
|
||||||
|
auto* restartBtn = new QPushButton(QStringLiteral("立即重启"), restartRow);
|
||||||
|
rlay->addWidget(hint);
|
||||||
|
rlay->addWidget(restartBtn);
|
||||||
|
rlay->addStretch();
|
||||||
|
restartRow->setVisible(false);
|
||||||
|
v->addWidget(restartRow);
|
||||||
|
|
||||||
|
QObject::connect(fontCombo, &QComboBox::activated, page, [fontCombo, restartRow](int) {
|
||||||
|
geopro::app::setFontScalePreference(fontCombo->currentData().toInt());
|
||||||
|
restartRow->setVisible(true);
|
||||||
|
});
|
||||||
|
QObject::connect(restartBtn, &QPushButton::clicked, restartBtn, [] {
|
||||||
|
QProcess::startDetached(QCoreApplication::applicationFilePath(),
|
||||||
|
QCoreApplication::arguments().mid(1));
|
||||||
|
qApp->quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
v->addStretch();
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* buildAboutPage() {
|
||||||
|
auto* page = new QWidget();
|
||||||
|
auto* v = new QVBoxLayout(page);
|
||||||
|
v->setContentsMargins(24, 20, 24, 20);
|
||||||
|
v->setSpacing(12);
|
||||||
|
v->addWidget(sectionTitle(QStringLiteral("关于"), page));
|
||||||
|
|
||||||
|
auto* ver = new QLabel(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"), page);
|
||||||
|
ver->setStyleSheet(QStringLiteral("font-size:%1px; font-weight:600;")
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kTitle)));
|
||||||
|
v->addWidget(ver);
|
||||||
|
|
||||||
|
auto* license = new QTextBrowser(page);
|
||||||
|
license->setOpenExternalLinks(true);
|
||||||
|
license->setHtml(QStringLiteral(
|
||||||
|
"<b>第三方组件与许可证</b>"
|
||||||
|
"<table cellpadding='3' style='margin-top:6px'>"
|
||||||
|
"<tr><td>Qt 6</td><td>GUI 框架</td><td>LGPL-3.0</td></tr>"
|
||||||
|
"<tr><td>VTK 9</td><td>二维/三维渲染</td><td>BSD-3-Clause</td></tr>"
|
||||||
|
"<tr><td>Qt-Advanced-Docking-System</td><td>停靠布局</td><td>LGPL-2.1</td></tr>"
|
||||||
|
"<tr><td>QtKeychain</td><td>凭证安全存取</td><td>BSD-3-Clause</td></tr>"
|
||||||
|
"</table>"
|
||||||
|
"<p style='margin-top:8px'>完整声明见随附 <i>NOTICE.md</i>。</p>"));
|
||||||
|
v->addWidget(license, 1);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) {
|
||||||
|
setWindowTitle(QStringLiteral("设置"));
|
||||||
|
resize(720, 480);
|
||||||
|
|
||||||
|
auto* root = new QHBoxLayout(this);
|
||||||
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
|
root->setSpacing(0);
|
||||||
|
|
||||||
|
// 左:分类列表。
|
||||||
|
auto* sidebar = new QListWidget(this);
|
||||||
|
sidebar->setObjectName(QStringLiteral("settingsSidebar"));
|
||||||
|
sidebar->setFixedWidth(150);
|
||||||
|
sidebar->addItem(QStringLiteral("外观"));
|
||||||
|
sidebar->addItem(QStringLiteral("关于"));
|
||||||
|
root->addWidget(sidebar);
|
||||||
|
|
||||||
|
// 右:分页。
|
||||||
|
auto* stack = new QStackedWidget(this);
|
||||||
|
stack->addWidget(buildAppearancePage());
|
||||||
|
stack->addWidget(buildAboutPage());
|
||||||
|
root->addWidget(stack, 1);
|
||||||
|
|
||||||
|
QObject::connect(sidebar, &QListWidget::currentRowChanged, stack,
|
||||||
|
&QStackedWidget::setCurrentIndex);
|
||||||
|
sidebar->setCurrentRow(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 设置对话框(左分类 + 右内容页,参考常见客户端布局):
|
||||||
|
// 外观 —— 主题(跟随系统/浅色/深色,热切) + 界面字号(小/标准/大/特大,重启生效)
|
||||||
|
// 关于 —— 版本号 + 第三方组件与许可证
|
||||||
|
class SettingsDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SettingsDialog(QWidget* parent = nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -1,102 +1,176 @@
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QObject>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QProxyStyle>
|
||||||
|
#include <QSettings>
|
||||||
#include <QStyleFactory>
|
#include <QStyleFactory>
|
||||||
|
#include <QStyleHints>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// 应用样式:在 Fusion 基础上把下拉框弹窗改为「列表紧贴文本框下方」(而非 Fusion 默认的
|
||||||
|
// 菜单式弹窗覆盖当前项——那会导致弹窗位置怪、容器+列表两层、选中不清)。
|
||||||
|
class AppProxyStyle : public QProxyStyle {
|
||||||
|
public:
|
||||||
|
AppProxyStyle() : QProxyStyle(QStyleFactory::create(QStringLiteral("Fusion"))) {}
|
||||||
|
int styleHint(StyleHint hint, const QStyleOption* opt, const QWidget* w,
|
||||||
|
QStyleHintReturn* ret) const override
|
||||||
|
{
|
||||||
|
if (hint == QStyle::SH_ComboBox_Popup) return 0;
|
||||||
|
return QProxyStyle::styleHint(hint, opt, w, ret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 语义令牌表(全 UI 唯一颜色来源)。改色只改这一处。 ──────────────────
|
||||||
|
// 取值来源:规范 §1.5 语义映射 + 附录 A 速查 + §1.3 画布专用色。
|
||||||
|
// 画布(canvas/*)与 bg/canvas 两模式同值——规范 §0.5「视图区永远深色」。
|
||||||
|
struct Token { const char* name; const char* light; const char* dark; };
|
||||||
|
const Token kTokens[] = {
|
||||||
|
// 背景
|
||||||
|
{"bg/app", "#F7F8FA", "#0E1116"},
|
||||||
|
{"bg/panel", "#FFFFFF", "#161A20"},
|
||||||
|
{"bg/panel-subtle", "#FCFCFD", "#161B22"},
|
||||||
|
{"bg/header", "#FFFFFF", "#12161C"},
|
||||||
|
{"bg/hover", "#EFF1F4", "#1B2129"},
|
||||||
|
{"bg/selected", "#EFF5FF", "#16243F"},
|
||||||
|
{"bg/canvas", "#0B1320", "#0B1320"},
|
||||||
|
// 边框
|
||||||
|
{"border/default", "#E3E6EB", "#262C35"},
|
||||||
|
{"border/strong", "#CDD2DA", "#333B45"},
|
||||||
|
{"border/focus", "#3B73EC", "#5E8DF5"},
|
||||||
|
// 文字
|
||||||
|
{"text/primary", "#272C35", "#E6E9EF"},
|
||||||
|
{"text/secondary", "#5A626F", "#A4ADBB"},
|
||||||
|
{"text/tertiary", "#7C8493", "#7A8494"},
|
||||||
|
{"text/disabled", "#A8AFBC", "#5A626F"},
|
||||||
|
{"text/link", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"text/on-primary", "#FFFFFF", "#FFFFFF"},
|
||||||
|
// 强调
|
||||||
|
{"accent/primary", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"accent/primary-hover", "#2B5FD9", "#93B4FA"},
|
||||||
|
{"accent/primary-pressed","#2450B8", "#3B73EC"},
|
||||||
|
// 其他
|
||||||
|
{"divider", "#E3E6EB", "#22272F"},
|
||||||
|
{"scrollbar/thumb", "#CDD2DA", "#3A424D"},
|
||||||
|
{"scrollbar/thumb-hover", "#A8AFBC", "#4A535F"},
|
||||||
|
// 状态色(主色 + 浅底)规范 §1.4
|
||||||
|
{"status/danger", "#E5484D", "#FF6166"},
|
||||||
|
{"status/danger-bg", "#FDECEC", "#3A1D1F"},
|
||||||
|
{"status/warning", "#E08A1E", "#F5A623"},
|
||||||
|
{"status/warning-bg", "#FBF0DD", "#3A2C12"},
|
||||||
|
{"status/success", "#2E9E5B", "#46C07A"},
|
||||||
|
{"status/success-bg", "#E7F6ED", "#16301F"},
|
||||||
|
{"status/info", "#3B73EC", "#5E8DF5"},
|
||||||
|
{"status/info-bg", "#EFF5FF", "#16243F"},
|
||||||
|
{"status/neutral", "#7C8493", "#8A93A3"},
|
||||||
|
// 画布专用(两模式同值)规范 §1.3
|
||||||
|
{"canvas/bg", "#0B1320", "#0B1320"},
|
||||||
|
{"canvas/bg-soft", "#111B2D", "#111B2D"},
|
||||||
|
{"canvas/grid", "#1E2A3D", "#1E2A3D"},
|
||||||
|
{"canvas/text", "#E6ECF5", "#E6ECF5"},
|
||||||
|
{"canvas/text-dim", "#8A97AC", "#8A97AC"},
|
||||||
|
};
|
||||||
|
|
||||||
|
QString tokenHex(const char* name, bool dark)
|
||||||
|
{
|
||||||
|
for (const auto& t : kTokens)
|
||||||
|
if (qstrcmp(t.name, name) == 0) return QString::fromLatin1(dark ? t.dark : t.light);
|
||||||
|
return QStringLiteral("#FF00FF"); // 漏配的令牌显眼品红,便于一眼发现
|
||||||
|
}
|
||||||
|
|
||||||
// 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。
|
// 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。
|
||||||
// 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写,
|
// 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写,
|
||||||
// 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制,
|
// 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制,
|
||||||
// 它会自动采用调色板的 Highlight(#2D6CB5) 作勾选色,省去打包图片资源。
|
// 它会自动采用调色板的 Highlight(accent/primary) 作勾选色,省去打包图片资源。
|
||||||
const char* kStyleSheet = R"QSS(
|
const char* kStyleSheet = R"QSS(
|
||||||
/* ── 基础 ───────────────────────────────────────────────── */
|
/* ── 基础 ───────────────────────────────────────────────── */
|
||||||
QWidget {
|
QWidget {
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
}
|
}
|
||||||
QMainWindow, QDialog {
|
QMainWindow, QDialog {
|
||||||
background: #F4F6FA;
|
background: {{bg/app}};
|
||||||
}
|
|
||||||
QToolTip {
|
|
||||||
background: #1F2A3D;
|
|
||||||
color: #F4F6FA;
|
|
||||||
border: 1px solid #2D6CB5;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
}
|
||||||
|
/* QToolTip 不写 QSS:用系统原生工具提示(自定义 QSS 会让弹窗圆角露直角、且不像原生)。 */
|
||||||
|
|
||||||
/* ── 视图内工具条(2D/3D、数据详情):白底分段控件,柔和不刺眼 ── */
|
/* ── 视图内工具条(2D/3D、数据详情):白底分段控件,柔和不刺眼 ── */
|
||||||
QToolBar {
|
QToolBar {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid #EAEEF4;
|
border-bottom: 1px solid {{divider}};
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
spacing: 4px;
|
spacing: 4px;
|
||||||
}
|
}
|
||||||
QToolBar QToolButton {
|
QToolBar QToolButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
QToolBar QToolButton:hover {
|
QToolBar QToolButton:hover {
|
||||||
background: #EEF3FB;
|
background: {{bg/hover}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
}
|
}
|
||||||
QToolBar QToolButton:pressed {
|
QToolBar QToolButton:pressed {
|
||||||
background: #DCE9F8;
|
background: {{bg/selected}};
|
||||||
}
|
}
|
||||||
QToolBar QToolButton:checked {
|
QToolBar QToolButton:checked {
|
||||||
background: #EAF1FB;
|
background: {{bg/hover}};
|
||||||
color: #2D6CB5;
|
color: {{accent/primary}};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
QToolBar QToolButton:checked:hover {
|
QToolBar QToolButton:checked:hover {
|
||||||
background: #DCE9F8;
|
background: {{bg/selected}};
|
||||||
}
|
}
|
||||||
QToolBar::separator {
|
QToolBar::separator {
|
||||||
background: #EAEEF4;
|
background: {{divider}};
|
||||||
width: 1px;
|
width: 1px;
|
||||||
margin: 6px 8px;
|
margin: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */
|
/* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */
|
||||||
QTreeWidget, QListWidget, QTreeView, QListView {
|
QTreeWidget, QListWidget, QTreeView, QListView {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
|
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
|
||||||
padding: 7px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 6px;
|
|
||||||
margin: 1px 4px;
|
|
||||||
}
|
}
|
||||||
QTreeWidget::item:hover, QListWidget::item:hover,
|
QTreeWidget::item:hover, QListWidget::item:hover,
|
||||||
QTreeView::item:hover, QListView::item:hover {
|
QTreeView::item:hover, QListView::item:hover {
|
||||||
background: #EEF3FB;
|
background: {{bg/hover}};
|
||||||
}
|
}
|
||||||
QTreeWidget::item:selected, QListWidget::item:selected,
|
QTreeWidget::item:selected, QListWidget::item:selected,
|
||||||
QTreeView::item:selected, QListView::item:selected {
|
QTreeView::item:selected, QListView::item:selected {
|
||||||
background: #DCE9F8;
|
background: {{bg/selected}};
|
||||||
color: #1B3D67;
|
color: {{text/primary}};
|
||||||
|
}
|
||||||
|
QTreeWidget::item:selected:!active, QListWidget::item:selected:!active,
|
||||||
|
QTreeView::item:selected:!active, QListView::item:selected:!active {
|
||||||
|
background: {{bg/selected}};
|
||||||
|
color: {{text/primary}};
|
||||||
}
|
}
|
||||||
/* 注意:不要给 QTreeView::branch 设 background——一旦改写 branch,Qt 会停止绘制
|
/* 注意:不要给 QTreeView::branch 设 background——一旦改写 branch,Qt 会停止绘制
|
||||||
默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */
|
默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */
|
||||||
|
|
||||||
/* 表头(对象显示栏) */
|
/* 表头(对象显示栏) */
|
||||||
QHeaderView::section {
|
QHeaderView::section {
|
||||||
background: #EDF1F7;
|
background: {{bg/hover}};
|
||||||
color: #3A475C;
|
color: {{text/secondary}};
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid #D5DBE5;
|
border-bottom: 1px solid {{border/default}};
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
@ -104,82 +178,82 @@ QHeaderView::section {
|
||||||
/* ── 标签页(数据 / 文件):现代下划线 tab,无边框盒子 ──────── */
|
/* ── 标签页(数据 / 文件):现代下划线 tab,无边框盒子 ──────── */
|
||||||
QTabWidget::pane {
|
QTabWidget::pane {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid #EAEEF4;
|
border-top: 1px solid {{divider}};
|
||||||
top: 0;
|
top: 0;
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
}
|
}
|
||||||
QTabBar {
|
QTabBar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
QTabBar::tab {
|
QTabBar::tab {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
QTabBar::tab:selected {
|
QTabBar::tab:selected {
|
||||||
color: #2D6CB5;
|
color: {{accent/primary}};
|
||||||
border-bottom: 2px solid #2D6CB5;
|
border-bottom: 2px solid {{accent/primary}};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
QTabBar::tab:hover:!selected {
|
QTabBar::tab:hover:!selected {
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 复选框(仅调间距/字色,indicator 交给 Fusion 原生)──── */
|
/* ── 复选框(仅调间距/字色,indicator 交给 Fusion 原生)──── */
|
||||||
QCheckBox {
|
QCheckBox {
|
||||||
spacing: 7px;
|
spacing: 7px;
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
}
|
}
|
||||||
QCheckBox:disabled {
|
QCheckBox:disabled {
|
||||||
color: #9AA6B6;
|
color: {{text/disabled}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */
|
/* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
border: 1px solid #C2CCDA;
|
border: 1px solid {{border/strong}};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background: #EEF3FB;
|
background: {{bg/hover}};
|
||||||
border-color: #2D6CB5;
|
border-color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background: #DCE9F8;
|
background: {{bg/selected}};
|
||||||
}
|
}
|
||||||
QPushButton:default {
|
QPushButton:default {
|
||||||
background: #2D6CB5;
|
background: {{accent/primary}};
|
||||||
color: #FFFFFF;
|
color: {{text/on-primary}};
|
||||||
border-color: #2D6CB5;
|
border-color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
QPushButton:default:hover {
|
QPushButton:default:hover {
|
||||||
background: #2862A6;
|
background: {{accent/primary-hover}};
|
||||||
}
|
}
|
||||||
QPushButton:disabled {
|
QPushButton:disabled {
|
||||||
background: #F0F2F6;
|
background: {{bg/app}};
|
||||||
color: #9AA6B6;
|
color: {{text/disabled}};
|
||||||
border-color: #DCE0E7;
|
border-color: {{border/default}};
|
||||||
}
|
}
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
border: 1px solid #C7D2E0;
|
border: 1px solid {{border/strong}};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 5px 8px;
|
padding: 6px 8px;
|
||||||
selection-background-color: #2D6CB5;
|
selection-background-color: {{accent/primary}};
|
||||||
selection-color: #FFFFFF;
|
selection-color: {{text/on-primary}};
|
||||||
}
|
}
|
||||||
QLineEdit:focus {
|
QLineEdit:focus {
|
||||||
border: 1px solid #2D6CB5;
|
border: 1px solid {{accent/primary}};
|
||||||
}
|
}
|
||||||
QLineEdit:disabled {
|
QLineEdit:disabled {
|
||||||
background: #F0F2F6;
|
background: {{bg/app}};
|
||||||
color: #8A93A3;
|
color: {{text/disabled}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
|
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
|
||||||
|
|
@ -189,12 +263,12 @@ QScrollBar:vertical {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:vertical {
|
QScrollBar::handle:vertical {
|
||||||
background: #C2CCDA;
|
background: {{scrollbar/thumb}};
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:vertical:hover {
|
QScrollBar::handle:vertical:hover {
|
||||||
background: #A7B4C7;
|
background: {{scrollbar/thumb-hover}};
|
||||||
}
|
}
|
||||||
QScrollBar:horizontal {
|
QScrollBar:horizontal {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -202,12 +276,12 @@ QScrollBar:horizontal {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:horizontal {
|
QScrollBar::handle:horizontal {
|
||||||
background: #C2CCDA;
|
background: {{scrollbar/thumb}};
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:horizontal:hover {
|
QScrollBar::handle:horizontal:hover {
|
||||||
background: #A7B4C7;
|
background: {{scrollbar/thumb-hover}};
|
||||||
}
|
}
|
||||||
QScrollBar::add-line, QScrollBar::sub-line {
|
QScrollBar::add-line, QScrollBar::sub-line {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
@ -219,98 +293,111 @@ QScrollBar::add-page, QScrollBar::sub-page {
|
||||||
|
|
||||||
/* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */
|
/* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */
|
||||||
QSplitter::handle {
|
QSplitter::handle {
|
||||||
background: #EAEEF4;
|
background: {{divider}};
|
||||||
}
|
}
|
||||||
QSplitter::handle:hover {
|
QSplitter::handle:hover {
|
||||||
background: #C7D2E0;
|
background: {{accent/primary}};
|
||||||
}
|
}
|
||||||
ads--CDockSplitter::handle {
|
ads--CDockSplitter::handle {
|
||||||
background: #EAEEF4;
|
background: {{divider}};
|
||||||
}
|
}
|
||||||
ads--CDockSplitter::handle:hover {
|
ads--CDockSplitter::handle:hover {
|
||||||
background: #C7D2E0;
|
background: {{accent/primary}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */
|
/* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */
|
||||||
QStatusBar {
|
QStatusBar {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
border-top: 1px solid #EAEEF4;
|
border-top: 1px solid {{divider}};
|
||||||
}
|
}
|
||||||
QStatusBar::item {
|
QStatusBar::item {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
QStatusBar QLabel {
|
QStatusBar QLabel {
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 菜单栏 / 菜单(按需出现时也与主题一致)────────────────── */
|
/* ── 菜单栏 / 菜单(标准 QMenuBar/QMenu):刻意不设 border-radius——弹窗圆角靠系统(Win11
|
||||||
|
原生圆角),QSS 设圆角会露出后面的直角。仅设底/字/选中,干净不刺眼。 */
|
||||||
QMenuBar {
|
QMenuBar {
|
||||||
background: #EDF1F7;
|
background: {{bg/panel}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
border-bottom: 1px solid #D5DBE5;
|
border-bottom: 1px solid {{divider}};
|
||||||
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
QMenuBar::item {
|
QMenuBar::item {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 5px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
QMenuBar::item:selected {
|
QMenuBar::item:selected {
|
||||||
background: #DCE6F4;
|
background: {{bg/hover}};
|
||||||
|
color: {{accent/primary}};
|
||||||
|
}
|
||||||
|
QMenuBar::item:pressed {
|
||||||
|
background: {{bg/selected}};
|
||||||
}
|
}
|
||||||
QMenu {
|
QMenu {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
border: 1px solid #D5DBE5;
|
border: 1px solid {{border/default}};
|
||||||
border-radius: 8px;
|
padding: 4px;
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
QMenu::item {
|
QMenu::item {
|
||||||
padding: 6px 24px 6px 14px;
|
padding: 6px 24px 6px 14px;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
QMenu::item:selected {
|
QMenu::item:selected {
|
||||||
background: #DCE9F8;
|
background: {{bg/hover}};
|
||||||
color: #1B3D67;
|
color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
QMenu::separator {
|
QMenu::separator {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: #E1E6EE;
|
background: {{divider}};
|
||||||
margin: 5px 8px;
|
margin: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
|
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
|
||||||
QComboBox {
|
QComboBox {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
border: 1px solid #C2CCDA;
|
border: 1px solid {{border/strong}};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 5px 10px;
|
padding: 6px 10px;
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
QComboBox:hover {
|
QComboBox:hover {
|
||||||
border-color: #2D6CB5;
|
border-color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
QComboBox:focus {
|
QComboBox:focus {
|
||||||
border-color: #2D6CB5;
|
border-color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
QComboBox::drop-down {
|
QComboBox::drop-down {
|
||||||
border: none;
|
border: none;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
QComboBox QAbstractItemView {
|
QComboBox QAbstractItemView {
|
||||||
background: #FFFFFF;
|
background: {{bg/panel}};
|
||||||
border: 1px solid #D5DBE5;
|
border: 1px solid {{border/default}};
|
||||||
border-radius: 6px;
|
|
||||||
selection-background-color: #DCE9F8;
|
|
||||||
selection-color: #1B3D67;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView::item {
|
||||||
|
border: none;
|
||||||
|
padding: 6px 10px;
|
||||||
|
min-height: 20px;
|
||||||
|
color: {{text/primary}};
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView::item:selected {
|
||||||
|
background: {{bg/hover}};
|
||||||
|
color: {{accent/primary}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
|
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
|
||||||
QGroupBox {
|
QGroupBox {
|
||||||
border: 1px solid #D5DBE5;
|
border: 1px solid {{border/default}};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
|
|
@ -320,109 +407,267 @@ QGroupBox::title {
|
||||||
subcontrol-origin: margin;
|
subcontrol-origin: margin;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
color: #3A475C;
|
color: {{text/secondary}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */
|
/* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */
|
||||||
QProgressBar {
|
QProgressBar {
|
||||||
background: #E6EBF3;
|
background: {{divider}};
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
}
|
}
|
||||||
QProgressBar::chunk {
|
QProgressBar::chunk {
|
||||||
background: #2D6CB5;
|
background: {{accent/primary}};
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
|
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
|
||||||
面板已锁定(无关闭/浮动/拖动),每个区只一个标签即表头:统一浅色头 +
|
面板已锁定(无关闭/浮动/拖动),每个区只一个标签即表头:统一浅色头 +
|
||||||
蓝色下划线强调 + 深色加粗标题。各区一致,读起来像固定子窗口表头。 */
|
蓝色下划线强调 + 深色加粗标题。各区一致,读起来像固定子窗口表头。 */
|
||||||
ads--CDockAreaWidget {
|
ads--CDockAreaWidget {
|
||||||
background: #F4F6FA;
|
background: {{bg/app}};
|
||||||
}
|
}
|
||||||
ads--CDockAreaTitleBar {
|
ads--CDockAreaTitleBar {
|
||||||
background: #EDF1F7;
|
background: {{bg/hover}};
|
||||||
border-bottom: 1px solid #D5DBE5;
|
border-bottom: 1px solid {{border/default}};
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
ads--CDockWidgetTab {
|
ads--CDockWidgetTab {
|
||||||
background: #EDF1F7;
|
background: {{bg/hover}};
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
ads--CDockWidgetTab[activeTab="true"] {
|
ads--CDockWidgetTab[activeTab="true"] {
|
||||||
background: #EDF1F7;
|
background: {{bg/hover}};
|
||||||
border-bottom: 2px solid #2D6CB5;
|
border-bottom: 2px solid {{accent/primary}};
|
||||||
}
|
}
|
||||||
ads--CDockWidgetTab QLabel {
|
ads--CDockWidgetTab QLabel {
|
||||||
color: #5A6B85;
|
color: {{text/secondary}};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
ads--CDockWidgetTab[activeTab="true"] QLabel {
|
ads--CDockWidgetTab[activeTab="true"] QLabel {
|
||||||
color: #1F2A3D;
|
color: {{text/primary}};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
)QSS";
|
)QSS";
|
||||||
|
|
||||||
// 浅色专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。
|
// 全局复选指示器:用 writeCheckboxIcon 生成清晰复选框 PNG(未选=明显边框空心框,
|
||||||
QPalette buildPalette()
|
// 选中=强调色填充+白勾),统一作用于 QCheckBox 与 树/列表的勾选指示器。规避 Fusion
|
||||||
|
// 原生复选框在浅底下边框过淡看不清的问题——全 UI 一套,避免逐控件打补丁。
|
||||||
|
QString indicatorQss(bool dark)
|
||||||
|
{
|
||||||
|
const QColor border = QColor(tokenHex("border/strong", dark));
|
||||||
|
const QColor boxBg = QColor(tokenHex("bg/panel", dark));
|
||||||
|
const QColor accent = QColor(tokenHex("accent/primary", dark));
|
||||||
|
const QString tag = dark ? QStringLiteral("gd") : QStringLiteral("gl"); // 全局缓存标签
|
||||||
|
const QString off = geopro::app::writeCheckboxIcon(false, border, boxBg, Qt::white, tag);
|
||||||
|
const QString on = geopro::app::writeCheckboxIcon(true, accent, accent, Qt::white, tag);
|
||||||
|
return QStringLiteral(
|
||||||
|
"QCheckBox::indicator, QTreeView::indicator, QListView::indicator,"
|
||||||
|
"QTreeWidget::indicator, QListWidget::indicator { width:16px; height:16px; }"
|
||||||
|
"QCheckBox::indicator:unchecked, QTreeView::indicator:unchecked,"
|
||||||
|
"QListView::indicator:unchecked, QTreeWidget::indicator:unchecked,"
|
||||||
|
"QListWidget::indicator:unchecked { image:url(%1); }"
|
||||||
|
"QCheckBox::indicator:checked, QTreeView::indicator:checked,"
|
||||||
|
"QListView::indicator:checked, QTreeWidget::indicator:checked,"
|
||||||
|
"QListWidget::indicator:checked { image:url(%2); }")
|
||||||
|
.arg(off, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前模式的全局 QSS。
|
||||||
|
QString styleSheetForMode(bool /*dark*/)
|
||||||
|
{
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。
|
||||||
|
QPalette buildPalette(bool dark)
|
||||||
{
|
{
|
||||||
QPalette p;
|
QPalette p;
|
||||||
const QColor shell("#F4F6FA");
|
const QColor shell = QColor(tokenHex("bg/app", dark));
|
||||||
const QColor panel("#FFFFFF");
|
const QColor panel = QColor(tokenHex("bg/panel", dark));
|
||||||
const QColor text("#1F2A3D");
|
const QColor text = QColor(tokenHex("text/primary", dark));
|
||||||
const QColor mutedText("#5A6B85");
|
const QColor muted = QColor(tokenHex("text/secondary", dark));
|
||||||
const QColor accent("#2D6CB5");
|
const QColor accent = QColor(tokenHex("accent/primary", dark));
|
||||||
|
const QColor border = QColor(tokenHex("border/default", dark));
|
||||||
|
const QColor disabled = QColor(tokenHex("text/disabled", dark));
|
||||||
|
const QColor hoverBg = QColor(tokenHex("bg/hover", dark));
|
||||||
|
|
||||||
p.setColor(QPalette::Window, shell);
|
p.setColor(QPalette::Window, shell);
|
||||||
p.setColor(QPalette::WindowText, text);
|
p.setColor(QPalette::WindowText, text);
|
||||||
p.setColor(QPalette::Base, panel);
|
p.setColor(QPalette::Base, panel);
|
||||||
p.setColor(QPalette::AlternateBase, QColor("#F0F3F8"));
|
p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark)));
|
||||||
p.setColor(QPalette::Text, text);
|
p.setColor(QPalette::Text, text);
|
||||||
p.setColor(QPalette::Button, QColor("#EDF1F7"));
|
p.setColor(QPalette::Button, hoverBg);
|
||||||
p.setColor(QPalette::ButtonText, text);
|
p.setColor(QPalette::ButtonText, text);
|
||||||
p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D"));
|
p.setColor(QPalette::ToolTipBase, text);
|
||||||
p.setColor(QPalette::ToolTipText, shell);
|
p.setColor(QPalette::ToolTipText, panel);
|
||||||
p.setColor(QPalette::Highlight, accent);
|
p.setColor(QPalette::Highlight, accent);
|
||||||
p.setColor(QPalette::HighlightedText, panel);
|
p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark)));
|
||||||
p.setColor(QPalette::PlaceholderText, mutedText);
|
p.setColor(QPalette::PlaceholderText, muted);
|
||||||
p.setColor(QPalette::Link, accent);
|
p.setColor(QPalette::Link, accent);
|
||||||
|
// Fusion 的明暗 3D 角色统一压成边框色,立体效果塌成平面(去斜角/凹槽)。
|
||||||
// 关键:把 Fusion 用于绘制 3D 凹凸(斜角/凹槽/分隔条阴影)的明暗角色统一压成相近浅灰,
|
p.setColor(QPalette::Light, panel);
|
||||||
// 立体效果即塌成平面。ADS 分隔条用 palette(dark),这样也变成一条扁平浅灰细线(无 3D)。
|
p.setColor(QPalette::Midlight, border);
|
||||||
p.setColor(QPalette::Light, QColor("#FFFFFF"));
|
p.setColor(QPalette::Mid, border);
|
||||||
p.setColor(QPalette::Midlight, QColor("#EEF1F5"));
|
p.setColor(QPalette::Dark, border);
|
||||||
p.setColor(QPalette::Mid, QColor("#E1E6EE"));
|
p.setColor(QPalette::Shadow, border);
|
||||||
p.setColor(QPalette::Dark, QColor("#D7DEE8"));
|
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
|
||||||
p.setColor(QPalette::Shadow, QColor("#D7DEE8"));
|
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
|
||||||
// 禁用态:统一灰化,避免 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;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void applyTheme(QApplication& app)
|
void applyThemeMode(QApplication& app, bool dark)
|
||||||
{
|
{
|
||||||
// Fusion:跨平台一致且对 QSS 友好(Windows 原生风对部分控件会忽略样式表)。
|
// Fusion + 下拉框弹窗修正(AppProxyStyle):跨平台一致、对 QSS 友好。
|
||||||
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
app.setStyle(new AppProxyStyle());
|
||||||
|
|
||||||
// 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。
|
// 基础字体:微软雅黑 UI;基准字号取令牌 type::kBody(13px),与 QSS 同单位。
|
||||||
// 10pt(≈13px)对齐主流商用客户端基准;9pt 偏小显拥挤。抗锯齿优先,观感更精致。
|
QFont base(QStringLiteral("Microsoft YaHei UI"));
|
||||||
QFont base(QStringLiteral("Microsoft YaHei UI"), 10);
|
base.setPixelSize(scaledPx(type::kBody)); // 随界面字号缩放
|
||||||
base.setStyleStrategy(QFont::PreferAntialias);
|
base.setStyleStrategy(QFont::PreferAntialias);
|
||||||
app.setFont(base);
|
app.setFont(base);
|
||||||
|
|
||||||
app.setPalette(buildPalette());
|
app.setPalette(buildPalette(dark));
|
||||||
app.setStyleSheet(QString::fromUtf8(kStyleSheet));
|
app.setStyleSheet(styleSheetForMode(dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTheme(QApplication& app)
|
||||||
|
{
|
||||||
|
applyThemeMode(app, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 主题管理器(替代 ElaTheme)+ 设置:主题 / 字号 偏好 ──────────────────
|
||||||
|
namespace {
|
||||||
|
constexpr int kBaseFontPx = 13; // 基准字号
|
||||||
|
int g_fontScale = 100; // 当前字号缩放百分比
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ThemeManager& ThemeManager::instance()
|
||||||
|
{
|
||||||
|
static ThemeManager inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeManager::ThemeManager(QObject* parent) : QObject(parent)
|
||||||
|
{
|
||||||
|
applyPersisted();
|
||||||
|
// 跟随系统时,系统明暗变化即同步并发 changed。
|
||||||
|
QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
|
||||||
|
[this](Qt::ColorScheme) {
|
||||||
|
if (!follow_) return;
|
||||||
|
const bool d = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
|
||||||
|
if (d != dark_) {
|
||||||
|
dark_ = d;
|
||||||
|
emit changed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::applyPersisted()
|
||||||
|
{
|
||||||
|
const QString m =
|
||||||
|
QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
|
||||||
|
if (m == QStringLiteral("light")) {
|
||||||
|
follow_ = false;
|
||||||
|
dark_ = false;
|
||||||
|
} else if (m == QStringLiteral("dark")) {
|
||||||
|
follow_ = false;
|
||||||
|
dark_ = true;
|
||||||
|
} else { // system
|
||||||
|
follow_ = true;
|
||||||
|
dark_ = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::setMode(const QString& mode)
|
||||||
|
{
|
||||||
|
QSettings().setValue(QStringLiteral("ui/themeMode"), mode);
|
||||||
|
applyPersisted();
|
||||||
|
emit changed(); // 热切:全 UI 重着色
|
||||||
|
}
|
||||||
|
|
||||||
|
QString themeModePreference()
|
||||||
|
{
|
||||||
|
return QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyPersistedThemeMode()
|
||||||
|
{
|
||||||
|
ThemeManager::instance().applyPersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setThemeModePreference(const QString& mode)
|
||||||
|
{
|
||||||
|
ThemeManager::instance().setMode(mode); // 持久化 + 热切
|
||||||
|
}
|
||||||
|
|
||||||
|
int fontScalePreference()
|
||||||
|
{
|
||||||
|
return QSettings().value(QStringLiteral("ui/fontScale"), 100).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFontScalePreference(int percent)
|
||||||
|
{
|
||||||
|
QSettings().setValue(QStringLiteral("ui/fontScale"), percent); // 重启后生效
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyPersistedFontScale()
|
||||||
|
{
|
||||||
|
g_fontScale = fontScalePreference();
|
||||||
|
QFont f = qApp->font();
|
||||||
|
f.setPixelSize(kBaseFontPx * g_fontScale / 100);
|
||||||
|
qApp->setFont(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int scaledPx(int basePx)
|
||||||
|
{
|
||||||
|
return basePx * g_fontScale / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDarkTheme()
|
||||||
|
{
|
||||||
|
return ThemeManager::instance().isDark();
|
||||||
|
}
|
||||||
|
|
||||||
|
void vtkBackground(double& r, double& g, double& b)
|
||||||
|
{
|
||||||
|
// 规范 §0.5/§11:数据画布永远深色,不随明暗切换。取 canvas/bg。
|
||||||
|
const QColor c = tokenColor("canvas/bg"); // #0B1320
|
||||||
|
r = c.redF();
|
||||||
|
g = c.greenF();
|
||||||
|
b = c.blueF();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString token(const char* name) { return tokenHex(name, isDarkTheme()); }
|
||||||
|
|
||||||
|
QColor tokenColor(const char* name) { return QColor(token(name)); }
|
||||||
|
|
||||||
|
QString fillTokens(const QString& tmpl)
|
||||||
|
{
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
QString s = tmpl;
|
||||||
|
for (const auto& t : kTokens)
|
||||||
|
s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)),
|
||||||
|
QString::fromLatin1(dark ? t.dark : t.light));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl)
|
||||||
|
{
|
||||||
|
if (!w) return;
|
||||||
|
w->setStyleSheet(fillTokens(tmpl));
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
|
||||||
|
[w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); });
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,134 @@
|
||||||
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
|
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
|
||||||
// 危险 #C0392B
|
// 危险 #C0392B
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
class QApplication;
|
class QApplication;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 应用浅色专业主题(Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。
|
// 主题管理器(纯 Qt,替代 ElaTheme):持有当前明暗 + 是否跟随系统;切换发 changed() 信号,
|
||||||
|
// 全 UI(全局 QSS 由 main 重应用、内联 chrome 由 applyTokenizedStyleSheet)据此热切重着色。
|
||||||
|
class ThemeManager : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
static ThemeManager& instance();
|
||||||
|
bool isDark() const { return dark_; }
|
||||||
|
void setMode(const QString& mode); // "system"|"light"|"dark":持久化 + 应用 + 发 changed
|
||||||
|
void applyPersisted(); // 按持久化偏好设置状态(系统模式则取系统明暗)
|
||||||
|
signals:
|
||||||
|
void changed();
|
||||||
|
private:
|
||||||
|
explicit ThemeManager(QObject* parent = nullptr);
|
||||||
|
bool dark_ = false;
|
||||||
|
bool follow_ = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 排版令牌(全项目唯一字号阶 + 字重角色)──────────────────────────
|
||||||
|
// 各处 QSS 的 font-size / font-weight 一律引用这些值,不再散落硬编码 px。
|
||||||
|
// 阶比 ~1.18(body→title→heading),刻意拉开层级——避免 11/12/13/14
|
||||||
|
// 这类只差 1px 的"糊层级",让标题/正文/说明三档一眼可分。
|
||||||
|
// 单位:px(与全局 px 化的 QSS、固定像素行高对齐;后续若做无障碍字号
|
||||||
|
// 缩放再统一切 pt)。字重:400 正文 / 500 可交互激活 /
|
||||||
|
// 600 标签·标题 / 700 仅展示性大标题。
|
||||||
|
namespace type {
|
||||||
|
|
||||||
|
inline constexpr int kCaption = 12; // 徽标·提示·角色名·错误·副标题·字段标签
|
||||||
|
inline constexpr int kBody = 13; // 树/列表/菜单/正文(= 全局基准字号)
|
||||||
|
inline constexpr int kLabel = 13; // 表头·用户名等需加粗的同级标签(配 600)
|
||||||
|
inline constexpr int kTitle = 15; // 面板/停靠区/区段标题·主操作按钮
|
||||||
|
inline constexpr int kHeading = 18; // 视图/对话框级标题(预留)
|
||||||
|
inline constexpr int kDisplay = 24; // 登录品牌名(唯一展示性大字)
|
||||||
|
|
||||||
|
inline constexpr int kWeightRegular = 400;
|
||||||
|
inline constexpr int kWeightMedium = 500;
|
||||||
|
inline constexpr int kWeightSemibold = 600;
|
||||||
|
inline constexpr int kWeightBold = 700;
|
||||||
|
|
||||||
|
} // namespace type
|
||||||
|
|
||||||
|
// ── 间距令牌(全项目唯一间距阶)──────────────────────────────────
|
||||||
|
// 取代散落的 5/7/9/11/13/15/26 等任意值。这是密集专业工具的实际节奏
|
||||||
|
// (非外加的 8pt 网格):相邻档 2px 粒度,足够紧凑又不糊成一片。
|
||||||
|
// 用法:布局 setContentsMargins/setSpacing/addSpacing 与 QSS padding/margin
|
||||||
|
// 一律引用这些档;明显的奇数值就近归档(13/15→lg, 11→ml, 26→xxl)。
|
||||||
|
namespace space {
|
||||||
|
|
||||||
|
inline constexpr int kXxs = 2; // 发丝级:下划线偏移、滚动条边距
|
||||||
|
inline constexpr int kXs = 4; // 紧凑内边距、最小间隙
|
||||||
|
inline constexpr int kSm = 6; // 行内紧凑(控件竖向 padding)
|
||||||
|
inline constexpr int kMd = 8; // 标准间隔(最常用)
|
||||||
|
inline constexpr int kMl = 10; // 偏大行距(密集行/验证码行)
|
||||||
|
inline constexpr int kLg = 12; // 分组间隔、面板内左右边距
|
||||||
|
inline constexpr int kXl = 16; // 区块内边距
|
||||||
|
inline constexpr int kXxl = 24; // 区块间距、表单纵向边距
|
||||||
|
inline constexpr int kXxxl = 32; // 页面级留白(登录窗左右边距)
|
||||||
|
|
||||||
|
} // namespace space
|
||||||
|
|
||||||
|
// ── 圆角令牌(统一原先 4/5/6/7/8/9 共 6 档为 3 档)────────────────
|
||||||
|
// 圆形元素(头像等)用 直径/2 单独写字面量,不入档。
|
||||||
|
namespace radius {
|
||||||
|
|
||||||
|
inline constexpr int kSm = 6; // 按钮·输入·菜单项·滚动条·进度条
|
||||||
|
inline constexpr int kMd = 8; // 卡片·面板·对话框·菜单·分组框
|
||||||
|
inline constexpr int kPill = 9; // 数量徽标胶囊
|
||||||
|
|
||||||
|
} // namespace radius
|
||||||
|
|
||||||
|
// ── 语义色令牌(状态/反馈,产品语境:只在承载含义处用,不作装饰)──────────
|
||||||
|
// 文字值均针对白底面板(#FFFFFF)选深色,对比度 ≥4.5:1(正文级);与冷调中性
|
||||||
|
// 调色板调和。danger 沿用既有红,避免引入第二种红。
|
||||||
|
namespace semantic {
|
||||||
|
|
||||||
|
inline constexpr const char* kInfo = "#2D6CB5"; // 信息·进行中(= 品牌蓝)
|
||||||
|
inline constexpr const char* kSuccess = "#15803D"; // 成功·已完成(深绿)
|
||||||
|
inline constexpr const char* kWarning = "#B45309"; // 警告·需注意(深琥珀)
|
||||||
|
inline constexpr const char* kDanger = "#C0392B"; // 危险·错误(沿用既有红)
|
||||||
|
|
||||||
|
// 浅色填充(徽标/标签底色,配同族深色文字使用)。
|
||||||
|
inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kWarning 文字)
|
||||||
|
|
||||||
|
} // namespace semantic
|
||||||
|
|
||||||
|
// 应用专业主题(Fusion + 调色板 + 全局样式表)。dark=true 走暗色(P2 主题桥用)。
|
||||||
|
// 暗色复用同一 QSS 结构,颜色全由 kTokens 双值(fillTokens/tokenHex)驱动;幂等,可随主题切换重复调用。
|
||||||
|
void applyThemeMode(QApplication& app, bool dark);
|
||||||
|
|
||||||
|
// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。
|
||||||
void applyTheme(QApplication& app);
|
void applyTheme(QApplication& app);
|
||||||
|
|
||||||
|
// ── 设置:主题 / 界面字号 偏好(QSettings 持久化)────────────────────────
|
||||||
|
// 启动时(eApp->init + applyBrandAccent 之后、弹登录窗之前)各调一次,使登录页与主页统一。
|
||||||
|
void applyPersistedThemeMode(); // 应用持久化主题:跟随系统 / 浅色 / 深色 → ElaTheme
|
||||||
|
void applyPersistedFontScale(); // 应用持久化字号:设 qApp 基准字体 + 记录缩放(供 scaledPx)
|
||||||
|
|
||||||
|
QString themeModePreference(); // "system" | "light" | "dark"(默认 system)
|
||||||
|
void setThemeModePreference(const QString& mode); // 持久化 + 立即应用(主题可热切)
|
||||||
|
|
||||||
|
int fontScalePreference(); // 缩放百分比 90/100/115/130(默认 100)
|
||||||
|
void setFontScalePreference(int percent); // 仅持久化(字号改动重启后生效)
|
||||||
|
|
||||||
|
int scaledPx(int basePx); // basePx × 当前字号% / 100(内联 QSS 字号用,使自定义 chrome 也随字号缩放)
|
||||||
|
|
||||||
|
// 当前 ElaTheme 是否暗色(供内联样式判断)。
|
||||||
|
bool isDarkTheme();
|
||||||
|
|
||||||
|
// VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b(0–1)。
|
||||||
|
void vtkBackground(double& r, double& g, double& b);
|
||||||
|
|
||||||
|
// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens;规范 §1.5 + 附录 A + §1.3)──
|
||||||
|
// 组件只引语义 token,禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。
|
||||||
|
QString token(const char* name); // 当前明暗下的 hex(未知名返回品红 "#FF00FF" 以便一眼发现漏配)
|
||||||
|
QColor tokenColor(const char* name); // 同上,QColor 形式
|
||||||
|
|
||||||
|
// 把 QSS 模板里的 {{token}} 占位替换为当前明暗的 hex 后返回。
|
||||||
|
QString fillTokens(const QString& tmpl);
|
||||||
|
|
||||||
|
// 应用一段 {{token}} 模板 QSS 到 widget,并随主题切换自动重填。
|
||||||
|
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl);
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
#include "TopBar.hpp"
|
#include "TopBar.hpp"
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
#include <QActionGroup>
|
#include <QActionGroup>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFont>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -34,12 +42,12 @@ QFrame* makeDivider(QWidget* parent)
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右侧图标按钮(仅图标,悬停显示文本)。
|
// 右侧图标按钮(QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #iconBtn QSS 给)。
|
||||||
QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip)
|
QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip)
|
||||||
{
|
{
|
||||||
auto* btn = new QToolButton(parent);
|
auto* btn = new QToolButton(parent);
|
||||||
btn->setObjectName(QStringLiteral("iconBtn"));
|
btn->setObjectName(QStringLiteral("iconBtn"));
|
||||||
btn->setIcon(makeGlyph(g, QColor("#5A6B85"), kToolIcon));
|
setThemedGlyph(btn, icon, kToolIcon);
|
||||||
btn->setIconSize(QSize(kToolIcon, kToolIcon));
|
btn->setIconSize(QSize(kToolIcon, kToolIcon));
|
||||||
btn->setToolTip(tip);
|
btn->setToolTip(tip);
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
|
@ -47,6 +55,29 @@ QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip)
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 圆形头像图标:强调色填充 + 白色缩写。2x 绘制保证高 DPI 清晰。
|
||||||
|
QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QColor& fg)
|
||||||
|
{
|
||||||
|
constexpr int kScale = 2;
|
||||||
|
const int s = px * kScale;
|
||||||
|
QPixmap pm(s, s);
|
||||||
|
pm.fill(Qt::transparent);
|
||||||
|
QPainter p(&pm);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(bg);
|
||||||
|
p.drawEllipse(0, 0, s, s);
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(static_cast<int>(s * 0.4));
|
||||||
|
f.setBold(true);
|
||||||
|
p.setFont(f);
|
||||||
|
p.setPen(fg);
|
||||||
|
p.drawText(QRect(0, 0, s, s), Qt::AlignCenter, initials);
|
||||||
|
p.end();
|
||||||
|
pm.setDevicePixelRatio(kScale);
|
||||||
|
return pm;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
|
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
|
||||||
QMenu* buildViewMenu(QWidget* p)
|
QMenu* buildViewMenu(QWidget* p)
|
||||||
{
|
{
|
||||||
|
|
@ -114,12 +145,7 @@ QWidget* buildMenuBar(QWidget* parent)
|
||||||
{
|
{
|
||||||
auto* mb = new QMenuBar(parent);
|
auto* mb = new QMenuBar(parent);
|
||||||
mb->setObjectName(QStringLiteral("appMenuBar"));
|
mb->setObjectName(QStringLiteral("appMenuBar"));
|
||||||
// 自带样式(覆盖全局),加大字号/内边距,专业观感。
|
// ElaMenuBar 自绘 Fluent 外观并自动随 ElaTheme 明暗,不再写内联 QSS。
|
||||||
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(buildViewMenu(mb));
|
||||||
mb->addMenu(buildProjectMenu(mb));
|
mb->addMenu(buildProjectMenu(mb));
|
||||||
mb->addMenu(buildToolsMenu(mb));
|
mb->addMenu(buildToolsMenu(mb));
|
||||||
|
|
@ -130,33 +156,52 @@ QWidget* buildMenuBar(QWidget* parent)
|
||||||
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
setObjectName(QStringLiteral("appToolBar"));
|
setObjectName(QStringLiteral("appToolBar"));
|
||||||
setFixedHeight(56);
|
setFixedHeight(56);
|
||||||
setStyleSheet(QStringLiteral(
|
// 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、
|
||||||
"#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }"
|
// 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。
|
||||||
"#topDivider { color:#E1E6EE; }"
|
// 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent,不再写它们的 QSS。
|
||||||
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
|
// 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 {{text/on-primary}} 令牌。
|
||||||
" font-size:14px; font-weight:600; }"
|
// 切换器下拉箭头:用生成的高清 chevron PNG 作 menu-indicator(替代旧的粗糙文字箭头),中性灰双主题可读。
|
||||||
"#wsSwitcher:hover { background:#EEF3FB; }"
|
const QString chevron = geopro::app::writeChevronIcon(true, QColor("#7C8493"));
|
||||||
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
|
this, QStringLiteral(
|
||||||
|
"#appToolBar { background:{{bg/header}}; border-bottom:1px solid {{divider}}; }"
|
||||||
|
"#topDivider { color:{{divider}}; }"
|
||||||
"QToolButton::menu-indicator { image:none; }"
|
"QToolButton::menu-indicator { image:none; }"
|
||||||
"#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:700;"
|
"#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 26px 8px 12px;"
|
||||||
" font-size:13px; }"
|
" font-size:%6px; font-weight:%4; }"
|
||||||
"#userName { color:#1F2A3D; font-size:13px; font-weight:600; }"
|
"#wsSwitcher:hover { background:{{bg/hover}}; }"
|
||||||
"#userRole { color:#8A93A3; font-size:11px; }"));
|
"#wsSwitcher::menu-indicator { image:url(%7); width:13px; height:13px;"
|
||||||
|
" subcontrol-position: right center; subcontrol-origin: padding; right:8px; }"
|
||||||
|
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
|
||||||
|
"QToolButton#iconBtn:hover { background:{{bg/hover}}; }"
|
||||||
|
"#userBtn { border:none; border-radius:8px; padding:4px 10px 4px 6px;"
|
||||||
|
" color:{{text/primary}}; font-size:%3px; }"
|
||||||
|
"#userBtn:hover { background:{{bg/hover}}; }"
|
||||||
|
"#userBtn::menu-indicator { image:none; }"
|
||||||
|
"#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;"
|
||||||
|
" font-size:%1px; }"
|
||||||
|
"#userName { color:{{text/primary}}; font-size:%3px; font-weight:%4; }"
|
||||||
|
"#userRole { color:{{text/tertiary}}; font-size:%5px; }")
|
||||||
|
.arg(scaledPx(type::kBody))
|
||||||
|
.arg(type::kWeightBold)
|
||||||
|
.arg(scaledPx(type::kLabel))
|
||||||
|
.arg(type::kWeightSemibold)
|
||||||
|
.arg(scaledPx(type::kCaption))
|
||||||
|
.arg(scaledPx(type::kTitle))
|
||||||
|
.arg(chevron));
|
||||||
|
|
||||||
auto* lay = new QHBoxLayout(this);
|
auto* lay = new QHBoxLayout(this);
|
||||||
lay->setContentsMargins(14, 0, 14, 0);
|
lay->setContentsMargins(14, 0, 14, 0);
|
||||||
lay->setSpacing(0);
|
lay->setSpacing(0);
|
||||||
|
|
||||||
// 工作空间切换器(数据驱动;初始占位文本,待 setWorkspaces 填充)。
|
// 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用高清 chevron menu-indicator;数据驱动)。
|
||||||
wsBtn_ = new QToolButton(this);
|
wsBtn_ = new QToolButton(this);
|
||||||
wsBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
wsBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
||||||
wsBtn_->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), kWorkspaceIcon));
|
setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon, kGlyphTextGapPad); // 图标→文字6px(§6.7)
|
||||||
wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
|
||||||
wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
wsBtn_->setPopupMode(QToolButton::InstantPopup);
|
wsBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||||
wsBtn_->setCursor(Qt::PointingHandCursor);
|
wsBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
wsBtn_->setText(QStringLiteral("(加载中…)"));
|
wsBtn_->setText(QStringLiteral("正在加载工作空间…"));
|
||||||
wsBtn_->setMenu(new QMenu(wsBtn_));
|
wsBtn_->setMenu(new QMenu(wsBtn_));
|
||||||
lay->addWidget(wsBtn_);
|
lay->addWidget(wsBtn_);
|
||||||
|
|
||||||
|
|
@ -164,15 +209,14 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
lay->addWidget(makeDivider(this));
|
lay->addWidget(makeDivider(this));
|
||||||
lay->addSpacing(10);
|
lay->addSpacing(10);
|
||||||
|
|
||||||
// 项目切换器(数据驱动)。
|
// 项目切换器(QToolButton + 主题化 QSS;数据驱动)。
|
||||||
projBtn_ = new QToolButton(this);
|
projBtn_ = new QToolButton(this);
|
||||||
projBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
projBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
||||||
projBtn_->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), kWorkspaceIcon));
|
setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon, kGlyphTextGapPad); // 中性主题色 + 图标→文字6px
|
||||||
projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
|
||||||
projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
projBtn_->setPopupMode(QToolButton::InstantPopup);
|
projBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||||
projBtn_->setCursor(Qt::PointingHandCursor);
|
projBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
projBtn_->setText(QStringLiteral("(加载中…)"));
|
projBtn_->setText(QStringLiteral("正在加载项目…"));
|
||||||
projBtn_->setMenu(new QMenu(projBtn_));
|
projBtn_->setMenu(new QMenu(projBtn_));
|
||||||
lay->addWidget(projBtn_);
|
lay->addWidget(projBtn_);
|
||||||
|
|
||||||
|
|
@ -180,30 +224,73 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
||||||
lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助")));
|
lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助")));
|
||||||
lay->addWidget(makeIconButton(this, Glyph::Bell, QStringLiteral("通知")));
|
lay->addWidget(makeIconButton(this, Glyph::Bell, QStringLiteral("通知")));
|
||||||
lay->addWidget(makeIconButton(this, Glyph::Gear, QStringLiteral("设置")));
|
auto* gearBtn = makeIconButton(this, Glyph::Gear, QStringLiteral("设置"));
|
||||||
|
if (auto* gb = qobject_cast<QAbstractButton*>(gearBtn))
|
||||||
|
QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); });
|
||||||
|
lay->addWidget(gearBtn);
|
||||||
lay->addSpacing(10);
|
lay->addSpacing(10);
|
||||||
lay->addWidget(makeDivider(this));
|
lay->addWidget(makeDivider(this));
|
||||||
lay->addSpacing(12);
|
lay->addSpacing(12);
|
||||||
|
|
||||||
// 用户区(本轮静态)。
|
// 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。
|
||||||
auto* avatar = new QLabel(QStringLiteral("ZL"), this);
|
// 用普通 QWidget + eventFilter:QWidget 按子布局正确撑开(QPushButton 装布局会按空文字算尺寸挤成一团)。
|
||||||
avatar->setObjectName(QStringLiteral("avatar"));
|
userRow_ = new QWidget(this);
|
||||||
avatar->setFixedSize(34, 34);
|
userRow_->setObjectName(QStringLiteral("userBtn"));
|
||||||
avatar->setAlignment(Qt::AlignCenter);
|
userRow_->setAttribute(Qt::WA_StyledBackground, true); // 令 QSS 背景(hover)在 QWidget 上生效
|
||||||
lay->addWidget(avatar);
|
userRow_->setCursor(Qt::PointingHandCursor);
|
||||||
lay->addSpacing(8);
|
userRow_->installEventFilter(this);
|
||||||
|
auto* uLay = new QHBoxLayout(userRow_);
|
||||||
|
uLay->setContentsMargins(8, 3, 8, 3);
|
||||||
|
uLay->setSpacing(10);
|
||||||
|
|
||||||
auto* userBox = new QWidget(this);
|
auto* avatar = new QLabel(userRow_);
|
||||||
auto* userLay = new QVBoxLayout(userBox);
|
avatar->setPixmap(
|
||||||
userLay->setContentsMargins(0, 0, 0, 0);
|
renderAvatar(QStringLiteral("ZL"), 34, geopro::app::tokenColor("accent/primary"), Qt::white));
|
||||||
userLay->setSpacing(0);
|
avatar->setFixedSize(34, 34);
|
||||||
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
|
avatar->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
uLay->addWidget(avatar, 0, Qt::AlignVCenter);
|
||||||
|
|
||||||
|
auto* nameBox = new QWidget(userRow_);
|
||||||
|
nameBox->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
auto* nameLay = new QVBoxLayout(nameBox);
|
||||||
|
nameLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
nameLay->setSpacing(0);
|
||||||
|
auto* userName = new QLabel(QStringLiteral("张磊"), nameBox);
|
||||||
userName->setObjectName(QStringLiteral("userName"));
|
userName->setObjectName(QStringLiteral("userName"));
|
||||||
auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBox);
|
auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox);
|
||||||
userRole->setObjectName(QStringLiteral("userRole"));
|
userRole->setObjectName(QStringLiteral("userRole"));
|
||||||
userLay->addWidget(userName);
|
nameLay->addWidget(userName);
|
||||||
userLay->addWidget(userRole);
|
nameLay->addWidget(userRole);
|
||||||
lay->addWidget(userBox);
|
uLay->addWidget(nameBox, 0, Qt::AlignVCenter);
|
||||||
|
|
||||||
|
auto* chevronLbl = new QLabel(userRow_);
|
||||||
|
chevronLbl->setPixmap(QPixmap(geopro::app::writeChevronIcon(true, QColor("#7C8493")))
|
||||||
|
.scaled(12, 12, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
chevronLbl->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
uLay->addWidget(chevronLbl, 0, Qt::AlignVCenter);
|
||||||
|
|
||||||
|
// 下拉菜单(加宽):账户 / 个人资料 / 偏好设置 / API 密钥 / 退出登录。
|
||||||
|
userMenu_ = new QMenu(this);
|
||||||
|
userMenu_->setMinimumWidth(200);
|
||||||
|
userMenu_->addAction(QStringLiteral("账户"));
|
||||||
|
userMenu_->addAction(QStringLiteral("个人资料"));
|
||||||
|
QObject::connect(userMenu_->addAction(QStringLiteral("偏好设置")), &QAction::triggered, this,
|
||||||
|
[this] { emit settingsRequested(); });
|
||||||
|
userMenu_->addAction(QStringLiteral("API 密钥"));
|
||||||
|
userMenu_->addSeparator();
|
||||||
|
QObject::connect(userMenu_->addAction(QStringLiteral("退出登录")), &QAction::triggered, this,
|
||||||
|
[this] { emit logoutRequested(); });
|
||||||
|
|
||||||
|
lay->addWidget(userRow_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
|
||||||
|
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
|
||||||
|
if (userMenu_)
|
||||||
|
userMenu_->exec(userRow_->mapToGlobal(QPoint(0, userRow_->height() + 2)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(obj, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
|
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
|
||||||
|
|
@ -223,7 +310,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
||||||
group->addAction(a);
|
group->addAction(a);
|
||||||
if (id == currentId) currentName = name;
|
if (id == currentId) currentName = name;
|
||||||
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||||
wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈
|
wsBtn_->setText(name); // 立即反馈
|
||||||
emit workspaceSwitchRequested(id);
|
emit workspaceSwitchRequested(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -232,8 +319,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
||||||
none->setEnabled(false);
|
none->setEnabled(false);
|
||||||
}
|
}
|
||||||
wsBtn_->setMenu(menu);
|
wsBtn_->setMenu(menu);
|
||||||
wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) +
|
wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName);
|
||||||
QStringLiteral(" ▾"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
|
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
|
||||||
|
|
@ -254,7 +340,7 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QS
|
||||||
group->addAction(a);
|
group->addAction(a);
|
||||||
if (id == currentId) currentName = name;
|
if (id == currentId) currentName = name;
|
||||||
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
|
||||||
projBtn_->setText(name + QStringLiteral(" ▾"));
|
projBtn_->setText(name);
|
||||||
emit projectSwitchRequested(id);
|
emit projectSwitchRequested(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -268,12 +354,11 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QS
|
||||||
QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); });
|
QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); });
|
||||||
}
|
}
|
||||||
projBtn_->setMenu(menu);
|
projBtn_->setMenu(menu);
|
||||||
projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) +
|
projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName);
|
||||||
QStringLiteral(" ▾"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TopBar::setProjectButtonText(const QString& name) {
|
void TopBar::setProjectButtonText(const QString& name) {
|
||||||
projBtn_->setText(name + QStringLiteral(" ▾"));
|
projBtn_->setText(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include "repo/RepoTypes.hpp"
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
|
class QEvent;
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -21,14 +23,21 @@ public:
|
||||||
bool hasMore);
|
bool hasMore);
|
||||||
void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字
|
void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* obj, QEvent* event) override; // 用户区整块可点 → 弹菜单
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void workspaceSwitchRequested(const QString& tenantId);
|
void workspaceSwitchRequested(const QString& tenantId);
|
||||||
void projectSwitchRequested(const QString& projectId);
|
void projectSwitchRequested(const QString& projectId);
|
||||||
void allProjectsRequested(); // 点击"全部项目…"
|
void allProjectsRequested(); // 点击"全部项目…"
|
||||||
|
void logoutRequested(); // 头像菜单「退出登录」
|
||||||
|
void settingsRequested(); // 点击齿轮图标 → 打开设置
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QToolButton* wsBtn_ = nullptr;
|
QToolButton* wsBtn_ = nullptr;
|
||||||
QToolButton* projBtn_ = nullptr;
|
QToolButton* projBtn_ = nullptr;
|
||||||
|
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
|
||||||
|
QMenu* userMenu_ = nullptr; // 用户下拉菜单
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,23 @@
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QEasingCurve>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QGraphicsOpacityEffect>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "AuthService.hpp"
|
#include "AuthService.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -82,21 +86,25 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
|
|
||||||
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
|
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
|
||||||
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
|
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
|
||||||
setStyleSheet(QStringLiteral(
|
// 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。
|
||||||
"QDialog { background: #F4F6FA; }"
|
// 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。
|
||||||
|
// 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
this, QStringLiteral(
|
||||||
|
"QDialog { background: {{bg/app}}; }"
|
||||||
"#headerBand {"
|
"#headerBand {"
|
||||||
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
|
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
|
||||||
" stop:0 #2D6CB5, stop:1 #234F87); }"
|
" stop:0 {{accent/primary}}, stop:1 {{accent/primary-pressed}}); }"
|
||||||
"#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }"
|
"#brandTitle { color: {{text/on-primary}}; font-size: %1px; font-weight: %2; }"
|
||||||
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }"
|
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
|
||||||
"#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }"
|
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
|
||||||
"QLineEdit {"
|
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
|
||||||
" background: #FFFFFF; color: #1F2A3D;"
|
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
|
||||||
" border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
|
.arg(scaledPx(type::kDisplay))
|
||||||
" selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
|
.arg(type::kWeightBold)
|
||||||
"QLineEdit:focus { border: 1px solid #2D6CB5; }"
|
.arg(scaledPx(type::kCaption))
|
||||||
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
|
.arg(scaledPx(type::kCaption))
|
||||||
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }"));
|
.arg(type::kWeightSemibold));
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(this);
|
auto* root = new QVBoxLayout(this);
|
||||||
root->setContentsMargins(0, 0, 0, 0);
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
@ -122,8 +130,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
// ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
|
// ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
|
||||||
auto* body = new QWidget(this);
|
auto* body = new QWidget(this);
|
||||||
auto* form = new QVBoxLayout(body);
|
auto* form = new QVBoxLayout(body);
|
||||||
form->setContentsMargins(32, 24, 32, 26);
|
// 表单边距取间距令牌:左右 xxxl(32)、上下 xxl(24),对称(原底部 26 是手调奇数)。
|
||||||
form->setSpacing(6);
|
form->setContentsMargins(space::kXxxl, space::kXxl, space::kXxxl, space::kXxl);
|
||||||
|
form->setSpacing(space::kSm);
|
||||||
|
|
||||||
// 统一字段构造:小号muted标签 + 40px 高输入框 + 字段间距。
|
// 统一字段构造:小号muted标签 + 40px 高输入框 + 字段间距。
|
||||||
auto addField = [&](const QString& labelText, QLineEdit* edit) {
|
auto addField = [&](const QString& labelText, QLineEdit* edit) {
|
||||||
|
|
@ -169,21 +178,23 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
|
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
|
||||||
refreshBtn_->setFlat(true);
|
refreshBtn_->setFlat(true);
|
||||||
refreshBtn_->setCursor(Qt::PointingHandCursor);
|
refreshBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
refreshBtn_->setStyleSheet(QStringLiteral(
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
"QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
|
refreshBtn_,
|
||||||
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
|
QStringLiteral(
|
||||||
|
"QPushButton { color: {{accent/primary}}; border: none; background: transparent; padding: 2px 0; }"
|
||||||
|
"QPushButton:hover { color: {{accent/primary-pressed}}; text-decoration: underline; }"));
|
||||||
refreshRow->addWidget(refreshBtn_);
|
refreshRow->addWidget(refreshBtn_);
|
||||||
form->addLayout(refreshRow);
|
form->addLayout(refreshRow);
|
||||||
|
|
||||||
// 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。
|
// 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。
|
||||||
rememberChk_ = new QCheckBox(QStringLiteral("记住登录(30 天内免登录)"), body);
|
rememberChk_ = new QCheckBox(QStringLiteral("记住登录(30 天内免登录)"), body);
|
||||||
rememberChk_->setCursor(Qt::PointingHandCursor);
|
rememberChk_->setCursor(Qt::PointingHandCursor); // ElaCheckBox 自绘 Fluent + 自动明暗
|
||||||
rememberChk_->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:13px;"));
|
|
||||||
form->addWidget(rememberChk_);
|
form->addWidget(rememberChk_);
|
||||||
|
|
||||||
// 错误提示:固定占位高度,避免出现时整体布局跳动。
|
// 错误提示:固定占位高度,避免出现时整体布局跳动。
|
||||||
errorLabel_ = new QLabel(body);
|
errorLabel_ = new QLabel(body);
|
||||||
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
errorLabel_, QStringLiteral("color: {{status/danger}}; font-size: %1px;").arg(scaledPx(type::kCaption)));
|
||||||
errorLabel_->setWordWrap(true);
|
errorLabel_->setWordWrap(true);
|
||||||
errorLabel_->setMinimumHeight(18);
|
errorLabel_->setMinimumHeight(18);
|
||||||
form->addWidget(errorLabel_);
|
form->addWidget(errorLabel_);
|
||||||
|
|
@ -191,15 +202,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
form->addStretch();
|
form->addStretch();
|
||||||
|
|
||||||
// 主操作:满宽强调主按钮(von Restorff:唯一高强调元素引导主流程)。
|
// 主操作:满宽强调主按钮(von Restorff:唯一高强调元素引导主流程)。
|
||||||
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body);
|
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body); // Fluent 主按钮(自动明暗)
|
||||||
loginBtn_->setMinimumHeight(44);
|
loginBtn_->setMinimumHeight(44);
|
||||||
loginBtn_->setCursor(Qt::PointingHandCursor);
|
loginBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
loginBtn_->setStyleSheet(QStringLiteral(
|
|
||||||
"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);
|
loginBtn_->setDefault(true);
|
||||||
form->addWidget(loginBtn_);
|
form->addWidget(loginBtn_);
|
||||||
|
|
||||||
|
|
@ -279,6 +284,20 @@ bool LoginWindow::remember() const
|
||||||
void LoginWindow::showError(const QString& msg)
|
void LoginWindow::showError(const QString& msg)
|
||||||
{
|
{
|
||||||
errorLabel_->setText(msg);
|
errorLabel_->setText(msg);
|
||||||
|
|
||||||
|
// 错误淡入:柔化失败时刻(仅透明度 200ms;errorLabel_ 已预留固定高度,
|
||||||
|
// 不引发布局跳动)。复用同一 opacity effect,重复报错每次重新淡入。
|
||||||
|
auto* fx = qobject_cast<QGraphicsOpacityEffect*>(errorLabel_->graphicsEffect());
|
||||||
|
if (!fx) {
|
||||||
|
fx = new QGraphicsOpacityEffect(errorLabel_);
|
||||||
|
errorLabel_->setGraphicsEffect(fx);
|
||||||
|
}
|
||||||
|
auto* anim = new QPropertyAnimation(fx, "opacity", errorLabel_);
|
||||||
|
anim->setDuration(200);
|
||||||
|
anim->setStartValue(0.0);
|
||||||
|
anim->setEndValue(1.0);
|
||||||
|
anim->setEasingCurve(QEasingCurve::OutQuad);
|
||||||
|
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
461
src/app/main.cpp
461
src/app/main.cpp
|
|
@ -28,18 +28,32 @@
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QEasingCurve>
|
||||||
|
#include <QEvent>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QButtonGroup>
|
||||||
|
#include <QCheckBox>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QGraphicsOpacityEffect>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QProcess>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QShortcut>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QVariantAnimation>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QStatusBar>
|
#include <QStatusBar>
|
||||||
|
#include <QStyle>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
|
#include <QTimer>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
#include <QTreeWidgetItem>
|
#include <QTreeWidgetItem>
|
||||||
|
|
@ -51,6 +65,7 @@
|
||||||
#include <DockManager.h>
|
#include <DockManager.h>
|
||||||
#include <DockWidget.h>
|
#include <DockWidget.h>
|
||||||
|
|
||||||
|
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
#include "repo/LocalSampleRepository.hpp"
|
#include "repo/LocalSampleRepository.hpp"
|
||||||
|
|
@ -61,6 +76,7 @@
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
#include "PanelHeader.hpp"
|
#include "PanelHeader.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
#include "SettingsDialog.hpp"
|
||||||
#include "TopBar.hpp"
|
#include "TopBar.hpp"
|
||||||
#include "CentralScene.hpp"
|
#include "CentralScene.hpp"
|
||||||
#include "ProjectListDialog.hpp"
|
#include "ProjectListDialog.hpp"
|
||||||
|
|
@ -93,15 +109,90 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVTKOpenGLStereoWidget.h>
|
#include <QVTKOpenGLStereoWidget.h>
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkCamera.h>
|
||||||
|
#include <vtkCameraInterpolator.h>
|
||||||
#include <vtkGenericOpenGLRenderWindow.h>
|
#include <vtkGenericOpenGLRenderWindow.h>
|
||||||
#include <vtkImagePlaneWidget.h>
|
#include <vtkImagePlaneWidget.h>
|
||||||
#include <vtkLookupTable.h>
|
#include <vtkLookupTable.h>
|
||||||
|
#include <vtkProperty.h>
|
||||||
#include <vtkRenderWindowInteractor.h>
|
#include <vtkRenderWindowInteractor.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
#include <vtkSmartPointer.h>
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// 居中浮层定位器:监视 host(中央 QVTK)尺寸/显示变化,把 overlay 浮层
|
||||||
|
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
|
||||||
|
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
|
||||||
|
class CenterOverlay : public QObject {
|
||||||
|
public:
|
||||||
|
CenterOverlay(QWidget* overlay, QWidget* host)
|
||||||
|
: QObject(host), overlay_(overlay), host_(host)
|
||||||
|
{
|
||||||
|
host_->installEventFilter(this);
|
||||||
|
}
|
||||||
|
void reposition()
|
||||||
|
{
|
||||||
|
overlay_->adjustSize();
|
||||||
|
const QSize h = host_->size();
|
||||||
|
const QSize o = overlay_->size();
|
||||||
|
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
|
||||||
|
host_->y() + (h.height() - o.height()) / 2);
|
||||||
|
overlay_->raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* obj, QEvent* e) override
|
||||||
|
{
|
||||||
|
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
|
||||||
|
reposition();
|
||||||
|
return QObject::eventFilter(obj, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWidget* overlay_;
|
||||||
|
QWidget* host_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。
|
||||||
|
// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve),单条 QVariantAnimation
|
||||||
|
// 逐帧驱动并 Render;结束回调锁定到目标态(防插值末值误差/残留半透明)。
|
||||||
|
// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。
|
||||||
|
void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw,
|
||||||
|
vtkSmartPointer<vtkCamera> fromCam, vtkSmartPointer<vtkCamera> toCam,
|
||||||
|
std::vector<vtkSmartPointer<vtkActor>> actors, int durationMs, QObject* owner)
|
||||||
|
{
|
||||||
|
auto interp = vtkSmartPointer<vtkCameraInterpolator>::New();
|
||||||
|
interp->SetInterpolationTypeToLinear();
|
||||||
|
interp->AddCamera(0.0, fromCam);
|
||||||
|
interp->AddCamera(1.0, toCam);
|
||||||
|
|
||||||
|
auto* anim = new QVariantAnimation(owner);
|
||||||
|
anim->setDuration(durationMs);
|
||||||
|
anim->setStartValue(0.0);
|
||||||
|
anim->setEndValue(1.0);
|
||||||
|
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||||
|
QObject::connect(anim, &QVariantAnimation::valueChanged, owner,
|
||||||
|
[interp, renderer, rw, actors](const QVariant& v) {
|
||||||
|
const double t = v.toDouble();
|
||||||
|
interp->InterpolateCamera(t, renderer->GetActiveCamera());
|
||||||
|
for (const auto& a : actors)
|
||||||
|
if (a) a->GetProperty()->SetOpacity(t);
|
||||||
|
renderer->ResetCameraClippingRange();
|
||||||
|
rw->Render();
|
||||||
|
});
|
||||||
|
QObject::connect(anim, &QVariantAnimation::finished, owner,
|
||||||
|
[renderer, rw, actors, toCam]() {
|
||||||
|
renderer->GetActiveCamera()->DeepCopy(toCam);
|
||||||
|
for (const auto& a : actors)
|
||||||
|
if (a) a->GetProperty()->SetOpacity(1.0);
|
||||||
|
renderer->ResetCameraClippingRange();
|
||||||
|
rw->Render();
|
||||||
|
});
|
||||||
|
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||||||
std::string readPem(const std::string& path)
|
std::string readPem(const std::string& path)
|
||||||
{
|
{
|
||||||
|
|
@ -198,12 +289,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
window.setCentralWidget(dockManager);
|
window.setCentralWidget(dockManager);
|
||||||
|
|
||||||
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
||||||
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。这里在其后追加同选择器规则覆盖为极淡分隔。
|
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
|
||||||
|
// 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。
|
||||||
|
const QString dockBaseQss = dockManager->styleSheet();
|
||||||
|
auto applyDockSplitter = [dockManager, dockBaseQss]() {
|
||||||
dockManager->setStyleSheet(
|
dockManager->setStyleSheet(
|
||||||
dockManager->styleSheet() +
|
dockBaseQss +
|
||||||
QStringLiteral(
|
geopro::app::fillTokens(QStringLiteral(
|
||||||
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }"
|
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: {{divider}}; }"
|
||||||
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"));
|
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: {{accent/primary}}; }")));
|
||||||
|
};
|
||||||
|
applyDockSplitter();
|
||||||
|
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
|
||||||
|
dockManager, [applyDockSplitter]() { applyDockSplitter(); });
|
||||||
|
|
||||||
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮),ADS 自带标题栏随后隐藏,
|
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮),ADS 自带标题栏随后隐藏,
|
||||||
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
|
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
|
||||||
|
|
@ -224,34 +322,56 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
centerLayout->setContentsMargins(0, 0, 0, 0);
|
centerLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
centerLayout->setSpacing(0);
|
centerLayout->setSpacing(0);
|
||||||
|
|
||||||
// 工具条:「二维地图/三维视图」两个互斥可勾选 action。切换=按当前勾选集重建对应内容。默认二维地图。
|
// 分段工具条按钮样式(QToolButton + 主题化 QSS):选中=强调色文字 + 强调色下划线,明暗都清晰。
|
||||||
auto* viewToolBar = new QToolBar();
|
// ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。
|
||||||
auto* viewGroup = new QActionGroup(viewToolBar);
|
const QString kBarBtnQss =
|
||||||
viewGroup->setExclusive(true);
|
QStringLiteral(
|
||||||
auto* act2D = viewToolBar->addAction(QStringLiteral("二维地图"));
|
"QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:{{text/primary}};"
|
||||||
auto* act3D = viewToolBar->addAction(QStringLiteral("三维视图"));
|
" font-size:%1px; }"
|
||||||
act2D->setCheckable(true);
|
"QToolButton:hover{ background:{{bg/hover}}; }"
|
||||||
act3D->setCheckable(true);
|
"QToolButton:checked{ color:{{accent/primary}}; font-weight:%2;"
|
||||||
viewGroup->addAction(act2D);
|
" border-bottom:2px solid {{accent/primary}}; }"
|
||||||
viewGroup->addAction(act3D);
|
"QToolButton#dataTab{ border:none; border-radius:0; background:transparent;"
|
||||||
act2D->setChecked(true); // 默认二维地图
|
" border-bottom:2px solid transparent; color:{{text/secondary}}; padding:8px 8px; }"
|
||||||
centerLayout->addWidget(viewToolBar);
|
"QToolButton#dataTab:hover{ color:{{text/primary}}; background:transparent; }"
|
||||||
|
"QToolButton#dataTab:checked{ color:{{accent/primary}}; font-weight:%2;"
|
||||||
|
" border-bottom:2px solid {{accent/primary}}; }")
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kBody))
|
||||||
|
.arg(geopro::app::type::kWeightSemibold);
|
||||||
|
|
||||||
|
// 「二维地图/三维视图」分段切换表头:与「异常/属性」面板表头同款(42px 表头底 + 强调色下划线页签)。
|
||||||
|
auto seg = geopro::app::buildSegmentedHeader(
|
||||||
|
{QStringLiteral("二维地图"), QStringLiteral("三维视图")},
|
||||||
|
{{geopro::app::Glyph::Collapse, QStringLiteral("折叠")},
|
||||||
|
{geopro::app::Glyph::Download, QStringLiteral("导出")}});
|
||||||
|
auto* viewHeader = seg.header;
|
||||||
|
auto* act2D = seg.buttons[0];
|
||||||
|
auto* act3D = seg.buttons[1];
|
||||||
|
centerLayout->addWidget(viewHeader);
|
||||||
centerLayout->addWidget(vtkWidget, 1);
|
centerLayout->addWidget(vtkWidget, 1);
|
||||||
|
|
||||||
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
|
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
|
||||||
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
|
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
|
||||||
auto* layerPanel = new QFrame(centerWidget);
|
auto* layerPanel = new QFrame(centerWidget);
|
||||||
layerPanel->setFrameShape(QFrame::StyledPanel);
|
layerPanel->setFrameShape(QFrame::StyledPanel);
|
||||||
layerPanel->setStyleSheet(
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
QStringLiteral("QFrame{background:rgba(255,255,255,0.96);border:1px solid #D5DBE5;"
|
layerPanel,
|
||||||
"border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}"
|
// 不设 border-radius:浮层是 centerWidget 的子控件,悬于原生 GL 画布上,圆角四角处会
|
||||||
"QCheckBox:disabled{color:#9AA6B6;}"));
|
// 露出父级浅底(浅色模式下即四个白直角)。改为直角矩形,不透明底铺满整块,无四角伪影。
|
||||||
|
QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};}"
|
||||||
|
"QCheckBox{padding:2px 1px;color:{{canvas/text}};}"
|
||||||
|
"QCheckBox:disabled{color:{{canvas/text-dim}};}"));
|
||||||
auto* layerLayout = new QVBoxLayout(layerPanel);
|
auto* layerLayout = new QVBoxLayout(layerPanel);
|
||||||
layerLayout->setContentsMargins(13, 10, 15, 11);
|
// 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。
|
||||||
layerLayout->setSpacing(6);
|
layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl,
|
||||||
|
geopro::app::space::kLg, geopro::app::space::kMl);
|
||||||
|
layerLayout->setSpacing(geopro::app::space::kSm);
|
||||||
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
|
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
|
||||||
layerTitle->setStyleSheet(QStringLiteral(
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
"font-weight:600;color:#2D6CB5;border:none;background:transparent;padding-bottom:3px;"));
|
layerTitle, QStringLiteral("font-weight:%1;color:{{canvas/text}};border:none;background:transparent;"
|
||||||
|
"padding-bottom:3px;font-size:%2px;")
|
||||||
|
.arg(geopro::app::type::kWeightSemibold)
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kTitle)));
|
||||||
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)"));
|
||||||
|
|
@ -278,6 +398,52 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
layerLayout->addWidget(chkTerrain);
|
layerLayout->addWidget(chkTerrain);
|
||||||
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||||||
|
|
||||||
|
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
|
||||||
|
// 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中;
|
||||||
|
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
|
||||||
|
auto* emptyState = new QFrame(centerWidget);
|
||||||
|
emptyState->setObjectName(QStringLiteral("centralEmpty"));
|
||||||
|
emptyState->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
// 背景取 canvas/bg(#0B1320)——与画布同色:在原生 GL 上覆盖透明无法生效(会回退成不透明浅底),
|
||||||
|
// 故用与画布等色的不透明底,卡片即「无缝隐形」,浅色提示字稳稳浮于深底(与左上视图详情浮层同法)。
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
emptyState, QStringLiteral("#centralEmpty { background: {{canvas/bg}}; }"
|
||||||
|
"#centralEmpty QLabel { background: transparent; }"));
|
||||||
|
auto* esLay = new QVBoxLayout(emptyState);
|
||||||
|
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
|
||||||
|
geopro::app::space::kXl, geopro::app::space::kXl);
|
||||||
|
esLay->setSpacing(geopro::app::space::kMd);
|
||||||
|
esLay->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
auto* esIcon = new QLabel(emptyState);
|
||||||
|
esIcon->setPixmap(
|
||||||
|
geopro::app::makeGlyph(geopro::app::Glyph::Dataset,
|
||||||
|
geopro::app::tokenColor("canvas/text-dim"), 56)
|
||||||
|
.pixmap(56, 56));
|
||||||
|
esIcon->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
|
||||||
|
esTitle->setAlignment(Qt::AlignCenter);
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
esTitle, QStringLiteral("color:{{canvas/text}}; font-size:%1px; font-weight:%2;")
|
||||||
|
.arg(geopro::app::scaledPx(geopro::app::type::kHeading))
|
||||||
|
.arg(geopro::app::type::kWeightSemibold));
|
||||||
|
|
||||||
|
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
|
||||||
|
"切到「三维视图」可叠加帘面、体素与地形图层"),
|
||||||
|
emptyState);
|
||||||
|
esHint->setAlignment(Qt::AlignCenter);
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
esHint,
|
||||||
|
QStringLiteral("color:{{canvas/text-dim}}; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody)));
|
||||||
|
|
||||||
|
esLay->addWidget(esIcon);
|
||||||
|
esLay->addWidget(esTitle);
|
||||||
|
esLay->addWidget(esHint);
|
||||||
|
|
||||||
|
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
|
||||||
|
emptyCentering->reposition();
|
||||||
|
|
||||||
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
||||||
vtkDock->setWidget(centerWidget);
|
vtkDock->setWidget(centerWidget);
|
||||||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||||||
|
|
@ -287,11 +453,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto* detailWidget = new QVTKOpenGLStereoWidget();
|
auto* detailWidget = new QVTKOpenGLStereoWidget();
|
||||||
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
|
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
|
||||||
vtkNew<vtkRenderer> detailRenderer;
|
vtkNew<vtkRenderer> detailRenderer;
|
||||||
detailRenderer->SetBackground(1.0, 1.0, 1.0); // 白底
|
{
|
||||||
|
double r, g, b;
|
||||||
|
geopro::app::vtkBackground(r, g, b); // 背景随主题
|
||||||
|
detailRenderer->SetBackground(r, g, b);
|
||||||
|
}
|
||||||
detailWidget->setRenderWindow(detailRenderWindow);
|
detailWidget->setRenderWindow(detailRenderWindow);
|
||||||
detailRenderWindow->AddRenderer(detailRenderer);
|
detailRenderWindow->AddRenderer(detailRenderer);
|
||||||
vtkRenderer* detailRendererPtr = detailRenderer.Get();
|
vtkRenderer* detailRendererPtr = detailRenderer.Get();
|
||||||
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
|
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
|
||||||
|
// 注:VTK 背景随主题切换的连接放在 rebuildCentral/rebuildDetail 定义之后(直接重跑它们,
|
||||||
|
// 走完整渲染路径必重绘,比手动 SetBackground+Render 稳)。
|
||||||
|
|
||||||
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
|
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
|
||||||
auto* detailContainer = new QWidget();
|
auto* detailContainer = new QWidget();
|
||||||
|
|
@ -299,27 +471,46 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
detailLayout->setContentsMargins(0, 0, 0, 0);
|
detailLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
detailLayout->setSpacing(0);
|
detailLayout->setSpacing(0);
|
||||||
|
|
||||||
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常」开关。
|
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关。
|
||||||
auto* detailToolBar = new QToolBar();
|
// QToolButton + 主题化 QSS(选中=强调色文字+下划线,明暗都清晰)。
|
||||||
auto* detailGroup = new QActionGroup(detailToolBar);
|
auto* detailToolBar = new QWidget();
|
||||||
|
auto* detailBarLay = new QHBoxLayout(detailToolBar);
|
||||||
|
detailBarLay->setContentsMargins(8, 6, 8, 6);
|
||||||
|
detailBarLay->setSpacing(6);
|
||||||
|
auto makeBarBtn = [detailToolBar](const QString& text, bool checkable) {
|
||||||
|
auto* b = new QToolButton(detailToolBar);
|
||||||
|
b->setText(text);
|
||||||
|
b->setCheckable(checkable);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
auto* detailGroup = new QButtonGroup(detailToolBar);
|
||||||
detailGroup->setExclusive(true);
|
detailGroup->setExclusive(true);
|
||||||
auto* actScatter = detailToolBar->addAction(QStringLiteral("原数据"));
|
auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true);
|
||||||
auto* actSection = detailToolBar->addAction(QStringLiteral("网格数据"));
|
auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true);
|
||||||
actScatter->setCheckable(true);
|
actScatter->setObjectName(QStringLiteral("dataTab"));
|
||||||
actSection->setCheckable(true);
|
actSection->setObjectName(QStringLiteral("dataTab"));
|
||||||
detailGroup->addAction(actScatter);
|
detailGroup->addButton(actScatter);
|
||||||
detailGroup->addAction(actSection);
|
detailGroup->addButton(actSection);
|
||||||
|
detailBarLay->addWidget(actScatter);
|
||||||
|
detailBarLay->addWidget(actSection);
|
||||||
actSection->setChecked(true); // 默认网格数据 (#18)
|
actSection->setChecked(true); // 默认网格数据 (#18)
|
||||||
detailToolBar->addSeparator();
|
auto* barSep = new QFrame(detailToolBar);
|
||||||
auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常"));
|
barSep->setFrameShape(QFrame::VLine);
|
||||||
actShowAnomaly->setCheckable(true);
|
barSep->setObjectName(QStringLiteral("topDivider"));
|
||||||
|
detailBarLay->addSpacing(4);
|
||||||
|
detailBarLay->addWidget(barSep);
|
||||||
|
detailBarLay->addSpacing(4);
|
||||||
|
auto* actShowAnomaly = makeBarBtn(QStringLiteral("显示异常"), true);
|
||||||
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
||||||
auto* actShowElectrodes = detailToolBar->addAction(QStringLiteral("显示电极"));
|
auto* actShowElectrodes = makeBarBtn(QStringLiteral("显示电极"), true);
|
||||||
actShowElectrodes->setCheckable(true);
|
|
||||||
actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型)
|
actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型)
|
||||||
auto* actShowContour = detailToolBar->addAction(QStringLiteral("显示等值线"));
|
auto* actShowContour = makeBarBtn(QStringLiteral("显示等值线"), true);
|
||||||
actShowContour->setCheckable(true);
|
|
||||||
actShowContour->setChecked(true); // 默认显示等值线(对齐原型)
|
actShowContour->setChecked(true); // 默认显示等值线(对齐原型)
|
||||||
|
detailBarLay->addWidget(actShowAnomaly);
|
||||||
|
detailBarLay->addWidget(actShowElectrodes);
|
||||||
|
detailBarLay->addWidget(actShowContour);
|
||||||
|
detailBarLay->addStretch();
|
||||||
|
geopro::app::applyTokenizedStyleSheet(detailToolBar, kBarBtnQss);
|
||||||
detailLayout->addWidget(detailToolBar);
|
detailLayout->addWidget(detailToolBar);
|
||||||
detailLayout->addWidget(detailWidget, 1);
|
detailLayout->addWidget(detailWidget, 1);
|
||||||
|
|
||||||
|
|
@ -333,8 +524,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
|
|
||||||
// 左上 dock:对象树(真实结构:项目根 → GS → TM)。被动视图,数据由控制器推送。
|
// 左上 dock:对象树(真实结构:项目根 → GS → TM)。被动视图,数据由控制器推送。
|
||||||
auto* objectTree = new geopro::app::ObjectTreePanel();
|
auto* objectTree = new geopro::app::ObjectTreePanel();
|
||||||
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
|
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象"));
|
||||||
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"),
|
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"),
|
||||||
objectTree,
|
objectTree,
|
||||||
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
|
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
|
||||||
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
||||||
|
|
@ -342,19 +533,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||||||
auto* datasetTabs = new QTabWidget();
|
auto* datasetTabs = new QTabWidget();
|
||||||
auto* datasetList = new QListWidget();
|
auto* datasetList = new QListWidget();
|
||||||
// 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。
|
geopro::app::applyDatasetCardDelegate(datasetList);
|
||||||
datasetList->setStyleSheet(QStringLiteral(
|
|
||||||
"QListWidget{ background:#FFFFFF; border:none; outline:none; }"
|
|
||||||
"QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }"
|
|
||||||
"QListWidget::item:hover{ background:#F5F8FD; }"
|
|
||||||
"QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"));
|
|
||||||
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||||||
auto* fileList = new QListWidget();
|
auto* fileList = new QListWidget();
|
||||||
fileList->setStyleSheet(datasetList->styleSheet()); // 与数据页签同款简洁分割
|
geopro::app::applyDatasetCardDelegate(fileList);
|
||||||
datasetTabs->addTab(fileList, QStringLiteral("文件"));
|
datasetTabs->addTab(fileList, QStringLiteral("文件"));
|
||||||
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
|
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据集"));
|
||||||
auto* datasetBox = wrapWithHeader(
|
auto* datasetBox = wrapWithHeader(
|
||||||
geopro::app::Glyph::Dataset, QStringLiteral("数据真实显示栏"), datasetTabs,
|
geopro::app::Glyph::Dataset, QStringLiteral("数据集"), datasetTabs,
|
||||||
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
||||||
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
|
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
|
||||||
datasetDock->setWidget(datasetBox);
|
datasetDock->setWidget(datasetBox);
|
||||||
|
|
@ -364,20 +550,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
|
|
||||||
// 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
|
// 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
|
||||||
auto* anomalyList = new QListWidget();
|
auto* anomalyList = new QListWidget();
|
||||||
anomalyList->setAlternatingRowColors(true);
|
geopro::app::applyAnomalyCardDelegate(anomalyList);
|
||||||
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
|
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
|
||||||
objAttrLabel->setWordWrap(true);
|
objAttrLabel->setWordWrap(true);
|
||||||
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||||
objAttrLabel->setMargin(8);
|
objAttrLabel->setMargin(8);
|
||||||
|
|
||||||
auto anomalyPanel = geopro::app::buildTabbedPanel(
|
auto anomalyPanel = geopro::app::buildTabbedPanel(
|
||||||
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常列表"), anomalyList, true},
|
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常"), anomalyList, true},
|
||||||
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}},
|
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}},
|
||||||
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
||||||
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
|
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
|
||||||
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
|
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
|
||||||
|
// colorize(C):异常计数用语义 warning“需注意”变体(区别于普通中性计数徽标),
|
||||||
|
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
|
||||||
|
// 注:徽标的填充/显隐在 loadDataset 内(当前被 park),故此色与徽标本身同属休眠态,
|
||||||
|
// 接 dd 详情渲染那轮一并可见。
|
||||||
|
if (anomalyBadge) {
|
||||||
|
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
|
||||||
|
anomalyBadge->style()->unpolish(anomalyBadge);
|
||||||
|
anomalyBadge->style()->polish(anomalyBadge);
|
||||||
|
}
|
||||||
|
|
||||||
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
|
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性"));
|
||||||
rightDock->setWidget(anomalyPanel.container);
|
rightDock->setWidget(anomalyPanel.container);
|
||||||
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
|
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
|
||||||
|
|
||||||
|
|
@ -386,19 +581,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
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(
|
propDock->setWidget(
|
||||||
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel));
|
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propLabel));
|
||||||
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
||||||
|
|
||||||
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
||||||
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
|
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
|
||||||
// 注:AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。
|
// 抽成 lambda:ADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏,
|
||||||
|
// 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。
|
||||||
|
const auto hideDockTitleBars = [&]() {
|
||||||
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
|
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
|
||||||
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
|
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
|
||||||
if (auto* area = d->dockAreaWidget())
|
if (auto* area = d->dockAreaWidget())
|
||||||
if (auto* bar = area->titleBar()) bar->setVisible(false);
|
if (auto* bar = area->titleBar()) bar->setVisible(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
hideDockTitleBars();
|
||||||
|
|
||||||
// 中央编排已解耦到 CentralScene::rebuildCentralScene(数据驱动)。本轮空 sections → 空背景占位。
|
// 中央编排已解耦到 CentralScene::rebuildCentralScene(数据驱动)。本轮空 sections → 空背景占位。
|
||||||
// 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。
|
// 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。
|
||||||
|
|
@ -416,16 +615,34 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
|
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
|
||||||
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
|
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
|
||||||
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
|
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
|
||||||
|
auto prevDsId = std::make_shared<QString>(); // 上次渲染的 DS id:判定“切换数据集”以触发揭示过渡
|
||||||
|
|
||||||
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
||||||
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
|
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
|
||||||
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
|
// overdrive(A):仅“切换数据集”这一加载时刻播放相机补间 + actor 淡入揭示;模式/叠加层开关
|
||||||
showAnomalies, showElectrodes, showContour, hiddenAnoms]() {
|
// 属同一数据集内微调,直接落定不放动画(特殊时刻才特殊,避免每次交互都动的疲劳)。
|
||||||
|
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, detailWidget, currentDsId,
|
||||||
|
prevDsId, detailMode, showAnomalies, showElectrodes, showContour,
|
||||||
|
hiddenAnoms]() {
|
||||||
|
const bool dsChanged = (*currentDsId != *prevDsId);
|
||||||
|
const bool animate = dsChanged && !prevDsId->isEmpty() && !currentDsId->isEmpty();
|
||||||
|
|
||||||
|
// 过渡起点:清场景前先快照当前相机位姿。
|
||||||
|
auto fromCam = vtkSmartPointer<vtkCamera>::New();
|
||||||
|
fromCam->DeepCopy(detailRendererPtr->GetActiveCamera());
|
||||||
|
|
||||||
detailRendererPtr->RemoveAllViewProps();
|
detailRendererPtr->RemoveAllViewProps();
|
||||||
|
{ // 背景随主题
|
||||||
|
double r, g, b;
|
||||||
|
geopro::app::vtkBackground(r, g, b);
|
||||||
|
detailRendererPtr->SetBackground(r, g, b);
|
||||||
|
}
|
||||||
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
||||||
|
*prevDsId = *currentDsId;
|
||||||
detailRenderWindowPtr->Render();
|
detailRenderWindowPtr->Render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
std::vector<vtkSmartPointer<vtkActor>> added; // 本次加入的 actor,供淡入
|
||||||
const std::string id = currentDsId->toStdString();
|
const std::string id = currentDsId->toStdString();
|
||||||
if (*detailMode == DetailMode::Section18) {
|
if (*detailMode == DetailMode::Section18) {
|
||||||
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。
|
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。
|
||||||
|
|
@ -435,10 +652,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (actors.bands) {
|
if (actors.bands) {
|
||||||
actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0);
|
actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||||||
detailRendererPtr->AddViewProp(actors.bands);
|
detailRendererPtr->AddViewProp(actors.bands);
|
||||||
|
added.push_back(actors.bands);
|
||||||
}
|
}
|
||||||
if (actors.edges && *showContour) {
|
if (actors.edges && *showContour) {
|
||||||
actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0);
|
actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||||||
detailRendererPtr->AddViewProp(actors.edges);
|
detailRendererPtr->AddViewProp(actors.edges);
|
||||||
|
added.push_back(actors.edges);
|
||||||
}
|
}
|
||||||
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
|
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
|
||||||
if (*showElectrodes) {
|
if (*showElectrodes) {
|
||||||
|
|
@ -446,6 +665,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (elec) {
|
if (elec) {
|
||||||
elec->SetScale(1.0, kVerticalExaggeration, 1.0);
|
elec->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||||||
detailRendererPtr->AddViewProp(elec);
|
detailRendererPtr->AddViewProp(elec);
|
||||||
|
added.push_back(elec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -456,6 +676,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (a) {
|
if (a) {
|
||||||
a->SetScale(1.0, kVerticalExaggeration, 1.0);
|
a->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||||||
detailRendererPtr->AddViewProp(a);
|
detailRendererPtr->AddViewProp(a);
|
||||||
|
added.push_back(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
|
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
|
||||||
|
|
@ -466,12 +687,26 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
|
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
|
||||||
act->SetScale(1.0, kVerticalExaggeration, 1.0);
|
act->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||||||
detailRendererPtr->AddViewProp(act);
|
detailRendererPtr->AddViewProp(act);
|
||||||
|
added.push_back(act);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
geopro::render::applyTop2D(detailRendererPtr);
|
geopro::render::applyTop2D(detailRendererPtr);
|
||||||
detailRendererPtr->ResetCamera();
|
detailRendererPtr->ResetCamera();
|
||||||
|
*prevDsId = *currentDsId;
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
// 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。
|
||||||
|
auto toCam = vtkSmartPointer<vtkCamera>::New();
|
||||||
|
toCam->DeepCopy(detailRendererPtr->GetActiveCamera());
|
||||||
|
for (const auto& a : added) a->GetProperty()->SetOpacity(0.0);
|
||||||
|
detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam);
|
||||||
|
detailRendererPtr->ResetCameraClippingRange();
|
||||||
|
animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450,
|
||||||
|
detailWidget);
|
||||||
|
} else {
|
||||||
detailRenderWindowPtr->Render();
|
detailRenderWindowPtr->Render();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
|
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
|
||||||
|
|
@ -503,9 +738,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
||||||
.arg(anomalies.size()));
|
.arg(anomalies.size()));
|
||||||
};
|
};
|
||||||
(void)loadDataset; // 暂未触发:保留待下一轮真实 DS 详情渲染复用
|
// 暂未触发:保留待下一轮真实 DS 详情渲染复用。
|
||||||
|
// TODO(overdrive-A 依赖):把下面数据集单击处理改调 loadDataset(dsId, name) 接通真实详情
|
||||||
|
// 渲染后,rebuildDetail 里已就绪的“相机补间 + actor 淡入”揭示动画会在切换数据集时自动激活
|
||||||
|
// (见 rebuildDetail 的 animate 分支与 animateReveal)。在此之前该动画为休眠态、不可见。
|
||||||
|
(void)loadDataset;
|
||||||
|
|
||||||
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
|
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
|
||||||
|
// 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。
|
||||||
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
|
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
|
||||||
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
|
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
|
||||||
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
|
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
|
||||||
|
|
@ -532,38 +772,38 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
|
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
|
||||||
QObject::connect(actSection, &QAction::triggered, detailWidget,
|
QObject::connect(actSection, &QAbstractButton::clicked, detailWidget,
|
||||||
[detailMode, rebuildDetail]() {
|
[detailMode, rebuildDetail]() {
|
||||||
*detailMode = DetailMode::Section18;
|
*detailMode = DetailMode::Section18;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
QObject::connect(actScatter, &QAction::triggered, detailWidget,
|
QObject::connect(actScatter, &QAbstractButton::clicked, detailWidget,
|
||||||
[detailMode, rebuildDetail]() {
|
[detailMode, rebuildDetail]() {
|
||||||
*detailMode = DetailMode::Scatter17;
|
*detailMode = DetailMode::Scatter17;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ──
|
// ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ──
|
||||||
QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget,
|
QObject::connect(actShowAnomaly, &QAbstractButton::toggled, detailWidget,
|
||||||
[showAnomalies, rebuildDetail](bool on) {
|
[showAnomalies, rebuildDetail](bool on) {
|
||||||
*showAnomalies = on;
|
*showAnomalies = on;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
QObject::connect(actShowElectrodes, &QAction::toggled, detailWidget,
|
QObject::connect(actShowElectrodes, &QAbstractButton::toggled, detailWidget,
|
||||||
[showElectrodes, rebuildDetail](bool on) {
|
[showElectrodes, rebuildDetail](bool on) {
|
||||||
*showElectrodes = on;
|
*showElectrodes = on;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
QObject::connect(actShowContour, &QAction::toggled, detailWidget,
|
QObject::connect(actShowContour, &QAbstractButton::toggled, detailWidget,
|
||||||
[showContour, rebuildDetail](bool on) {
|
[showContour, rebuildDetail](bool on) {
|
||||||
*showContour = on;
|
*showContour = on;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
|
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
|
||||||
auto showLayerPanel = [layerPanel, viewToolBar](bool show3D) {
|
auto showLayerPanel = [layerPanel, viewHeader](bool show3D) {
|
||||||
if (show3D) {
|
if (show3D) {
|
||||||
layerPanel->move(14, viewToolBar->height() + 12);
|
layerPanel->move(14, viewHeader->height() + 12);
|
||||||
layerPanel->adjustSize();
|
layerPanel->adjustSize();
|
||||||
layerPanel->setVisible(true);
|
layerPanel->setVisible(true);
|
||||||
layerPanel->raise();
|
layerPanel->raise();
|
||||||
|
|
@ -573,13 +813,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
|
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
|
||||||
QObject::connect(act2D, &QAction::triggered, vtkWidget,
|
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
|
||||||
[viewMode, rebuildCentral, showLayerPanel]() {
|
[viewMode, rebuildCentral, showLayerPanel]() {
|
||||||
*viewMode = ViewMode::Map2D;
|
*viewMode = ViewMode::Map2D;
|
||||||
showLayerPanel(false);
|
showLayerPanel(false);
|
||||||
rebuildCentral();
|
rebuildCentral();
|
||||||
});
|
});
|
||||||
QObject::connect(act3D, &QAction::triggered, vtkWidget,
|
QObject::connect(act3D, &QAbstractButton::clicked, vtkWidget,
|
||||||
[viewMode, rebuildCentral, showLayerPanel]() {
|
[viewMode, rebuildCentral, showLayerPanel]() {
|
||||||
*viewMode = ViewMode::View3D;
|
*viewMode = ViewMode::View3D;
|
||||||
showLayerPanel(true);
|
showLayerPanel(true);
|
||||||
|
|
@ -611,6 +851,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。
|
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。
|
||||||
rebuildCentral();
|
rebuildCentral();
|
||||||
|
|
||||||
|
// VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail(走完整渲染路径、末尾必 Render,
|
||||||
|
// 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。
|
||||||
|
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window,
|
||||||
|
[rebuildCentral, rebuildDetail]() {
|
||||||
|
rebuildCentral();
|
||||||
|
rebuildDetail();
|
||||||
|
});
|
||||||
|
|
||||||
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
|
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
|
||||||
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
|
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
|
||||||
geopro::app::TopBar* topBar = nullptr;
|
geopro::app::TopBar* topBar = nullptr;
|
||||||
|
|
@ -635,11 +883,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto addLoadMore = [](QListWidget* lw, int total) {
|
auto addLoadMore = [](QListWidget* lw, int total) {
|
||||||
const int loaded = lw->count();
|
const int loaded = lw->count();
|
||||||
if (loaded < total) {
|
if (loaded < total) {
|
||||||
auto* m = new QListWidgetItem(
|
auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw);
|
||||||
QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw);
|
|
||||||
m->setData(geopro::app::kDsLoadMoreRole, true);
|
m->setData(geopro::app::kDsLoadMoreRole, true);
|
||||||
m->setTextAlignment(Qt::AlignCenter);
|
m->setTextAlignment(Qt::AlignCenter);
|
||||||
m->setForeground(QColor("#2D6CB5"));
|
|
||||||
}
|
}
|
||||||
return loaded;
|
return loaded;
|
||||||
};
|
};
|
||||||
|
|
@ -647,6 +893,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
||||||
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
|
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
|
||||||
&geopro::controller::WorkbenchNavController::switchProject);
|
&geopro::controller::WorkbenchNavController::switchProject);
|
||||||
|
// 退出登录:清除记住的凭证(QtKeychain+QSettings) → 重启应用回到登录页。
|
||||||
|
QObject::connect(topBar, &geopro::app::TopBar::logoutRequested, &window, []() {
|
||||||
|
geopro::app::forgetSession();
|
||||||
|
QProcess::startDetached(QCoreApplication::applicationFilePath(),
|
||||||
|
QCoreApplication::arguments().mid(1));
|
||||||
|
qApp->quit();
|
||||||
|
});
|
||||||
|
// 设置:点齿轮 → 打开设置对话框(外观/关于)。
|
||||||
|
QObject::connect(topBar, &geopro::app::TopBar::settingsRequested, &window, [&window]() {
|
||||||
|
geopro::app::SettingsDialog dlg(&window);
|
||||||
|
dlg.exec();
|
||||||
|
});
|
||||||
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
|
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
|
||||||
[&projectRepo, &nav, topBar, &window]() {
|
[&projectRepo, &nav, topBar, &window]() {
|
||||||
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
|
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
|
||||||
|
|
@ -677,7 +935,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
objectTree->setStructure(projectName, nodes);
|
objectTree->setStructure(projectName, nodes);
|
||||||
datasetList->clear();
|
datasetList->clear();
|
||||||
fileList->clear();
|
fileList->clear();
|
||||||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
|
||||||
datasetTabs->setTabText(0, QStringLiteral("数据"));
|
datasetTabs->setTabText(0, QStringLiteral("数据"));
|
||||||
datasetTabs->setTabText(1, QStringLiteral("文件"));
|
datasetTabs->setTabText(1, QStringLiteral("文件"));
|
||||||
});
|
});
|
||||||
|
|
@ -688,7 +946,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
removeLoadMore(datasetList);
|
removeLoadMore(datasetList);
|
||||||
geopro::app::populateDatasetList(datasetList, rows, append);
|
geopro::app::populateDatasetList(datasetList, rows, append);
|
||||||
const int loaded = addLoadMore(datasetList, total);
|
const int loaded = addLoadMore(datasetList, total);
|
||||||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
|
||||||
datasetTabs->setTabText(
|
datasetTabs->setTabText(
|
||||||
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
|
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
|
||||||
: QStringLiteral("数据"));
|
: QStringLiteral("数据"));
|
||||||
|
|
@ -713,8 +971,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (stage == QStringLiteral("structure") ||
|
if (stage == QStringLiteral("structure") ||
|
||||||
stage == QStringLiteral("projects"))
|
stage == QStringLiteral("projects"))
|
||||||
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
|
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
|
||||||
window.statusBar()->showMessage(
|
// 状态栏错误反馈:临时染 danger 红,8s 后随消息超时还原全局中性色。
|
||||||
QStringLiteral("加载失败(%1):%2").arg(stage, msg), 8000);
|
auto* sb = window.statusBar();
|
||||||
|
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
|
||||||
|
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
|
||||||
|
sb->showMessage(QStringLiteral("加载失败(%1):%2").arg(stage, msg), 8000);
|
||||||
|
QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); });
|
||||||
});
|
});
|
||||||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
|
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
|
||||||
[](bool busy) {
|
[](bool busy) {
|
||||||
|
|
@ -737,14 +999,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
const QSettings settings;
|
const QSettings settings;
|
||||||
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
|
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
|
||||||
if (!geo.isEmpty()) window.restoreGeometry(geo);
|
if (!geo.isEmpty()) window.restoreGeometry(geo);
|
||||||
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
|
// 注意:ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局,
|
||||||
if (!dockState.isEmpty()) dockManager->restoreState(dockState);
|
// 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。
|
||||||
|
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v2")).toByteArray();
|
||||||
|
if (!dockState.isEmpty()) {
|
||||||
|
dockManager->restoreState(dockState);
|
||||||
|
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
|
||||||
|
hideDockTitleBars();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 退出时保存当前布局与几何(aboutToQuit 早于 window 析构,dockManager/window 仍存活)。
|
// 退出时保存当前布局与几何(aboutToQuit 早于 window 析构,dockManager/window 仍存活)。
|
||||||
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
|
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
|
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
|
||||||
settings.setValue(QStringLiteral("ui/dockState"), dockManager->saveState());
|
settings.setValue(QStringLiteral("ui/dockState_v2"), dockManager->saveState());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -765,8 +1033,11 @@ int main(int argc, char* argv[])
|
||||||
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
||||||
|
|
||||||
// 浅色专业主题(Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。
|
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。
|
||||||
geopro::app::applyTheme(app);
|
// 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。
|
||||||
|
geopro::app::applyPersistedThemeMode();
|
||||||
|
geopro::app::applyPersistedFontScale();
|
||||||
|
geopro::app::applyThemeMode(app, geopro::app::isDarkTheme());
|
||||||
|
|
||||||
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
|
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
|
||||||
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
|
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
|
||||||
|
|
@ -813,13 +1084,25 @@ int main(int argc, char* argv[])
|
||||||
geopro::data::ApiProjectRepository projectRepo(api);
|
geopro::data::ApiProjectRepository projectRepo(api);
|
||||||
geopro::controller::WorkbenchNavController nav(projectRepo);
|
geopro::controller::WorkbenchNavController nav(projectRepo);
|
||||||
|
|
||||||
QMainWindow window;
|
// ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其
|
||||||
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
|
// setCentralWidget/setMenuWidget/statusBar 承载工作台。
|
||||||
window.resize(1280, 800);
|
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
|
||||||
window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸
|
auto* window = new QMainWindow;
|
||||||
|
window->setWindowTitle(kTitle);
|
||||||
|
window->resize(1280, 800);
|
||||||
|
window->setMinimumSize(1024, 680);
|
||||||
|
buildWorkbench(*window, repo, projectRepo, nav);
|
||||||
|
|
||||||
buildWorkbench(window, repo, projectRepo, nav);
|
// 主题桥:ThemeManager 明/暗切换 → 重应用全局 QSS+调色板(标准控件 + ADS;内联 chrome 经各自连接)。
|
||||||
window.show();
|
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
|
||||||
|
window, [&app]() { geopro::app::applyThemeMode(app, geopro::app::isDarkTheme()); });
|
||||||
|
// 主题切换快捷键 Ctrl+Shift+T(持久化;设置→外观 亦可改)。
|
||||||
|
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), window);
|
||||||
|
QObject::connect(themeSc, &QShortcut::activated, window, [] {
|
||||||
|
geopro::app::setThemeModePreference(geopro::app::isDarkTheme() ? QStringLiteral("light")
|
||||||
|
: QStringLiteral("dark"));
|
||||||
|
});
|
||||||
|
window->show();
|
||||||
|
|
||||||
nav.start(); // 进入工作台后拉真实 空间/项目/结构
|
nav.start(); // 进入工作台后拉真实 空间/项目/结构
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,26 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QIcon>
|
#include <QEvent>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
#include <QPixmap>
|
#include <QMouseEvent>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPen>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// 颜色块图标边长(像素)。
|
|
||||||
constexpr int kSwatch = 12;
|
|
||||||
|
|
||||||
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
|
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
|
||||||
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
|
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
|
||||||
QString summarize(const geopro::core::Anomaly& a)
|
QString summarize(const geopro::core::Anomaly& a)
|
||||||
|
|
@ -43,15 +47,128 @@ QString summarize(const geopro::core::Anomaly& a)
|
||||||
.arg(span, 0, 'f', 0);
|
.arg(span, 0, 'f', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap。
|
// lineColor 字符串 → QColor(兼容 "#RRGGBB" 与 "rgba(...)")。
|
||||||
QPixmap swatch(const std::string& colorStr)
|
QColor barColor(const QString& s)
|
||||||
{
|
{
|
||||||
const auto c = geopro::core::parseColor(colorStr, geopro::core::AlphaScale::Bit255);
|
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
|
||||||
QPixmap pm(kSwatch, kSwatch);
|
return QColor(c.r, c.g, c.b);
|
||||||
pm.fill(QColor(c.r, c.g, c.b));
|
|
||||||
return pm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 右侧眼睛命中区(卡片右端,竖直居中)。
|
||||||
|
QRect anomalyEyeRect(const QRect& itemRect)
|
||||||
|
{
|
||||||
|
const QRect r = itemRect.adjusted(4, 2, -4, -2);
|
||||||
|
const int sz = 22;
|
||||||
|
return QRect(r.right() - sz - 8, r.center().y() - sz / 2, sz, sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnomalyCardDelegate : public QStyledItemDelegate {
|
||||||
|
public:
|
||||||
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
|
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override
|
||||||
|
{
|
||||||
|
return QSize(0, 58);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem& opt,
|
||||||
|
const QModelIndex& idx) override
|
||||||
|
{
|
||||||
|
if (e->type() == QEvent::MouseButtonRelease) {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(e);
|
||||||
|
if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) {
|
||||||
|
const auto cur = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
|
||||||
|
model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked,
|
||||||
|
Qt::CheckStateRole);
|
||||||
|
return true; // 吃掉点击:只切显隐,不改选中
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QStyledItemDelegate::editorEvent(e, model, opt, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override
|
||||||
|
{
|
||||||
|
p->save();
|
||||||
|
p->setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
const QRect r = opt.rect.adjusted(4, 3, -4, -3);
|
||||||
|
const bool selected = opt.state & QStyle::State_Selected;
|
||||||
|
const bool hover = opt.state & QStyle::State_MouseOver;
|
||||||
|
|
||||||
|
// 卡底(hover/选中高亮)
|
||||||
|
if (selected || hover) {
|
||||||
|
QPainterPath path; path.addRoundedRect(r, 6, 6);
|
||||||
|
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
|
||||||
|
}
|
||||||
|
// 左 3px 状态色竖条(取异常自身 lineColor)
|
||||||
|
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
|
||||||
|
barColor(idx.data(kAnomalyColorRole).toString()));
|
||||||
|
|
||||||
|
const QString name = idx.data(Qt::DisplayRole).toString();
|
||||||
|
const QString type = idx.data(kAnomalyTypeRole).toString();
|
||||||
|
const QString summary = idx.data(kAnomalySummaryRole).toString();
|
||||||
|
|
||||||
|
const int left = r.left() + 14;
|
||||||
|
const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位
|
||||||
|
const int rowW = right - left;
|
||||||
|
|
||||||
|
// 第一行:名称(加粗)
|
||||||
|
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
|
||||||
|
p->setFont(nf);
|
||||||
|
p->setPen(geopro::app::tokenColor("text/primary"));
|
||||||
|
const QRect nameR(left, r.top() + 8, rowW, 20);
|
||||||
|
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
p->fontMetrics().elidedText(name, Qt::ElideRight, rowW));
|
||||||
|
|
||||||
|
// 第二行:类型胶囊 + 摘要
|
||||||
|
int x = left;
|
||||||
|
const int cy = r.top() + 38;
|
||||||
|
if (!type.isEmpty()) {
|
||||||
|
QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11));
|
||||||
|
p->setFont(pf);
|
||||||
|
const QFontMetrics fm(pf);
|
||||||
|
const int tw = fm.horizontalAdvance(type);
|
||||||
|
const int ph = fm.height() + 2;
|
||||||
|
const QRect pill(x, cy - ph / 2, tw + 12, ph);
|
||||||
|
QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0);
|
||||||
|
p->fillPath(pp, geopro::app::tokenColor("bg/hover"));
|
||||||
|
p->setPen(geopro::app::tokenColor("text/secondary"));
|
||||||
|
p->drawText(pill, Qt::AlignCenter, type);
|
||||||
|
x = pill.right() + 8;
|
||||||
|
}
|
||||||
|
if (!summary.isEmpty()) {
|
||||||
|
QFont sf = opt.font; sf.setPixelSize(geopro::app::scaledPx(11));
|
||||||
|
p->setFont(sf);
|
||||||
|
p->setPen(geopro::app::tokenColor("text/secondary"));
|
||||||
|
const QRect sumR(x, cy - 10, right - x, 20);
|
||||||
|
p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠
|
||||||
|
const bool visible =
|
||||||
|
static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked;
|
||||||
|
const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled");
|
||||||
|
const QRectF eb = anomalyEyeRect(opt.rect);
|
||||||
|
const QPointF c = eb.center();
|
||||||
|
const double w = eb.width() * 0.42, h = eb.height() * 0.24;
|
||||||
|
p->setPen(QPen(eyeCol, 1.4));
|
||||||
|
p->setBrush(Qt::NoBrush);
|
||||||
|
QPainterPath eye;
|
||||||
|
eye.moveTo(c.x() - w, c.y());
|
||||||
|
eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y());
|
||||||
|
eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y());
|
||||||
|
p->drawPath(eye);
|
||||||
|
p->setBrush(eyeCol);
|
||||||
|
p->drawEllipse(c, h * 0.95, h * 0.95);
|
||||||
|
p->setBrush(Qt::NoBrush);
|
||||||
|
if (!visible)
|
||||||
|
p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6));
|
||||||
|
|
||||||
|
p->restore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies)
|
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies)
|
||||||
|
|
@ -61,16 +178,24 @@ void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anom
|
||||||
for (std::size_t i = 0; i < anomalies.size(); ++i) {
|
for (std::size_t i = 0; i < anomalies.size(); ++i) {
|
||||||
const auto& a = anomalies[i];
|
const auto& a = anomalies[i];
|
||||||
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
|
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
|
||||||
const QString type = QString::fromStdString(a.typeName);
|
auto* item = new QListWidgetItem(name, list);
|
||||||
QString text = name;
|
|
||||||
if (!type.isEmpty()) text += QStringLiteral("(%1)").arg(type);
|
|
||||||
text += QStringLiteral("\n%1").arg(summarize(a));
|
|
||||||
|
|
||||||
auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list);
|
|
||||||
item->setData(kAnomalyIndexRole, static_cast<int>(i));
|
item->setData(kAnomalyIndexRole, static_cast<int>(i));
|
||||||
|
item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor));
|
||||||
|
item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName));
|
||||||
|
item->setData(kAnomalySummaryRole, summarize(a));
|
||||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||||
item->setCheckState(Qt::Checked); // 默认显示
|
item->setCheckState(Qt::Checked); // 默认显示
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyAnomalyCardDelegate(QListWidget* list)
|
||||||
|
{
|
||||||
|
if (!list) return;
|
||||||
|
list->setItemDelegate(new AnomalyCardDelegate(list));
|
||||||
|
list->setMouseTracking(true);
|
||||||
|
list->setSpacing(0);
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
|
||||||
|
[list]() { list->viewport()->update(); });
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ namespace geopro::app {
|
||||||
// 异常索引存于条目的 Qt::UserRole(= 在原异常 vector 中的下标,用于显隐映射)。
|
// 异常索引存于条目的 Qt::UserRole(= 在原异常 vector 中的下标,用于显隐映射)。
|
||||||
constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole
|
constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole
|
||||||
|
|
||||||
|
// 卡片委托读取的结构化角色(避免把数据塞进显示文本)。
|
||||||
|
constexpr int kAnomalyColorRole = 0x0101; // lineColor 字符串
|
||||||
|
constexpr int kAnomalyTypeRole = 0x0102; // typeName
|
||||||
|
constexpr int kAnomalySummaryRole = 0x0103; // 位置·深·尺寸 摘要
|
||||||
|
|
||||||
|
// 给异常列表套用卡片委托(左色条+名称+类型标签+摘要+右侧显隐眼睛,规范§6.3)。
|
||||||
|
void applyAnomalyCardDelegate(QListWidget* list);
|
||||||
|
|
||||||
// 用异常填充 QListWidget(对齐原型右上「异常列表」):每条目 = 颜色块图标 + 名称 +
|
// 用异常填充 QListWidget(对齐原型右上「异常列表」):每条目 = 颜色块图标 + 名称 +
|
||||||
// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。
|
// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。
|
||||||
// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。
|
// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -14,6 +20,88 @@ QString humanSize(long long b) {
|
||||||
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
|
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
|
||||||
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
|
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据/文件列表卡片委托:标题+元信息双行、悬停/选中圆角高亮 + 选中左 2px 强调竖条(规范§6.2)。
|
||||||
|
// 特殊行(加载更多 / 占位提示)退回为居中纯文本,不画卡片。
|
||||||
|
class DatasetCardDelegate : public QStyledItemDelegate {
|
||||||
|
public:
|
||||||
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
|
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override {
|
||||||
|
const bool special =
|
||||||
|
idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable);
|
||||||
|
return QSize(0, special ? 34 : 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
|
||||||
|
p->save();
|
||||||
|
p->setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
const QString disp = idx.data(Qt::DisplayRole).toString();
|
||||||
|
|
||||||
|
// 「加载更多」:居中强调色文本(hover 时加底)。
|
||||||
|
if (idx.data(kDsLoadMoreRole).toBool()) {
|
||||||
|
if (opt.state & QStyle::State_MouseOver) {
|
||||||
|
QPainterPath bgp;
|
||||||
|
bgp.addRoundedRect(opt.rect.adjusted(4, 2, -4, -2), 6, 6);
|
||||||
|
p->fillPath(bgp, geopro::app::tokenColor("bg/hover"));
|
||||||
|
}
|
||||||
|
p->setPen(geopro::app::tokenColor("accent/primary"));
|
||||||
|
p->drawText(opt.rect, Qt::AlignCenter, disp);
|
||||||
|
p->restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 占位提示行(不可选):居中淡色文本。
|
||||||
|
if (!(idx.flags() & Qt::ItemIsSelectable)) {
|
||||||
|
p->setPen(geopro::app::tokenColor("text/disabled"));
|
||||||
|
p->drawText(opt.rect, Qt::AlignCenter, disp);
|
||||||
|
p->restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片
|
||||||
|
const QRect r = opt.rect.adjusted(4, 2, -4, -2);
|
||||||
|
const bool selected = opt.state & QStyle::State_Selected;
|
||||||
|
const bool hover = opt.state & QStyle::State_MouseOver;
|
||||||
|
if (selected || hover) {
|
||||||
|
QPainterPath path;
|
||||||
|
path.addRoundedRect(r, 6, 6);
|
||||||
|
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
|
||||||
|
}
|
||||||
|
if (selected) { // 左 2px 强调竖条(规范§6.2)
|
||||||
|
p->fillRect(QRect(r.left(), r.top() + 4, 2, r.height() - 8),
|
||||||
|
geopro::app::tokenColor("accent/primary"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString title = disp, meta;
|
||||||
|
const int nl = disp.indexOf(QLatin1Char('\n'));
|
||||||
|
if (nl >= 0) {
|
||||||
|
title = disp.left(nl);
|
||||||
|
meta = disp.mid(nl + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRect textR = r.adjusted(14, 6, -12, -6);
|
||||||
|
// 标题
|
||||||
|
QFont tf = opt.font;
|
||||||
|
tf.setPixelSize(geopro::app::scaledPx(13));
|
||||||
|
p->setFont(tf);
|
||||||
|
p->setPen(geopro::app::tokenColor("text/primary"));
|
||||||
|
const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2);
|
||||||
|
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width()));
|
||||||
|
// 元信息
|
||||||
|
if (!meta.isEmpty()) {
|
||||||
|
QFont mf = opt.font;
|
||||||
|
mf.setPixelSize(geopro::app::scaledPx(11));
|
||||||
|
p->setFont(mf);
|
||||||
|
p->setPen(geopro::app::tokenColor("text/tertiary"));
|
||||||
|
const QRect metaR(textR.left(), textR.center().y() + 1, textR.width(),
|
||||||
|
textR.height() / 2);
|
||||||
|
p->drawText(metaR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
p->fontMetrics().elidedText(meta, Qt::ElideRight, metaR.width()));
|
||||||
|
}
|
||||||
|
p->restore();
|
||||||
|
}
|
||||||
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
|
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
|
||||||
|
|
@ -37,7 +125,6 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
|
||||||
if (!append && rows.empty()) {
|
if (!append && rows.empty()) {
|
||||||
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
|
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
|
||||||
hint->setFlags(Qt::NoItemFlags);
|
hint->setFlags(Qt::NoItemFlags);
|
||||||
hint->setForeground(QColor("#9AA6B6"));
|
|
||||||
hint->setTextAlignment(Qt::AlignCenter);
|
hint->setTextAlignment(Qt::AlignCenter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -53,4 +140,13 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyDatasetCardDelegate(QListWidget* list) {
|
||||||
|
if (!list) return;
|
||||||
|
list->setItemDelegate(new DatasetCardDelegate(list));
|
||||||
|
list->setMouseTracking(true); // 让委托收到 hover 状态
|
||||||
|
list->setSpacing(0); // 卡间距由委托内边距控制
|
||||||
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
|
||||||
|
[list]() { list->viewport()->update(); });
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,7 @@ void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRo
|
||||||
// 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。
|
// 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。
|
||||||
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
|
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
|
||||||
|
|
||||||
|
// 给数据/文件列表套用卡片委托(标题+元信息双行、悬停/选中圆角高亮+左强调竖条,规范§6.2)。
|
||||||
|
void applyDatasetCardDelegate(QListWidget* list);
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "panels/ObjectTreePanel.hpp"
|
#include "panels/ObjectTreePanel.hpp"
|
||||||
|
|
||||||
#include <QColor>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
|
@ -8,6 +7,7 @@
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
#include "dto/NavDto.hpp"
|
#include "dto/NavDto.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -35,24 +35,16 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
|
||||||
lay->setContentsMargins(0, 0, 0, 0);
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
lay->setSpacing(0);
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
// Qt 原生标准树:复选框/展开箭头由 Fusion 绘制(明暗都清晰);配色取自全局主题 QSS。
|
||||||
tree_ = new QTreeWidget(this);
|
tree_ = new QTreeWidget(this);
|
||||||
tree_->setHeaderHidden(true);
|
tree_->setHeaderHidden(true);
|
||||||
{
|
tree_->setIndentation(14); // 收紧缩进
|
||||||
const QString openArrow = writeChevronIcon(true, QColor("#8A93A3"));
|
|
||||||
const QString closedArrow = 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));
|
|
||||||
}
|
|
||||||
lay->addWidget(tree_, 1);
|
lay->addWidget(tree_, 1);
|
||||||
|
|
||||||
hint_ = new QLabel(QStringLiteral("(加载中…)"), this);
|
hint_ = new QLabel(QStringLiteral("正在加载对象…"), this);
|
||||||
hint_->setAlignment(Qt::AlignCenter);
|
hint_->setAlignment(Qt::AlignCenter);
|
||||||
hint_->setStyleSheet(QStringLiteral("color:#9AA6B6; padding:16px;"));
|
geopro::app::applyTokenizedStyleSheet(hint_, QStringLiteral("color:{{text/disabled}}; padding:16px;"));
|
||||||
hint_->setVisible(false);
|
hint_->setVisible(false);
|
||||||
lay->addWidget(hint_);
|
lay->addWidget(hint_);
|
||||||
|
|
||||||
|
|
@ -62,8 +54,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
|
||||||
});
|
});
|
||||||
QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item, int) {
|
QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item, int) {
|
||||||
const QString tmId = item->data(0, kRoleTmId).toString();
|
const QString tmId = item->data(0, kRoleTmId).toString();
|
||||||
if (!tmId.isEmpty())
|
if (!tmId.isEmpty()) emit tmCheckToggled(tmId, item->checkState(0) == Qt::Checked);
|
||||||
emit tmCheckToggled(tmId, item->checkState(0) == Qt::Checked);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ signals:
|
||||||
void tmCheckToggled(const QString& tmObjectId, bool checked);
|
void tmCheckToggled(const QString& tmObjectId, bool checked);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTreeWidget* tree_ = nullptr;
|
QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控)
|
||||||
QLabel* hint_ = nullptr;
|
QLabel* hint_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue