diff --git a/CMakeLists.txt b/CMakeLists.txt index 2357669..da3abd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 bindings。静态链接(MIT 许可,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) diff --git a/NOTICE.md b/NOTICE.md index 3b12ba3..b37b895 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -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 Widgets(Fusion 风格 + 项目自带 QSS 主题),未使用第三方 UI 控件库。 + ## 许可证要点 - **Qt 6 / Qt-Advanced-Docking-System(LGPL)**:本项目以动态链接方式使用 Qt 与 ADS,符合 LGPL 对动态链接的要求;如需替换这些库,最终用户可自行替换对应动态库。若改为静态链接或分发修改版, 需遵循 LGPL 的相应义务(提供目标代码/重新链接能力)。 -- **ElaWidgetTools(MIT)**:以静态库方式集成;MIT 允许静态链接与闭源分发,需保留版权声明与许可证文本。 - **VTK / QtKeychain(BSD-3-Clause)**:需在分发物中保留版权声明与许可证文本,不得用作者名背书。 > 各组件完整许可证文本随其源码分发(FetchContent 拉取于 build 目录的 `_deps/<组件>-src/`, > 或随 Qt/VTK 安装目录)。如需在发行包中附带完整 LICENSE 文本,请从对应来源复制。 - -## ElaWidgetTools 版本钉定 - -为保证可复现构建,ElaWidgetTools 钉定到提交 -`b80eadc4a199186e14656dce09959b3216a593be`(见 `CMakeLists.txt` 的 `FetchContent_Declare(elawidgettools ...)`)。 diff --git a/docs/Geopro3.0_视觉设计规范.md b/docs/Geopro3.0_视觉设计规范.md new file mode 100644 index 0000000..278678c --- /dev/null +++ b/docs/Geopro3.0_视觉设计规范.md @@ -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 原型(浅色)提取并派生深色模式,覆盖项目分析视图工作台及客户端通用组件。色值为初始建议值,落地后应结合实机截图微调对比度与一致性,并随设计迭代维护版本。* diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index d8193ea..c7b9579 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -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/elawidgettools):ElaWindow/ElaTheme/Ela* 控件 qt6keychain nlohmann_json::nlohmann_json geopro_core # Phase 1:ColorScale 上色 diff --git a/src/app/Glyphs.cpp b/src/app/Glyphs.cpp index edd4a20..d4df0e7 100644 --- a/src/app/Glyphs.cpp +++ b/src/app/Glyphs.cpp @@ -14,9 +14,6 @@ #include #include -#include -#include - #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 diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index aadb7a7..844f2f8 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -2,10 +2,6 @@ #include "Theme.hpp" -#include -#include -#include - #include #include #include @@ -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; } diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index 46ccffa..34bfb9c 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -16,10 +16,6 @@ #include "Theme.hpp" -#include -#include -#include -#include 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); diff --git a/src/app/SettingsDialog.cpp b/src/app/SettingsDialog.cpp index 5a695e6..8ca3537 100644 --- a/src/app/SettingsDialog.cpp +++ b/src/app/SettingsDialog.cpp @@ -1,19 +1,17 @@ #include "SettingsDialog.hpp" +#include #include #include #include #include #include +#include #include #include #include #include -#include -#include -#include - #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() { "Qt 6GUI 框架LGPL-3.0" "VTK 9二维/三维渲染BSD-3-Clause" "Qt-Advanced-Docking-System停靠布局LGPL-2.1" - "ElaWidgetToolsFluent 控件MIT" "QtKeychain凭证安全存取BSD-3-Clause" "" "

完整声明见随附 NOTICE.md

")); diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 4f8471f..c504ffe 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -7,11 +7,9 @@ #include #include #include +#include #include -#include -#include - 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}, // 危险 - // 内联 chrome(TopBar/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 diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 0563274..50e98bb 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -9,6 +9,7 @@ // 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA // 危险 #C0392B +#include #include 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.18(body→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 diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 73295b3..5599dd4 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -17,11 +17,6 @@ #include "Glyphs.hpp" #include "Theme.hpp" -#include -#include -#include -#include -#include 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(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& 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& list, const QStri void TopBar::setProjects(const std::vector& 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(); diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index 8df61fd..abb81de 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -20,10 +20,6 @@ #include "AuthService.hpp" #include "Theme.hpp" -#include -#include -#include - 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); // 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。 - 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); diff --git a/src/app/main.cpp b/src/app/main.cpp index a493a93..fcd7b63 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -64,12 +65,6 @@ #include #include -#include -#include -#include -#include -#include -#include #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")); - // ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 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 包裹一个承载工作台的内层 - // QMainWindow(buildWorkbench 依赖 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(); // 进入工作台后拉真实 空间/项目/结构 diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 5b520ae..0f96985 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -6,9 +6,6 @@ #include #include -#include -#include - #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);