refactor(ui): 方案A — 移除 ElaWidgetTools,统一为标准 Qt + 单一设计系统

根因: 此前 Fusion+自定义QSS+ElaWidgetTools(自绘控件) 三套样式系统并存、互相打架,
是各种丑/不一致的来源。本次彻底收敛为一套:

- 移除 ElaWidgetTools 依赖(CMake FetchContent + 链接 + spike);所有 Ela* 控件 → 标准 Qt:
  ElaWindow→QMainWindow(原生标题栏)、ElaMenu/MenuBar→QMenu/QMenuBar、ElaLineEdit/ComboBox/
  CheckBox/PushButton/ToolButton/Text/TableWidget→对应 Qt、ElaIconButton→QToolButton+glyph
- 主题系统: 自建 ThemeManager(替代 ElaTheme,QStyleHints 检测系统明暗、持久化、changed 信号热切)
  + 单一「浅→暗」颜色映射(kDarkMap,全 UI 唯一颜色来源) + 单份 QSS(明色基线,暗色按表替换)
- 主题: 跟随系统/浅/深(持久化, 启动应用→登录与主页统一); 字号缩放经 scaledPx 覆盖内联 chrome
- NOTICE/关于 同步去掉 ElaWidgetTools
- ctest 53/53
This commit is contained in:
gaozheng 2026-06-10 14:44:59 +08:00
parent a13b58e09f
commit 9010b20b57
14 changed files with 924 additions and 265 deletions

View File

@ -63,24 +63,7 @@ FetchContent_Declare(qtkeychain
GIT_TAG v0.14.0)
FetchContent_MakeAvailable(qtkeychain)
# ElaWidgetTools spike feat/elawidgettools Fluent UI for QWidget
# RainbowCandyX fork Qt6.10+ 6.11 SOURCE_SUBDIR
# /PySide bindingsMIT static DLL
# find_package(Qt6 Widgets/WidgetsPrivate) .qrc(靠全局 AUTORCC)
set(ELAWIDGETTOOLS_BUILD_STATIC_LIB ON CACHE BOOL "" FORCE)
FetchContent_Declare(elawidgettools
GIT_REPOSITORY https://github.com/RainbowCandyX/ElaWidgetTools.git
GIT_TAG b80eadc4a199186e14656dce09959b3216a593be # 构建可复现(review H1)
SOURCE_SUBDIR ElaWidgetTools)
FetchContent_MakeAvailable(elawidgettools)
add_subdirectory(src)
# ElaWidgetTools spike demo -DGEOPRO_BUILD_ELA_SPIKE=ON review M4
option(GEOPRO_BUILD_ELA_SPIKE "Build ElaWidgetTools evaluation spike" OFF)
if(GEOPRO_BUILD_ELA_SPIKE)
add_subdirectory(spike/ela)
endif()
enable_testing()
add_subdirectory(tests)

View File

@ -8,21 +8,16 @@
| **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 |
| **ElaWidgetTools** | Fluent UI 控件(菜单/列表/表单/图标等) | MIT | https://github.com/Liniyous/ElaWidgetTools Qt6.10+ 支持取自 fork https://github.com/RainbowCandyX/ElaWidgetTools |
| **QtKeychain** (0.14.0) | 凭证安全存取("记住登录" | BSD-3-Clause | https://github.com/frankosterfeld/qtkeychain |
> UI 为标准 Qt WidgetsFusion 风格 + 项目自带 QSS 主题),未使用第三方 UI 控件库。
## 许可证要点
- **Qt 6 / Qt-Advanced-Docking-SystemLGPL**:本项目以动态链接方式使用 Qt 与 ADS符合 LGPL
对动态链接的要求;如需替换这些库,最终用户可自行替换对应动态库。若改为静态链接或分发修改版,
需遵循 LGPL 的相应义务(提供目标代码/重新链接能力)。
- **ElaWidgetToolsMIT**以静态库方式集成MIT 允许静态链接与闭源分发,需保留版权声明与许可证文本。
- **VTK / QtKeychainBSD-3-Clause**:需在分发物中保留版权声明与许可证文本,不得用作者名背书。
> 各组件完整许可证文本随其源码分发FetchContent 拉取于 build 目录的 `_deps/<组件>-src/`
> 或随 Qt/VTK 安装目录)。如需在发行包中附带完整 LICENSE 文本,请从对应来源复制。
## ElaWidgetTools 版本钉定
为保证可复现构建ElaWidgetTools 钉定到提交
`b80eadc4a199186e14656dce09959b3216a593be`(见 `CMakeLists.txt``FetchContent_Declare(elawidgettools ...)`)。

View File

@ -0,0 +1,669 @@
# Geopro 3.0 桌面客户端 — 视觉设计规范Design System
**版本 v1.0** · 适用范围Geopro 3.0 桌面客户端全部界面
**技术载体**Qt 6QtWidgets+ 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→ 语义 tokenSemantic。组件只引用语义 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 间距阶梯px4 的倍数节奏)
| 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`,可拖拽 `220400px` | 上下两段:对象树 + 数据集列表 |
| 右栏 | 默认宽 `340px`,可拖拽 `280460px` | 上下两段:异常/属性 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`34s 自动消失;可手动关闭 |
| 行内提示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
原型剖面图色阶为**电阻率经典彩虹色阶**(紫蓝→青绿→黄→橙红→深红,对数刻度 51000 Ω·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` 插入线 |
| 实时刷新(色阶/图例调整) | 视图即时重绘,无整页闪烁 |
> 动效克制:仅用 `120200ms` 的 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 原型(浅色)提取并派生深色模式,覆盖项目分析视图工作台及客户端通用组件。色值为初始建议值,落地后应结合实机截图微调对比度与一致性,并随设计迭代维护版本。*

View File

@ -36,7 +36,6 @@ target_link_libraries(geopro_desktop PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg
${VTK_LIBRARIES}
ads::qt6advanceddocking
ElaWidgetTools # Fluent UI feat/elawidgettoolsElaWindow/ElaTheme/Ela*
qt6keychain
nlohmann_json::nlohmann_json
geopro_core # Phase 1ColorScale

View File

@ -14,9 +14,6 @@
#include <QString>
#include <QSvgRenderer>
#include <ElaDef.h>
#include <ElaTheme.h>
#include "Theme.hpp"
namespace geopro::app {
@ -203,8 +200,7 @@ namespace {
// 当前主题下的 chrome 图标色:取主题主文本色(暗=浅、亮=深),保证两种模式都清晰。
QColor themedIconColor()
{
return eTheme->getThemeColor(
isDarkTheme() ? ElaThemeType::Dark : ElaThemeType::Light, ElaThemeType::BasicText);
return isDarkTheme() ? QColor(0xE6, 0xE8, 0xEB) : QColor(0x1F, 0x2A, 0x3D); // 主文字明/暗
}
} // namespace
@ -215,8 +211,7 @@ void setThemedGlyph(QLabel* label, Glyph type, int px)
label->setPixmap(makeGlyph(type, themedIconColor(), px).pixmap(px, px));
};
apply();
QObject::connect(eTheme, &ElaTheme::themeModeChanged, label,
[apply](ElaThemeType::ThemeMode) { apply(); });
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, label, [apply]() { apply(); });
}
void setThemedGlyph(QAbstractButton* button, Glyph type, int px)
@ -224,8 +219,7 @@ void setThemedGlyph(QAbstractButton* button, Glyph type, int px)
if (!button) return;
auto apply = [button, type, px]() { button->setIcon(makeGlyph(type, themedIconColor(), px)); };
apply();
QObject::connect(eTheme, &ElaTheme::themeModeChanged, button,
[apply](ElaThemeType::ThemeMode) { apply(); });
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
}
} // namespace geopro::app

View File

@ -2,10 +2,6 @@
#include "Theme.hpp"
#include <ElaDef.h>
#include <ElaIconButton.h>
#include <ElaToolButton.h>
#include <QButtonGroup>
#include <QColor>
#include <QHBoxLayout>
@ -41,6 +37,8 @@ QString headerQss()
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
"#panelBadgeWarn { background:%4; color:%5; border-radius:9px;"
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
"QToolButton#panelAction:hover { background:#EEF3FB; }"
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
" padding:8px 6px; font-size:%6px; }"
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
@ -65,25 +63,16 @@ QLabel* makeBadge(QWidget* parent)
return badge;
}
// 表头操作 Glyph → Ela 字体图标Fluent清晰且随主题不再用位图 makeGlyph 以免发糊)。
ElaIconType::IconName actionElaIcon(Glyph g)
{
switch (g) {
case Glyph::Filter: return ElaIconType::Filter;
case Glyph::Upload: return ElaIconType::Upload;
case Glyph::Plus: return ElaIconType::Plus;
case Glyph::Download: return ElaIconType::Download;
case Glyph::Collapse: return ElaIconType::ChevronUp;
default: return ElaIconType::Ellipsis;
}
}
// 表头操作按钮Fluent ElaIconButton 字体图标,固定 28×28与顶栏图标按钮一致
// 表头操作按钮QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #panelAction QSS 给)。
QWidget* makeActionButton(QWidget* parent, const HeaderAction& a)
{
auto* btn = new ElaIconButton(actionElaIcon(a.first), 16, 28, 28, parent);
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("panelAction"));
setThemedGlyph(btn, a.first, kActionIcon);
btn->setIconSize(QSize(kActionIcon, kActionIcon));
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(a.second + QStringLiteral("(占位)"));
btn->setAutoRaise(true);
return btn;
}

View File

@ -16,10 +16,6 @@
#include "Theme.hpp"
#include <ElaComboBox.h>
#include <ElaLineEdit.h>
#include <ElaPushButton.h>
#include <ElaTableWidget.h>
namespace geopro::app {
namespace {
@ -50,24 +46,24 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa
auto* filter = new QHBoxLayout();
filter->addWidget(new QLabel(QStringLiteral("项目名称"), this));
nameEdit_ = new ElaLineEdit(this);
nameEdit_ = new QLineEdit(this);
nameEdit_->setPlaceholderText(QStringLiteral("输入项目名称"));
nameEdit_->setFixedWidth(200);
filter->addWidget(nameEdit_);
filter->addSpacing(8);
filter->addWidget(new QLabel(QStringLiteral("项目类型"), this));
typeCombo_ = new ElaComboBox(this);
typeCombo_ = new QComboBox(this);
typeCombo_->setFixedWidth(160);
filter->addWidget(typeCombo_);
filter->addSpacing(8);
auto* searchBtn = new ElaPushButton(QStringLiteral("搜索"), this);
auto* resetBtn = new ElaPushButton(QStringLiteral("重置"), this);
auto* searchBtn = new QPushButton(QStringLiteral("搜索"), this);
auto* resetBtn = new QPushButton(QStringLiteral("重置"), this);
filter->addWidget(searchBtn);
filter->addWidget(resetBtn);
filter->addStretch();
root->addLayout(filter);
table_ = new ElaTableWidget(this); // Ela item 版表格(继承 QTableWidget),直替
table_ = new QTableWidget(this); // Ela item 版表格(继承 QTableWidget),直替
table_->setColumnCount(8);
table_->setHorizontalHeaderLabels(QStringList{
QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"),
@ -86,8 +82,8 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa
pageLabel_ = new QLabel(this);
bottom->addWidget(pageLabel_);
bottom->addStretch();
prevBtn_ = new ElaPushButton(QStringLiteral("上一页"), this);
nextBtn_ = new ElaPushButton(QStringLiteral("下一页"), this);
prevBtn_ = new QPushButton(QStringLiteral("上一页"), this);
nextBtn_ = new QPushButton(QStringLiteral("下一页"), this);
bottom->addWidget(prevBtn_);
bottom->addWidget(nextBtn_);
root->addLayout(bottom);

View File

@ -1,19 +1,17 @@
#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 <ElaComboBox.h>
#include <ElaPushButton.h>
#include <ElaText.h>
#include "Theme.hpp"
namespace geopro::app {
@ -34,9 +32,10 @@ QWidget* makeRow(const QString& label, QWidget* control) {
}
// 区段标题。
ElaText* sectionTitle(const QString& text, QWidget* parent) {
auto* t = new ElaText(text, parent);
t->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kHeading));
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;
}
@ -48,7 +47,7 @@ QWidget* buildAppearancePage() {
v->addWidget(sectionTitle(QStringLiteral("外观"), page));
// 主题:跟随系统 / 浅色 / 深色(热切)。
auto* themeCombo = new ElaComboBox(page);
auto* themeCombo = new QComboBox(page);
themeCombo->addItem(QStringLiteral("跟随系统"), QStringLiteral("system"));
themeCombo->addItem(QStringLiteral("浅色"), QStringLiteral("light"));
themeCombo->addItem(QStringLiteral("深色"), QStringLiteral("dark"));
@ -60,7 +59,7 @@ QWidget* buildAppearancePage() {
v->addWidget(makeRow(QStringLiteral("主题"), themeCombo));
// 界面字号:小/标准/大/特大(重启生效)。
auto* fontCombo = new ElaComboBox(page);
auto* fontCombo = new QComboBox(page);
fontCombo->addItem(QStringLiteral(""), 90);
fontCombo->addItem(QStringLiteral("标准"), 100);
fontCombo->addItem(QStringLiteral(""), 115);
@ -74,9 +73,11 @@ QWidget* buildAppearancePage() {
auto* rlay = new QHBoxLayout(restartRow);
rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐
rlay->setSpacing(10);
auto* hint = new ElaText(QStringLiteral("界面字号将在重启后生效"), restartRow);
hint->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kCaption));
auto* restartBtn = new ElaPushButton(QStringLiteral("立即重启"), restartRow);
auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow);
geopro::app::applyThemedStyleSheet(
hint, QStringLiteral("color:#5A6B85; 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();
@ -104,8 +105,9 @@ QWidget* buildAboutPage() {
v->setSpacing(12);
v->addWidget(sectionTitle(QStringLiteral("关于"), page));
auto* ver = new ElaText(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"), page);
ver->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kTitle));
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);
@ -116,7 +118,6 @@ QWidget* buildAboutPage() {
"<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>ElaWidgetTools</td><td>Fluent 控件</td><td>MIT</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>"));

View File

@ -7,11 +7,9 @@
#include <QPalette>
#include <QSettings>
#include <QStyleFactory>
#include <QStyleHints>
#include <QWidget>
#include <ElaDef.h>
#include <ElaTheme.h>
namespace geopro::app {
namespace {
@ -243,8 +241,45 @@ QStatusBar QLabel {
padding: 0 4px;
}
/* ── 菜单栏 / 菜单:已全部改用 ElaMenuBar/ElaMenu(自绘透明圆角弹窗+随主题)
QMenuBar/QMenu QSS border-radius */
/* ── 菜单栏 / 菜单(标准 QMenuBar/QMenu刻意不设 border-radius——弹窗圆角靠系统(Win11
)QSS // */
QMenuBar {
background: #FFFFFF;
color: #1F2A3D;
border-bottom: 1px solid #EEF1F5;
padding: 2px 6px;
}
QMenuBar::item {
background: transparent;
padding: 6px 12px;
border-radius: 6px;
}
QMenuBar::item:selected {
background: #EAF1FB;
color: #2D6CB5;
}
QMenuBar::item:pressed {
background: #DCE6F4;
}
QMenu {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #D5DBE5;
padding: 4px;
}
QMenu::item {
padding: 6px 24px 6px 14px;
border-radius: 6px;
}
QMenu::item:selected {
background: #EAF1FB;
color: #2D6CB5;
}
QMenu::separator {
height: 1px;
background: #E1E6EE;
margin: 4px 8px;
}
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
QComboBox {
@ -339,54 +374,53 @@ ads--CDockWidgetTab[activeTab="true"] QLabel {
// 保证里外ElaWindow 外壳 ↔ 内部工作台)在明/暗两模下完全一致——这是关键,
// 否则外壳一种灰、工作台另一种灰,明暗都显得割裂。
// 做法:把浅色设计稿里每个色令牌按语义角色替换为 ElaTheme 当前模式的真实颜色。
struct RoleMap {
const char* token; // 浅色设计稿中的占位 hex
ElaThemeType::ThemeColor role; // 对应 ElaTheme 颜色角色
// 浅→暗 颜色映射(全 UI 唯一颜色来源)。明色 = 令牌本身QSS 直接写的就是明色);
// 暗色 = 此表对应值。覆盖全部 QSS 用色,缺一即在暗色下露浅色。改色只改这一处。
struct DarkPair {
const char* light;
const char* dark;
};
const RoleMap kRoleMap[] = {
{"#F4F6FA", ElaThemeType::WindowBase}, // 外壳/停靠区底
{"#FFFFFF", ElaThemeType::BasicBase}, // 面板/输入/菜单 底
{"#EDF1F7", ElaThemeType::BasicBaseDeep}, // 抬升/表头底
{"#1F2A3D", ElaThemeType::BasicText}, // 主文字
{"#5A6B85", ElaThemeType::BasicTextNoFocus}, // 次要文字
{"#3A475C", ElaThemeType::BasicTextCategory}, // 表头文字
{"#1B3D67", ElaThemeType::BasicText}, // 选中行文字
{"#DCE9F8", ElaThemeType::BasicPress}, // 选中行/按下 底
{"#EEF3FB", ElaThemeType::BasicHover}, // 悬停底
{"#EAEEF4", ElaThemeType::BasicBorder}, // 细分隔/边框
{"#D5DBE5", ElaThemeType::BasicBorder}, // 边框
{"#C2CCDA", ElaThemeType::BasicBorderDeep}, // 强边框/滚动条柄
{"#C7D2E0", ElaThemeType::BasicBorder}, // 输入边框
{"#2D6CB5", ElaThemeType::PrimaryNormal}, // 强调
{"#2862A6", ElaThemeType::PrimaryHover}, // 强调悬停
{"#234F87", ElaThemeType::PrimaryPress}, // 强调按下
{"#9AA6B6", ElaThemeType::BasicTextDisable}, // 禁用文字
{"#F0F2F6", ElaThemeType::BasicDisable}, // 禁用底
{"#8A93A3", ElaThemeType::BasicTextDisable}, // 禁用文字2
{"#DCE0E7", ElaThemeType::BasicBorder}, // 禁用边框
{"#A7B4C7", ElaThemeType::BasicBorderDeep}, // 滚动条柄悬停
{"#E1E6EE", ElaThemeType::BasicBorder}, // 菜单分隔
{"#DCE6F4", ElaThemeType::BasicHover}, // 菜单栏选中
{"#E6EBF3", ElaThemeType::BasicDisable}, // 进度条底
{"#EAF1FB", ElaThemeType::BasicHover}, // 工具按钮选中底
{"#C0392B", ElaThemeType::StatusDanger}, // 危险
// 内联 chromeTopBar/PanelHeader额外用到的令牌
{"#EEF1F5", ElaThemeType::BasicBorder}, // 菜单栏底边线
{"#E6EAF1", ElaThemeType::BasicBorder}, // 面板表头底边线
{"#EAEEF5", ElaThemeType::BasicBaseDeep}, // 面板徽标底
const DarkPair kDarkMap[] = {
// 背景(外壳→面板→抬升)
{"#F4F6FA", "#1E1F22"}, {"#FFFFFF", "#2B2D30"}, {"#EDF1F7", "#34373C"},
{"#F0F2F6", "#2B2D30"}, {"#EAEEF4", "#3A3D42"}, {"#EAEEF5", "#34373C"},
{"#EEF2FB", "#34373C"}, {"#E6EBF3", "#34373C"}, {"#EEF3FB", "#34373C"},
{"#EAF1FB", "#2F4257"}, {"#DCE6F4", "#2A3A4F"}, {"#DCE9F8", "#33527A"},
{"#FBEAD2", "#46371F"},
// 文字
{"#1F2A3D", "#E6E8EB"}, {"#5A6B85", "#A0A8B4"}, {"#3A475C", "#C4CCD8"},
{"#8A93A3", "#828B98"}, {"#9AA6B6", "#6E7681"}, {"#1B3D67", "#E8F1FB"},
// 边框
{"#D5DBE5", "#3A3D42"}, {"#C2CCDA", "#484C52"}, {"#C7D2E0", "#484C52"},
{"#E1E6EE", "#34373C"}, {"#E6EAF1", "#34373C"}, {"#EEF1F5", "#34373C"},
{"#DCE0E7", "#3A3D42"}, {"#A7B4C7", "#4A4E54"},
// 强调(品牌蓝)
{"#2D6CB5", "#5E9BD6"}, {"#2862A6", "#6FA8DD"}, {"#234F87", "#4E89C4"},
// 语义
{"#C0392B", "#E06A5E"}, {"#B45309", "#E0964A"}, {"#15803D", "#5BBF7A"},
};
QColor roleColor(bool dark, ElaThemeType::ThemeColor role)
QString darkOf(const QString& lightHex)
{
return eTheme->getThemeColor(dark ? ElaThemeType::Dark : ElaThemeType::Light, role);
for (const auto& p : kDarkMap)
if (lightHex.compare(QLatin1String(p.light), Qt::CaseInsensitive) == 0)
return QString::fromLatin1(p.dark);
return lightHex;
}
// 把一段设计稿 QSS 按 ElaTheme 当前模式着色(浅色令牌→真实角色色)。供全局与内联样式共用。
// 设计令牌(浅色 hex) → 当前明暗的真实色。
QColor roleColor(bool dark, const char* lightHex)
{
return dark ? QColor(darkOf(QString::fromLatin1(lightHex))) : QColor(QLatin1String(lightHex));
}
// 把一段浅色设计稿 QSS 按当前明暗着色:明色原样;暗色把每个浅色令牌替换为暗色。
QString themedQss(const QString& designQss, bool dark)
{
if (!dark) return designQss;
QString s = designQss;
for (const auto& rm : kRoleMap)
s.replace(QLatin1String(rm.token), roleColor(dark, rm.role).name());
for (const auto& p : kDarkMap)
s.replace(QString::fromLatin1(p.light), QString::fromLatin1(p.dark), Qt::CaseInsensitive);
return s;
}
@ -400,20 +434,20 @@ QString styleSheetForMode(bool dark)
QPalette buildPalette(bool dark)
{
QPalette p;
const QColor shell = roleColor(dark, ElaThemeType::WindowBase);
const QColor panel = roleColor(dark, ElaThemeType::BasicBase);
const QColor text = roleColor(dark, ElaThemeType::BasicText);
const QColor muted = roleColor(dark, ElaThemeType::BasicTextNoFocus);
const QColor accent = roleColor(dark, ElaThemeType::PrimaryNormal);
const QColor border = roleColor(dark, ElaThemeType::BasicBorder);
const QColor disabled = roleColor(dark, ElaThemeType::BasicTextDisable);
const QColor shell = roleColor(dark, "#F4F6FA");
const QColor panel = roleColor(dark, "#FFFFFF");
const QColor text = roleColor(dark, "#1F2A3D");
const QColor muted = roleColor(dark, "#5A6B85");
const QColor accent = roleColor(dark, "#2D6CB5");
const QColor border = roleColor(dark, "#D5DBE5");
const QColor disabled = roleColor(dark, "#9AA6B6");
p.setColor(QPalette::Window, shell);
p.setColor(QPalette::WindowText, text);
p.setColor(QPalette::Base, panel);
p.setColor(QPalette::AlternateBase, roleColor(dark, ElaThemeType::BasicAlternating));
p.setColor(QPalette::AlternateBase, roleColor(dark, "#EDF1F7"));
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, roleColor(dark, ElaThemeType::BasicBaseDeep));
p.setColor(QPalette::Button, roleColor(dark, "#EDF1F7"));
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::ToolTipBase, text);
p.setColor(QPalette::ToolTipText, shell);
@ -442,7 +476,7 @@ void applyThemeMode(QApplication& app, bool dark)
// 基础字体:微软雅黑 UI基准字号取令牌 type::kBody(13px),与 QSS 同单位。
QFont base(QStringLiteral("Microsoft YaHei UI"));
base.setPixelSize(type::kBody);
base.setPixelSize(scaledPx(type::kBody)); // 随界面字号缩放
base.setStyleStrategy(QFont::PreferAntialias);
app.setFont(base);
@ -455,31 +489,56 @@ void applyTheme(QApplication& app)
applyThemeMode(app, false);
}
void applyBrandAccent()
{
// 品牌蓝强调色。亮色用主色 #2D6CB5暗色用同色相提亮版 #5E9BD6深底上对比足够
// Hover 略亮、Press 略深,三态成体系。设给 Ela 的 Primary全 UI 选中/激活统一这一套。
eTheme->setThemeColor(ElaThemeType::Light, ElaThemeType::PrimaryNormal, QColor(0x2D, 0x6C, 0xB5));
eTheme->setThemeColor(ElaThemeType::Light, ElaThemeType::PrimaryHover, QColor(0x3E, 0x7B, 0xC0));
eTheme->setThemeColor(ElaThemeType::Light, ElaThemeType::PrimaryPress, QColor(0x24, 0x5A, 0x9B));
eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::PrimaryNormal, QColor(0x5E, 0x9B, 0xD6));
eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::PrimaryHover, QColor(0x71, 0xA9, 0xDE));
eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::PrimaryPress, QColor(0x4E, 0x89, 0xC4));
// 选中底Ela 默认是半透明灰、对比度弱。改成清晰的强调蓝实色(明=浅蓝、暗=中深蓝),
// 让所有 Ela 列表/视图的选中行一眼可辨(与对象树 QSS 选中色 kTreeSel* 保持一致)。
eTheme->setThemeColor(ElaThemeType::Light, ElaThemeType::BasicSelectedAlpha, QColor(0xC2, 0xD9, 0xF2));
eTheme->setThemeColor(ElaThemeType::Light, ElaThemeType::BasicSelectedHoverAlpha, QColor(0xB1, 0xCD, 0xEF));
eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::BasicSelectedAlpha, QColor(0x33, 0x52, 0x7A));
eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::BasicSelectedHoverAlpha, QColor(0x3C, 0x5D, 0x87));
}
// ── 设置:主题 / 字号 偏好 ──────────────────────────────────────────────
// ── 主题管理器(替代 ElaTheme+ 设置:主题 / 字号 偏好 ──────────────────
namespace {
constexpr int kBaseFontPx = 13; // 基准字号(与 eApp 默认一致)
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();
@ -487,22 +546,12 @@ QString themeModePreference()
void applyPersistedThemeMode()
{
const QString m = themeModePreference();
if (m == QStringLiteral("light")) {
eTheme->setIsFollowSystemTheme(false);
eTheme->setThemeMode(ElaThemeType::Light);
} else if (m == QStringLiteral("dark")) {
eTheme->setIsFollowSystemTheme(false);
eTheme->setThemeMode(ElaThemeType::Dark);
} else { // system跟随系统明暗
eTheme->setIsFollowSystemTheme(true);
}
ThemeManager::instance().applyPersisted();
}
void setThemeModePreference(const QString& mode)
{
QSettings().setValue(QStringLiteral("ui/themeMode"), mode);
applyPersistedThemeMode(); // 主题可热切,立即应用
ThemeManager::instance().setMode(mode); // 持久化 + 热切
}
int fontScalePreference()
@ -530,7 +579,7 @@ int scaledPx(int basePx)
bool isDarkTheme()
{
return eTheme->getThemeMode() == ElaThemeType::Dark;
return ThemeManager::instance().isDark();
}
QString themed(const QString& designQss)
@ -540,7 +589,7 @@ QString themed(const QString& designQss)
void vtkBackground(double& r, double& g, double& b)
{
const QColor c = roleColor(isDarkTheme(), ElaThemeType::WindowBase);
const QColor c = roleColor(isDarkTheme(), "#F4F6FA");
r = c.redF();
g = c.greenF();
b = c.blueF();
@ -550,10 +599,8 @@ void applyThemedStyleSheet(QWidget* w, const QString& designQss)
{
if (!w) return;
w->setStyleSheet(themedQss(designQss, isDarkTheme()));
QObject::connect(eTheme, &ElaTheme::themeModeChanged, w,
[w, designQss](ElaThemeType::ThemeMode m) {
w->setStyleSheet(themedQss(designQss, m == ElaThemeType::Dark));
});
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
[w, designQss]() { w->setStyleSheet(themedQss(designQss, isDarkTheme())); });
}
} // namespace geopro::app

View File

@ -9,6 +9,7 @@
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
// 危险 #C0392B
#include <QObject>
#include <QString>
class QApplication;
@ -16,6 +17,23 @@ class QWidget;
namespace geopro::app {
// 主题管理器(纯 Qt替代 ElaTheme持有当前明暗 + 是否跟随系统;切换发 changed() 信号,
// 全 UI全局 QSS 由 main 重应用、内联 chrome 由 applyThemedStyleSheet据此热切重着色。
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.18body→title→heading刻意拉开层级——避免 11/12/13/14
@ -90,10 +108,6 @@ void applyThemeMode(QApplication& app, bool dark);
// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。
void applyTheme(QApplication& app);
// 统一强调色:把 ElaTheme 主色(Primary)设为本项目品牌蓝(亮/暗各一档),让所有 Ela 原生控件
// (选中/激活/标题栏强调) 与本项目 QSS 共用同一套蓝,消除"多种蓝打架"。在 eApp->init() 后调一次。
void applyBrandAccent();
// ── 设置:主题 / 界面字号 偏好QSettings 持久化)────────────────────────
// 启动时eApp->init + applyBrandAccent 之后、弹登录窗之前)各调一次,使登录页与主页统一。
void applyPersistedThemeMode(); // 应用持久化主题:跟随系统 / 浅色 / 深色 → ElaTheme

View File

@ -17,11 +17,6 @@
#include "Glyphs.hpp"
#include "Theme.hpp"
#include <ElaDef.h>
#include <ElaIconButton.h>
#include <ElaMenu.h>
#include <ElaMenuBar.h>
#include <ElaToolButton.h>
namespace geopro::app {
@ -42,20 +37,23 @@ QFrame* makeDivider(QWidget* parent)
return line;
}
// 右侧图标按钮Fluent ElaIconButton自带图标字体 + 悬停 + 随主题着色)。
// 用带固定宽高的构造(icon, 字号, 宽, 高, parent)——否则图标会被压扁变形。
QWidget* makeIconButton(QWidget* parent, ElaIconType::IconName icon, const QString& tip)
// 右侧图标按钮QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #iconBtn QSS 给)。
QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip)
{
auto* btn = new ElaIconButton(icon, 18, 34, 34, parent);
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("iconBtn"));
setThemedGlyph(btn, icon, kToolIcon);
btn->setIconSize(QSize(kToolIcon, kToolIcon));
btn->setToolTip(tip);
btn->setCursor(Qt::PointingHandCursor);
btn->setAutoRaise(true);
return btn;
}
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
QMenu* buildViewMenu(QWidget* p)
{
auto* m = new ElaMenu(QStringLiteral("视图"), p);
auto* m = new QMenu(QStringLiteral("视图"), p);
m->addAction(QStringLiteral("分析视图"));
m->addAction(QStringLiteral("大屏视图"));
return m;
@ -63,7 +61,7 @@ QMenu* buildViewMenu(QWidget* p)
QMenu* buildProjectMenu(QWidget* p)
{
auto* m = new ElaMenu(QStringLiteral("项目管理"), p);
auto* m = new QMenu(QStringLiteral("项目管理"), p);
m->addAction(QStringLiteral("数据视图"));
auto* cfg = m->addMenu(QStringLiteral("项目配置"));
cfg->addAction(QStringLiteral("基本信息"));
@ -97,7 +95,7 @@ QMenu* buildProjectMenu(QWidget* p)
QMenu* buildToolsMenu(QWidget* p)
{
auto* m = new ElaMenu(QStringLiteral("业务工具"), p);
auto* m = new QMenu(QStringLiteral("业务工具"), p);
m->addAction(QStringLiteral("ERT 思维分析"));
m->addAction(QStringLiteral("电法脚本与装置"));
m->addAction(QStringLiteral("Geo 反演"));
@ -107,7 +105,7 @@ QMenu* buildToolsMenu(QWidget* p)
QMenu* buildDeviceMenu(QWidget* p)
{
auto* m = new ElaMenu(QStringLiteral("设备"), p);
auto* m = new QMenu(QStringLiteral("设备"), p);
m->addAction(QStringLiteral("连接设备"));
m->addAction(QStringLiteral("设备管理"));
return m;
@ -117,7 +115,7 @@ QMenu* buildDeviceMenu(QWidget* p)
QWidget* buildMenuBar(QWidget* parent)
{
auto* mb = new ElaMenuBar(parent);
auto* mb = new QMenuBar(parent);
mb->setObjectName(QStringLiteral("appMenuBar"));
// ElaMenuBar 自绘 Fluent 外观并自动随 ElaTheme 明暗,不再写内联 QSS。
mb->addMenu(buildViewMenu(mb));
@ -142,6 +140,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
" font-size:%6px; font-weight:%4; }"
"#wsSwitcher:hover { background:#EEF3FB; }"
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
"#avatar { background:#2D6CB5; color:white; border-radius:17px; font-weight:%2;"
" font-size:%1px; }"
"#userName { color:#1F2A3D; font-size:%3px; font-weight:%4; }"
@ -166,7 +166,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
wsBtn_->setPopupMode(QToolButton::InstantPopup);
wsBtn_->setCursor(Qt::PointingHandCursor);
wsBtn_->setText(QStringLiteral("正在加载工作空间…"));
wsBtn_->setMenu(new ElaMenu(wsBtn_));
wsBtn_->setMenu(new QMenu(wsBtn_));
lay->addWidget(wsBtn_);
lay->addSpacing(10);
@ -182,14 +182,14 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
projBtn_->setPopupMode(QToolButton::InstantPopup);
projBtn_->setCursor(Qt::PointingHandCursor);
projBtn_->setText(QStringLiteral("正在加载项目…"));
projBtn_->setMenu(new ElaMenu(projBtn_));
projBtn_->setMenu(new QMenu(projBtn_));
lay->addWidget(projBtn_);
lay->addStretch();
lay->addWidget(makeIconButton(this, ElaIconType::CircleQuestion, QStringLiteral("帮助")));
lay->addWidget(makeIconButton(this, ElaIconType::Bell, QStringLiteral("通知")));
auto* gearBtn = makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置"));
lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助")));
lay->addWidget(makeIconButton(this, Glyph::Bell, 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);
@ -204,7 +204,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
avatar->setFixedSize(34, 34);
avatar->setCursor(Qt::PointingHandCursor);
avatar->setPopupMode(QToolButton::InstantPopup);
auto* userMenu = new ElaMenu(avatar);
auto* userMenu = new QMenu(avatar);
QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this,
[this] { emit logoutRequested(); });
avatar->setMenu(userMenu);
@ -225,7 +225,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
}
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
auto* menu = new ElaMenu(wsBtn_);
auto* menu = new QMenu(wsBtn_);
auto* header = menu->addAction(QStringLiteral("切换空间"));
header->setEnabled(false);
menu->addSeparator();
@ -256,7 +256,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
bool hasMore) {
auto* menu = new ElaMenu(projBtn_);
auto* menu = new QMenu(projBtn_);
auto* header = menu->addAction(QStringLiteral("切换项目"));
header->setEnabled(false);
menu->addSeparator();

View File

@ -20,10 +20,6 @@
#include "AuthService.hpp"
#include "Theme.hpp"
#include <ElaCheckBox.h>
#include <ElaLineEdit.h>
#include <ElaPushButton.h>
namespace geopro::app {
namespace {
@ -148,12 +144,12 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
form->addSpacing(6);
};
userEdit_ = new ElaLineEdit(body);
userEdit_ = new QLineEdit(body);
userEdit_->setPlaceholderText(QStringLiteral("请输入用户名"));
userEdit_->setClearButtonEnabled(true);
addField(QStringLiteral("用户名"), userEdit_);
pwdEdit_ = new ElaLineEdit(body);
pwdEdit_ = new QLineEdit(body);
pwdEdit_->setEchoMode(QLineEdit::Password);
pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码"));
addField(QStringLiteral("密码"), pwdEdit_);
@ -165,7 +161,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
auto* captchaRow = new QHBoxLayout();
captchaRow->setSpacing(10);
codeEdit_ = new ElaLineEdit(body);
codeEdit_ = new QLineEdit(body);
codeEdit_->setMinimumHeight(40);
codeEdit_->setPlaceholderText(QStringLiteral("请输入验证码"));
captchaLabel_ = new QLabel(body);
@ -191,7 +187,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
form->addLayout(refreshRow);
// 记住登录:勾选后成功登录将安全存储 token30 天内免登录。默认不勾(更安全)。
rememberChk_ = new ElaCheckBox(QStringLiteral("记住登录30 天内免登录)"), body);
rememberChk_ = new QCheckBox(QStringLiteral("记住登录30 天内免登录)"), body);
rememberChk_->setCursor(Qt::PointingHandCursor); // ElaCheckBox 自绘 Fluent + 自动明暗
form->addWidget(rememberChk_);
@ -206,7 +202,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
form->addStretch();
// 主操作满宽强调主按钮von Restorff唯一高强调元素引导主流程
loginBtn_ = new ElaPushButton(QStringLiteral("登 录"), body); // Fluent 主按钮(自动明暗)
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body); // Fluent 主按钮(自动明暗)
loginBtn_->setMinimumHeight(44);
loginBtn_->setCursor(Qt::PointingHandCursor);
loginBtn_->setDefault(true);

View File

@ -32,6 +32,7 @@
#include <QEvent>
#include <QFile>
#include <QButtonGroup>
#include <QCheckBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QGraphicsOpacityEffect>
@ -64,12 +65,6 @@
#include <DockManager.h>
#include <DockWidget.h>
#include <ElaApplication.h>
#include <ElaCheckBox.h>
#include <ElaDef.h>
#include <ElaTheme.h>
#include <ElaToolButton.h>
#include <ElaWindow.h>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
@ -305,8 +300,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }")));
};
applyDockSplitter();
QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager,
[applyDockSplitter](ElaThemeType::ThemeMode) { applyDockSplitter(); });
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
dockManager, [applyDockSplitter]() { applyDockSplitter(); });
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮ADS 自带标题栏随后隐藏,
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
@ -382,13 +377,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
"padding-bottom:3px;font-size:%2px;")
.arg(geopro::app::type::kWeightSemibold)
.arg(geopro::app::scaledPx(geopro::app::type::kTitle)));
auto* chkCurtain = new ElaCheckBox(QStringLiteral("帘面(断面墙)"));
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
chkCurtain->setChecked(true);
auto* chkVoxel = new ElaCheckBox(QStringLiteral("体素dd_voxel"));
auto* chkVoxel = new QCheckBox(QStringLiteral("体素dd_voxel"));
chkVoxel->setChecked(false);
auto* chkTerrain = new ElaCheckBox(QStringLiteral("地形DEM+影像)"));
auto* chkTerrain = new QCheckBox(QStringLiteral("地形DEM+影像)"));
chkTerrain->setChecked(false);
auto* chkSlice = new ElaCheckBox(QStringLiteral("切片dd_slice"));
auto* chkSlice = new QCheckBox(QStringLiteral("切片dd_slice"));
chkSlice->setChecked(false);
if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示
const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用");
@ -557,8 +552,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(selBg, selFg));
};
styleIt();
QObject::connect(eTheme, &ElaTheme::themeModeChanged, lw,
[styleIt](ElaThemeType::ThemeMode) { styleIt(); });
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
lw, [styleIt]() { styleIt(); });
};
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
@ -884,7 +879,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail走完整渲染路径、末尾必 Render
// 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。
QObject::connect(eTheme, &ElaTheme::themeModeChanged, &window,
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window,
[rebuildCentral, rebuildDetail]() {
rebuildCentral();
rebuildDetail();
@ -1063,16 +1058,11 @@ int main(int argc, char* argv[])
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
// ElaApplicationFluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化,
// 两种壳都需要它登录发生在选壳之前。Ela 控件跟随 ElaTheme标准控件仍由下面 QSS 接管。
eApp->init();
geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝
geopro::app::applyPersistedThemeMode(); // 应用持久化主题(跟随系统/浅/深)——登录页与主页统一
geopro::app::applyPersistedFontScale(); // 应用持久化字号缩放(登录页也随之缩放)
// 专业主题Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。
// 跟随最终 ElaTheme 模式(持久化偏好已生效),使登录窗与标准控件明暗一致。
geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark);
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。
// 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。
geopro::app::applyPersistedThemeMode();
geopro::app::applyPersistedFontScale();
geopro::app::applyThemeMode(app, geopro::app::isDarkTheme());
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
@ -1119,35 +1109,25 @@ int main(int argc, char* argv[])
geopro::data::ApiProjectRepository projectRepo(api);
geopro::controller::WorkbenchNavController nav(projectRepo);
// ── 外壳Fluent ElaWindow唯一路径。ElaWindow 用 addPageNode 包裹一个承载工作台的内层
// QMainWindowbuildWorkbench 依赖 QMainWindow 的 setCentralWidget/setMenuWidget/statusBar
// ElaWindow 自身将其设为私有,故用内层 QMainWindow 承接,零改 buildWorkbench
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其
// setCentralWidget/setMenuWidget/statusBar 承载工作台。
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
auto* ela = new ElaWindow;
ela->setWindowTitle(kTitle);
ela->resize(1280, 800);
ela->setMinimumSize(1024, 680);
ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏
ela->setAppBarHeight(38); // 默认 45 偏大,收紧标题栏更接近原生
auto* inner = new QMainWindow(ela); // 以 ela 为父,避免无父期调色板/DPI 抖动
buildWorkbench(*inner, repo, projectRepo, nav);
// 用 addPageNode 把工作台作为唯一页面放进中心页栈(填满到底边)。
// 注意:不能用 setCentralCustomWidget——它把控件插到页栈容器“之上”空页栈仍占底部
// 导致状态栏不贴底边(见 ElaCentralStackedWidget::setCustomWidget 的 insertWidget(0,...))。
ela->addPageNode(kTitle, inner);
auto* window = new QMainWindow;
window->setWindowTitle(kTitle);
window->resize(1280, 800);
window->setMinimumSize(1024, 680);
buildWorkbench(*window, repo, projectRepo, nav);
// 主题桥ElaTheme 明/暗切换 → 同步全局 QSS+调色板(覆盖所有标准控件与 ADS
QObject::connect(eTheme, &ElaTheme::themeModeChanged, ela, [&app](ElaThemeType::ThemeMode m) {
geopro::app::applyThemeMode(app, m == ElaThemeType::Dark);
// 主题桥ThemeManager 明/暗切换 → 重应用全局 QSS+调色板(标准控件 + ADS内联 chrome 经各自连接)。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
window, [&app]() { geopro::app::applyThemeMode(app, geopro::app::isDarkTheme()); });
// 主题切换快捷键 Ctrl+Shift+T持久化设置→外观 亦可改)。
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), window);
QObject::connect(themeSc, &QShortcut::activated, window, [] {
geopro::app::setThemeModePreference(geopro::app::isDarkTheme() ? QStringLiteral("light")
: QStringLiteral("dark"));
});
geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // 初始对齐
// 主题切换快捷键 Ctrl+Shift+T标题栏亦有 Ela 自带的明暗切换键)。
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), ela);
QObject::connect(themeSc, &QShortcut::activated, ela, [] {
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark
: ElaThemeType::Light);
});
ela->show();
window->show();
nav.start(); // 进入工作台后拉真实 空间/项目/结构

View File

@ -6,9 +6,6 @@
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <ElaDef.h>
#include <ElaTheme.h>
#include "Glyphs.hpp"
#include "Theme.hpp"
#include "dto/NavDto.hpp"
@ -49,8 +46,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
const bool dark = geopro::app::isDarkTheme();
const QColor border = dark ? QColor(0x8A, 0x92, 0x9C) : QColor(0x8A, 0x93, 0xA3);
const QColor boxBg = dark ? QColor(0x2B, 0x2D, 0x30) : QColor(0xFF, 0xFF, 0xFF);
const QColor accent = eTheme->getThemeColor(
dark ? ElaThemeType::Dark : ElaThemeType::Light, ElaThemeType::PrimaryNormal);
const QColor accent = dark ? QColor(0x5E, 0x9B, 0xD6) : QColor(0x2D, 0x6C, 0xB5); // 强调明/暗
const QString tag = dark ? QStringLiteral("d") : QStringLiteral("l");
const QString off = geopro::app::writeCheckboxIcon(false, border, boxBg, Qt::white, tag);
const QString on = geopro::app::writeCheckboxIcon(true, accent, accent, Qt::white, tag);
@ -65,8 +61,8 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
.arg(off, on, selBg, selFg));
};
applyCheckboxStyle();
QObject::connect(eTheme, &ElaTheme::themeModeChanged, tree_,
[applyCheckboxStyle](ElaThemeType::ThemeMode) { applyCheckboxStyle(); });
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, tree_,
[applyCheckboxStyle]() { applyCheckboxStyle(); });
lay->addWidget(tree_, 1);