From caf6f9ebd0bf447b904811b03cc3ceb6e875638c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 19:07:14 +0800 Subject: [PATCH 01/56] =?UTF-8?q?docs(build):=20=E6=96=B0=E5=A2=9E=20build?= =?UTF-8?q?.bat=20=E4=B8=80=E9=94=AE=E6=9E=84=E5=BB=BA=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=20+=20README=20=E8=A1=A5=E5=85=85=E6=9E=84=E5=BB=BA=E4=B8=8E?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 52 +++++++++++++++++++++++++++++------- build.bat | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 build.bat diff --git a/README.md b/README.md index e883264..6602a55 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## 技术栈 -Qt 6.8 LTS(QtWidgets)+ VTK 9.3+ · CMake + vcpkg(全量,含 Qt)· MSVC 2022 / C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。 +Qt 6.11(QtWidgets)+ VTK 9.6 · CMake + Ninja · 官方 MSVC 预编译 Qt + vcpkg(仅非 Qt 依赖)· MSVC(VS 2022/2026)/ C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。 ## 目录(设计 §3) @@ -25,19 +25,51 @@ tools/ 离线验证脚本(validate_samples.py) docs/ 规约、API、样本数据、设计文档 ``` -## 快速开始 +## 构建与运行 -前置:VS2022(C++ 桌面开发)、Git、vcpkg(设 `VCPKG_ROOT`)。详见 ENV_SETUP_Windows.md。 +前置:**Visual Studio 2022/2026**(勾选「使用 C++ 的桌面开发」工作负载,自带 CMake + Ninja)、Git、**vcpkg** 并设环境变量 `VCPKG_ROOT`。构建方案②:单一官方 MSVC 预编译 Qt(`CMAKE_PREFIX_PATH` → `D:/Qt/6.11.1/msvc2022_64`)、VTK 预编译于 `external/vtk-install`、ADS/QtKeychain 经 FetchContent 对接同一份 Qt、仅非 Qt 依赖(GDAL/PROJ/OpenSSL/…)走 vcpkg。详见 [docs/ENV_SETUP_Windows.md](docs/ENV_SETUP_Windows.md)。 -```powershell -# x64 Native Tools 命令行,项目根 -vcpkg x-update-baseline --add-initial-baseline # 锁依赖版本 -cmake --preset msvc-debug # 首次编译 Qt+VTK,较久 -cmake --build build/debug -.\build\debug\src\app\geopro_desktop.exe # spike 冒烟:应显示一个锥体 -ctest --test-dir build/debug # 运行单测 +> ⚠️ 本机 `cmake` / `ninja` / `cl` **默认不在 PATH**,必须在已激活 MSVC 环境的终端里构建。下面三种方式都已处理好这一点。 + +### 方式一:一键脚本(推荐) + +项目根的 `build.bat` 自动用 vswhere 定位 VS、激活 MSVC 环境、按需配置并编译。在 **cmd** 里于项目根执行 `build <命令>`: + +| 命令 | 作用 | +|---|---| +| `build`(或 `build app`) | 编译主程序 `geopro_desktop`(默认) | +| `build run` | 编译并运行主程序 | +| `build test` | 编译并跑单元测试(ctest) | +| `build all` | 编译全部目标 | +| `build configure` | 改了 CMakeLists / 新增源文件后,强制重新配置 | + +### 方式二:Visual Studio 打开文件夹 + +VS →「打开本地文件夹」→ 选仓库根 → 自动识别 `CMakePresets.json` → 选配置 **MSVC Release** → 菜单「生成 → 全部生成」;运行/调试目标选 `geopro_desktop`。 + +### 方式三:手动命令行 + +开始菜单打开「**x64 Native Tools Command Prompt for VS**」(已带 MSVC 环境),在仓库根(CMake 用 VS 自带的全路径,因其不在 PATH): + +```bat +set CMAKE="%VSINSTALLDIR%Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" +%CMAKE% --preset msvc-release :: 配置(首次 / 改 CMakeLists 后) +%CMAKE% --build build/release --target geopro_desktop :: 编译主程序 +build\release\src\app\geopro_desktop.exe :: 运行 +%CMAKE% --build build/release --target geopro_tests :: 编译测试 +ctest --test-dir build/release --output-on-failure :: 跑测试 ``` +(Debug 用 `--preset msvc-debug`,对应 `build/debug`。`%VSINSTALLDIR%` 在 Native Tools 提示符里已设好。) + +### 构建目标与产物 + +- `geopro_desktop` — 主程序,产物 `build/release/src/app/geopro_desktop.exe` +- `geopro_tests` — 单元测试,配 `ctest` +- 不带 `--target` 编译全部 + +> 改了已有 `.cpp/.hpp` 直接 `--build`(Ninja 增量);改了 `CMakeLists.txt` 或新增源文件需先 `--preset` / `build configure`。链接报 **LNK1104**(文件被占用)时,先关掉运行中的 `geopro_desktop.exe`。 + ## 当前状态 M1 设计完成(v2,经双专家评审)。进入 **spike 预研**(设计 §15):① 全 vcpkg 构建/部署 ② ADS + QVTKOpenGLStereoWidget 停靠稳定 ③ 真实样本跑通 banded contour。spike 通过后展开完整实现计划。 diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..86db412 --- /dev/null +++ b/build.bat @@ -0,0 +1,79 @@ +@echo off +REM ============================================================ +REM geopro build helper (Windows / MSVC + Ninja, CMake presets) +REM +REM Usage: build [app | all | test | run | configure] +REM app (default) build target geopro_desktop +REM all build all targets +REM test build + run unit tests via ctest +REM run build + launch geopro_desktop +REM configure force re-run CMake configure (after CMakeLists changes) +REM +REM Requires: Visual Studio 2022/2026 (Desktop C++ workload, ships +REM CMake + Ninja) and the VCPKG_ROOT environment variable. +REM Note: cmake/ninja/cl are NOT on PATH on this machine; this script +REM locates VS via vswhere and activates the MSVC env itself. +REM ============================================================ +setlocal +set "ROOT=%~dp0" +set "BUILDDIR=%ROOT%build\release" +set "PRESET=msvc-release" + +REM --- locate Visual Studio (vswhere lives at a fixed path) --- +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "%VSWHERE%" ( + echo [build] vswhere not found. Open "x64 Native Tools Command Prompt for VS" and build manually. + exit /b 1 +) +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do set "VSPATH=%%i" +if not defined VSPATH ( echo [build] Visual Studio not found. & exit /b 1 ) + +set "VCVARS=%VSPATH%\VC\Auxiliary\Build\vcvars64.bat" +set "CMAKE=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" +set "CTEST=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe" +if not exist "%VCVARS%" ( echo [build] vcvars64.bat not found: %VCVARS% & exit /b 1 ) +if not exist "%CMAKE%" ( echo [build] cmake not found: %CMAKE% & exit /b 1 ) + +REM --- activate MSVC environment (cl / link / include / lib) --- +call "%VCVARS%" >nul + +set "CMD=%~1" +if "%CMD%"=="" set "CMD=app" + +if /i "%CMD%"=="configure" goto :configure +if /i "%CMD%"=="app" goto :app +if /i "%CMD%"=="all" goto :all +if /i "%CMD%"=="test" goto :test +if /i "%CMD%"=="run" goto :run +echo [build] unknown command "%CMD%". Use: app ^| all ^| test ^| run ^| configure +exit /b 1 + +:ensure +if not exist "%BUILDDIR%\CMakeCache.txt" "%CMAKE%" --preset %PRESET% +exit /b 0 + +:configure +"%CMAKE%" --preset %PRESET% +exit /b %errorlevel% + +:app +call :ensure +"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop +exit /b %errorlevel% + +:all +call :ensure +"%CMAKE%" --build "%BUILDDIR%" +exit /b %errorlevel% + +:test +call :ensure +"%CMAKE%" --build "%BUILDDIR%" --target geopro_tests || exit /b 1 +"%CTEST%" --test-dir "%BUILDDIR%" --output-on-failure +exit /b %errorlevel% + +:run +call :ensure +"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop || exit /b 1 +"%BUILDDIR%\src\app\geopro_desktop.exe" +exit /b %errorlevel% From 1a9fb72cf07b5f5bd62c2e0c6b82ec1b5a91c09c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 20:26:00 +0800 Subject: [PATCH 02/56] =?UTF-8?q?feat(ui):=20impeccable=20=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E4=BB=A4=E7=89=8C=E4=BD=93=E7=B3=BB=20+=20=E7=A9=BA?= =?UTF-8?q?=E7=8A=B6=E6=80=81/=E8=AF=AD=E4=B9=89=E8=89=B2/=E5=8A=A8?= =?UTF-8?q?=E6=95=88=20+=20dock=20=E6=A0=87=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - typeset: Theme.hpp 新增排版令牌(type::),统一各处散落字号/字重 - layout: 间距/圆角令牌(space::/radius::),圆角 6 档→2 档,手调奇数余白对称化 - delight: 中央空状态引导浮层、上下文化加载文案、登录错误淡入 - colorize: 语义色令牌(semantic::),项目状态着色、状态栏错误染色、异常徽标警示色(休眠) - overdrive(休眠): 详情视图相机补间+actor淡入(animateReveal),待 dd 详情渲染接通后激活 - fix(dock): restoreState 后重新隐藏 ADS 子窗口标题栏,修复已保存布局下标题栏复现 --- src/app/PanelHeader.cpp | 46 ++++-- src/app/ProjectListDialog.cpp | 22 ++- src/app/Theme.cpp | 37 +++-- src/app/Theme.hpp | 67 +++++++++ src/app/TopBar.cpp | 46 +++--- src/app/login/LoginWindow.cpp | 75 +++++++--- src/app/main.cpp | 226 +++++++++++++++++++++++++++-- src/app/panels/ObjectTreePanel.cpp | 2 +- 8 files changed, 432 insertions(+), 89 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index a561de6..30c342d 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -1,5 +1,7 @@ #include "PanelHeader.hpp" +#include "Theme.hpp" + #include #include #include @@ -20,19 +22,33 @@ constexpr int kTitleIcon = 20; // 表头标题图标 constexpr int kActionIcon = 19; // 表头操作按钮图标 constexpr int kTabIcon = 19; // Tab 图标 -// 表头统一样式(标准表头 + Tab 表头共用)。 -const char* kHeaderQss = - "#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }" - "#panelTitle { color:#1F2A3D; font-size:14px; font-weight:600; }" - "#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;" - " padding:1px 7px; font-size:12px; font-weight:600; }" - "QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }" - "QToolButton#panelAction:hover { background:#EEF3FB; }" - "QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;" - " padding:8px 4px; font-size:14px; }" - "QToolButton#tabBtn:hover { color:#1F2A3D; }" - "QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:600;" - " border-bottom:2px solid #2D6CB5; }"; +// 表头统一样式(标准表头 + Tab 表头共用)。字号/字重引用 Theme 排版令牌: +// 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。 +// #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色), +// 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。 +QString headerQss() +{ + return QStringLiteral( + "#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }" + "#panelTitle { color:#1F2A3D; font-size:%1px; font-weight:%4; }" + "#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;" + " padding:1px 7px; font-size:%2px; font-weight:%4; }" + "#panelBadgeWarn { background:%5; color:%6; border-radius:9px;" + " padding:1px 7px; font-size:%2px; font-weight:%4; }" + "QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }" + "QToolButton#panelAction:hover { background:#EEF3FB; }" + "QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;" + " padding:8px 4px; font-size:%3px; }" + "QToolButton#tabBtn:hover { color:#1F2A3D; }" + "QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:%4;" + " border-bottom:2px solid #2D6CB5; }") + .arg(type::kTitle) + .arg(type::kCaption) + .arg(type::kBody) + .arg(type::kWeightSemibold) + .arg(QString::fromUtf8(semantic::kWarningFill)) + .arg(QString::fromUtf8(semantic::kWarning)); +} // 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。 QLabel* makeBadge(QWidget* parent) @@ -65,7 +81,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - header->setStyleSheet(QString::fromUtf8(kHeaderQss)); + header->setStyleSheet(headerQss()); auto* lay = new QHBoxLayout(header); lay->setContentsMargins(12, 0, 8, 0); @@ -98,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - header->setStyleSheet(QString::fromUtf8(kHeaderQss)); + header->setStyleSheet(headerQss()); auto* hlay = new QHBoxLayout(header); hlay->setContentsMargins(10, 0, 8, 0); hlay->setSpacing(2); diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index 02f0574..23c28a6 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,8 @@ #include #include +#include "Theme.hpp" + namespace geopro::app { namespace { QString statusText(int s) { @@ -22,6 +25,15 @@ QString statusText(int s) { default: return QString::number(s); } } + +// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。 +const char* statusColorHex(int s) { + switch (s) { + case 1: return "#8A93A3"; // 未开始:弱化 + case 2: return semantic::kInfo; // 进行中:活动中 + default: return "#5A6B85"; // 未知:中性 + } +} } // namespace ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent) @@ -145,7 +157,15 @@ void ProjectListDialog::query() { nameItem->setForeground(QColor("#2D6CB5")); table_->setItem(i, 1, nameItem); set(2, QString::fromStdString(p.code)); - set(3, statusText(p.status)); + // 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。 + auto* statusItem = new QTableWidgetItem(statusText(p.status)); + statusItem->setForeground(QColor(statusColorHex(p.status))); + if (p.status == 2) { + QFont f = statusItem->font(); + f.setBold(true); + statusItem->setFont(f); + } + table_->setItem(i, 3, statusItem); set(4, QString::fromStdString(p.typeName)); set(5, QString::fromStdString(p.ownerCompany)); set(6, QString::fromStdString(p.responsiblePerson)); diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 3497124..ede5b4d 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -26,7 +26,7 @@ QToolTip { background: #1F2A3D; color: #F4F6FA; border: 1px solid #2D6CB5; - border-radius: 4px; + border-radius: 6px; padding: 4px 8px; } @@ -42,7 +42,7 @@ QToolBar QToolButton { background: transparent; color: #5A6B85; border: none; - border-radius: 7px; + border-radius: 8px; padding: 6px 14px; font-weight: 500; } @@ -75,7 +75,7 @@ QTreeWidget, QListWidget, QTreeView, QListView { outline: none; } QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item { - padding: 7px 8px; + padding: 8px 8px; border-radius: 6px; margin: 1px 4px; } @@ -170,7 +170,7 @@ QLineEdit { color: #1F2A3D; border: 1px solid #C7D2E0; border-radius: 6px; - padding: 5px 8px; + padding: 6px 8px; selection-background-color: #2D6CB5; selection-color: #FFFFFF; } @@ -190,7 +190,7 @@ QScrollBar:vertical { } QScrollBar::handle:vertical { background: #C2CCDA; - border-radius: 5px; + border-radius: 6px; min-height: 28px; } QScrollBar::handle:vertical:hover { @@ -203,7 +203,7 @@ QScrollBar:horizontal { } QScrollBar::handle:horizontal { background: #C2CCDA; - border-radius: 5px; + border-radius: 6px; min-width: 28px; } QScrollBar::handle:horizontal:hover { @@ -253,7 +253,7 @@ QMenuBar { } QMenuBar::item { background: transparent; - padding: 5px 12px; + padding: 6px 12px; border-radius: 6px; } QMenuBar::item:selected { @@ -264,11 +264,11 @@ QMenu { color: #1F2A3D; border: 1px solid #D5DBE5; border-radius: 8px; - padding: 5px; + padding: 6px; } QMenu::item { padding: 6px 24px 6px 14px; - border-radius: 5px; + border-radius: 6px; } QMenu::item:selected { background: #DCE9F8; @@ -277,7 +277,7 @@ QMenu::item:selected { QMenu::separator { height: 1px; background: #E1E6EE; - margin: 5px 8px; + margin: 6px 8px; } /* ── 下拉框(按需出现时也与主题一致)──────────────────────── */ @@ -286,7 +286,7 @@ QComboBox { color: #1F2A3D; border: 1px solid #C2CCDA; border-radius: 6px; - padding: 5px 10px; + padding: 6px 10px; min-height: 18px; } QComboBox:hover { @@ -327,14 +327,14 @@ QGroupBox::title { QProgressBar { background: #E6EBF3; border: none; - border-radius: 5px; + border-radius: 6px; height: 8px; text-align: center; color: #5A6B85; } QProgressBar::chunk { background: #2D6CB5; - border-radius: 5px; + border-radius: 6px; } /* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)────────────── @@ -416,12 +416,19 @@ void applyTheme(QApplication& app) app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); // 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。 - // 10pt(≈13px)对齐主流商用客户端基准;9pt 偏小显拥挤。抗锯齿优先,观感更精致。 - QFont base(QStringLiteral("Microsoft YaHei UI"), 10); + // 基准字号取排版令牌 type::kBody(13px)——统一为 px,与 QSS 同单位 + // (旧值 10pt≈13.3px,观感几乎不变);9pt 偏小显拥挤。抗锯齿优先,观感更精致。 + QFont base(QStringLiteral("Microsoft YaHei UI")); + base.setPixelSize(type::kBody); base.setStyleStrategy(QFont::PreferAntialias); app.setFont(base); app.setPalette(buildPalette()); + + // 注意:不要给 ADS 停靠标题(ads--CDockWidgetTab QLabel)追加任何样式—— + // 这些子窗口标题栏在 main.cpp 里被 setVisible(false) 刻意隐藏(表头由各面板 + // 自绘的 PanelHeader 承担)。改写其字号/内边距会变更度量并触发 ADS 重新 + // 评估标题栏可见性,把隐藏的标题又显示出来。字号统一只作用于可见控件。 app.setStyleSheet(QString::fromUtf8(kStyleSheet)); } diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index d6734a8..71a51d0 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -13,6 +13,73 @@ class QApplication; namespace geopro::app { +// ── 排版令牌(全项目唯一字号阶 + 字重角色)────────────────────────── +// 各处 QSS 的 font-size / font-weight 一律引用这些值,不再散落硬编码 px。 +// 阶比 ~1.18(body→title→heading),刻意拉开层级——避免 11/12/13/14 +// 这类只差 1px 的"糊层级",让标题/正文/说明三档一眼可分。 +// 单位:px(与全局 px 化的 QSS、固定像素行高对齐;后续若做无障碍字号 +// 缩放再统一切 pt)。字重:400 正文 / 500 可交互激活 / +// 600 标签·标题 / 700 仅展示性大标题。 +namespace type { + +inline constexpr int kCaption = 12; // 徽标·提示·角色名·错误·副标题·字段标签 +inline constexpr int kBody = 13; // 树/列表/菜单/正文(= 全局基准字号) +inline constexpr int kLabel = 13; // 表头·用户名等需加粗的同级标签(配 600) +inline constexpr int kTitle = 15; // 面板/停靠区/区段标题·主操作按钮 +inline constexpr int kHeading = 18; // 视图/对话框级标题(预留) +inline constexpr int kDisplay = 24; // 登录品牌名(唯一展示性大字) + +inline constexpr int kWeightRegular = 400; +inline constexpr int kWeightMedium = 500; +inline constexpr int kWeightSemibold = 600; +inline constexpr int kWeightBold = 700; + +} // namespace type + +// ── 间距令牌(全项目唯一间距阶)────────────────────────────────── +// 取代散落的 5/7/9/11/13/15/26 等任意值。这是密集专业工具的实际节奏 +// (非外加的 8pt 网格):相邻档 2px 粒度,足够紧凑又不糊成一片。 +// 用法:布局 setContentsMargins/setSpacing/addSpacing 与 QSS padding/margin +// 一律引用这些档;明显的奇数值就近归档(13/15→lg, 11→ml, 26→xxl)。 +namespace space { + +inline constexpr int kXxs = 2; // 发丝级:下划线偏移、滚动条边距 +inline constexpr int kXs = 4; // 紧凑内边距、最小间隙 +inline constexpr int kSm = 6; // 行内紧凑(控件竖向 padding) +inline constexpr int kMd = 8; // 标准间隔(最常用) +inline constexpr int kMl = 10; // 偏大行距(密集行/验证码行) +inline constexpr int kLg = 12; // 分组间隔、面板内左右边距 +inline constexpr int kXl = 16; // 区块内边距 +inline constexpr int kXxl = 24; // 区块间距、表单纵向边距 +inline constexpr int kXxxl = 32; // 页面级留白(登录窗左右边距) + +} // namespace space + +// ── 圆角令牌(统一原先 4/5/6/7/8/9 共 6 档为 3 档)──────────────── +// 圆形元素(头像等)用 直径/2 单独写字面量,不入档。 +namespace radius { + +inline constexpr int kSm = 6; // 按钮·输入·菜单项·滚动条·进度条 +inline constexpr int kMd = 8; // 卡片·面板·对话框·菜单·分组框 +inline constexpr int kPill = 9; // 数量徽标胶囊 + +} // namespace radius + +// ── 语义色令牌(状态/反馈,产品语境:只在承载含义处用,不作装饰)────────── +// 文字值均针对白底面板(#FFFFFF)选深色,对比度 ≥4.5:1(正文级);与冷调中性 +// 调色板调和。danger 沿用既有红,避免引入第二种红。 +namespace semantic { + +inline constexpr const char* kInfo = "#2D6CB5"; // 信息·进行中(= 品牌蓝) +inline constexpr const char* kSuccess = "#15803D"; // 成功·已完成(深绿) +inline constexpr const char* kWarning = "#B45309"; // 警告·需注意(深琥珀) +inline constexpr const char* kDanger = "#C0392B"; // 危险·错误(沿用既有红) + +// 浅色填充(徽标/标签底色,配同族深色文字使用)。 +inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kWarning 文字) + +} // namespace semantic + // 应用浅色专业主题(Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。 void applyTheme(QApplication& app); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 9d20fc7..3a9824f 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -14,6 +14,7 @@ #include #include "Glyphs.hpp" +#include "Theme.hpp" namespace geopro::app { @@ -116,10 +117,11 @@ QWidget* buildMenuBar(QWidget* parent) mb->setObjectName(QStringLiteral("appMenuBar")); // 自带样式(覆盖全局),加大字号/内边距,专业观感。 mb->setStyleSheet(QStringLiteral( - "#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }" - "#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:14px; color:#1F2A3D; }" - "#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }" - "#appMenuBar::item:pressed { background:#DCE6F4; }")); + "#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }" + "#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:%1px; color:#1F2A3D; }" + "#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }" + "#appMenuBar::item:pressed { background:#DCE6F4; }") + .arg(type::kBody)); mb->addMenu(buildViewMenu(mb)); mb->addMenu(buildProjectMenu(mb)); mb->addMenu(buildToolsMenu(mb)); @@ -130,19 +132,27 @@ QWidget* buildMenuBar(QWidget* parent) TopBar::TopBar(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("appToolBar")); setFixedHeight(56); + // 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、 + // 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。 setStyleSheet(QStringLiteral( - "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" - "#topDivider { color:#E1E6EE; }" - "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" - " font-size:14px; font-weight:600; }" - "#wsSwitcher:hover { background:#EEF3FB; }" - "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" - "QToolButton#iconBtn:hover { background:#EEF3FB; }" - "QToolButton::menu-indicator { image:none; }" - "#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:700;" - " font-size:13px; }" - "#userName { color:#1F2A3D; font-size:13px; font-weight:600; }" - "#userRole { color:#8A93A3; font-size:11px; }")); + "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" + "#topDivider { color:#E1E6EE; }" + "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" + " font-size:%1px; font-weight:%5; }" + "#wsSwitcher:hover { background:#EEF3FB; }" + "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" + "QToolButton#iconBtn:hover { background:#EEF3FB; }" + "QToolButton::menu-indicator { image:none; }" + "#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:%6;" + " font-size:%2px; }" + "#userName { color:#1F2A3D; font-size:%3px; font-weight:%5; }" + "#userRole { color:#8A93A3; font-size:%4px; }") + .arg(type::kTitle) + .arg(type::kBody) + .arg(type::kLabel) + .arg(type::kCaption) + .arg(type::kWeightSemibold) + .arg(type::kWeightBold)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); @@ -156,7 +166,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); wsBtn_->setPopupMode(QToolButton::InstantPopup); wsBtn_->setCursor(Qt::PointingHandCursor); - wsBtn_->setText(QStringLiteral("(加载中…)")); + wsBtn_->setText(QStringLiteral("正在加载工作空间…")); wsBtn_->setMenu(new QMenu(wsBtn_)); lay->addWidget(wsBtn_); @@ -172,7 +182,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); projBtn_->setPopupMode(QToolButton::InstantPopup); projBtn_->setCursor(Qt::PointingHandCursor); - projBtn_->setText(QStringLiteral("(加载中…)")); + projBtn_->setText(QStringLiteral("正在加载项目…")); projBtn_->setMenu(new QMenu(projBtn_)); lay->addWidget(projBtn_); diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index ff5bdea..10594ef 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -2,19 +2,23 @@ #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include "AuthService.hpp" +#include "Theme.hpp" namespace geopro::app { @@ -82,21 +86,27 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。 // QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。 + // 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。 setStyleSheet(QStringLiteral( - "QDialog { background: #F4F6FA; }" - "#headerBand {" - " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," - " stop:0 #2D6CB5, stop:1 #234F87); }" - "#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }" - "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }" - "#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }" - "QLineEdit {" - " background: #FFFFFF; color: #1F2A3D;" - " border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;" - " selection-background-color: #2D6CB5; selection-color: #FFFFFF; }" - "QLineEdit:focus { border: 1px solid #2D6CB5; }" - "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }" - "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }")); + "QDialog { background: #F4F6FA; }" + "#headerBand {" + " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #2D6CB5, stop:1 #234F87); }" + "#brandTitle { color: #FFFFFF; font-size: %1px; font-weight: %2; }" + "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }" + "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" + "QLineEdit {" + " background: #FFFFFF; color: #1F2A3D;" + " border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;" + " selection-background-color: #2D6CB5; selection-color: #FFFFFF; }" + "QLineEdit:focus { border: 1px solid #2D6CB5; }" + "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }" + "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") + .arg(type::kDisplay) + .arg(type::kWeightBold) + .arg(type::kCaption) + .arg(type::kCaption) + .arg(type::kWeightSemibold)); auto* root = new QVBoxLayout(this); root->setContentsMargins(0, 0, 0, 0); @@ -122,8 +132,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)── auto* body = new QWidget(this); auto* form = new QVBoxLayout(body); - form->setContentsMargins(32, 24, 32, 26); - form->setSpacing(6); + // 表单边距取间距令牌:左右 xxxl(32)、上下 xxl(24),对称(原底部 26 是手调奇数)。 + form->setContentsMargins(space::kXxxl, space::kXxl, space::kXxxl, space::kXxl); + form->setSpacing(space::kSm); // 统一字段构造:小号muted标签 + 40px 高输入框 + 字段间距。 auto addField = [&](const QString& labelText, QLineEdit* edit) { @@ -178,12 +189,14 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。 rememberChk_ = new QCheckBox(QStringLiteral("记住登录(30 天内免登录)"), body); rememberChk_->setCursor(Qt::PointingHandCursor); - rememberChk_->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:13px;")); + rememberChk_->setStyleSheet( + QStringLiteral("color:#5A6B85; font-size:%1px;").arg(type::kBody)); form->addWidget(rememberChk_); // 错误提示:固定占位高度,避免出现时整体布局跳动。 errorLabel_ = new QLabel(body); - errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;")); + errorLabel_->setStyleSheet( + QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption)); errorLabel_->setWordWrap(true); errorLabel_->setMinimumHeight(18); form->addWidget(errorLabel_); @@ -195,11 +208,13 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) loginBtn_->setMinimumHeight(44); loginBtn_->setCursor(Qt::PointingHandCursor); loginBtn_->setStyleSheet(QStringLiteral( - "QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; " - "font-size: 15px; font-weight: 600; }" - "QPushButton:hover { background: #2862A6; }" - "QPushButton:pressed { background: #234F87; }" - "QPushButton:disabled { background: #9FB4CC; }")); + "QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; " + "font-size: %1px; font-weight: %2; }" + "QPushButton:hover { background: #2862A6; }" + "QPushButton:pressed { background: #234F87; }" + "QPushButton:disabled { background: #9FB4CC; }") + .arg(type::kTitle) + .arg(type::kWeightSemibold)); loginBtn_->setDefault(true); form->addWidget(loginBtn_); @@ -279,6 +294,20 @@ bool LoginWindow::remember() const void LoginWindow::showError(const QString& msg) { errorLabel_->setText(msg); + + // 错误淡入:柔化失败时刻(仅透明度 200ms;errorLabel_ 已预留固定高度, + // 不引发布局跳动)。复用同一 opacity effect,重复报错每次重新淡入。 + auto* fx = qobject_cast(errorLabel_->graphicsEffect()); + if (!fx) { + fx = new QGraphicsOpacityEffect(errorLabel_); + errorLabel_->setGraphicsEffect(fx); + } + auto* anim = new QPropertyAnimation(fx, "opacity", errorLabel_); + anim->setDuration(200); + anim->setStartValue(0.0); + anim->setEndValue(1.0); + anim->setEasingCurve(QEasingCurve::OutQuad); + anim->start(QAbstractAnimation::DeleteWhenStopped); } } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index b78e0bd..f9296e4 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -28,18 +28,25 @@ #include #include #include +#include +#include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -93,15 +100,90 @@ #include #include +#include +#include +#include #include #include #include +#include #include #include #include namespace { +// 居中浮层定位器:监视 host(中央 QVTK)尺寸/显示变化,把 overlay 浮层 +// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。 +// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。 +class CenterOverlay : public QObject { +public: + CenterOverlay(QWidget* overlay, QWidget* host) + : QObject(host), overlay_(overlay), host_(host) + { + host_->installEventFilter(this); + } + void reposition() + { + overlay_->adjustSize(); + const QSize h = host_->size(); + const QSize o = overlay_->size(); + overlay_->move(host_->x() + (h.width() - o.width()) / 2, + host_->y() + (h.height() - o.height()) / 2); + overlay_->raise(); + } + +protected: + bool eventFilter(QObject* obj, QEvent* e) override + { + if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show)) + reposition(); + return QObject::eventFilter(obj, e); + } + +private: + QWidget* overlay_; + QWidget* host_; +}; + +// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。 +// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve),单条 QVariantAnimation +// 逐帧驱动并 Render;结束回调锁定到目标态(防插值末值误差/残留半透明)。 +// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。 +void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw, + vtkSmartPointer fromCam, vtkSmartPointer toCam, + std::vector> actors, int durationMs, QObject* owner) +{ + auto interp = vtkSmartPointer::New(); + interp->SetInterpolationTypeToLinear(); + interp->AddCamera(0.0, fromCam); + interp->AddCamera(1.0, toCam); + + auto* anim = new QVariantAnimation(owner); + anim->setDuration(durationMs); + anim->setStartValue(0.0); + anim->setEndValue(1.0); + anim->setEasingCurve(QEasingCurve::OutCubic); + QObject::connect(anim, &QVariantAnimation::valueChanged, owner, + [interp, renderer, rw, actors](const QVariant& v) { + const double t = v.toDouble(); + interp->InterpolateCamera(t, renderer->GetActiveCamera()); + for (const auto& a : actors) + if (a) a->GetProperty()->SetOpacity(t); + renderer->ResetCameraClippingRange(); + rw->Render(); + }); + QObject::connect(anim, &QVariantAnimation::finished, owner, + [renderer, rw, actors, toCam]() { + renderer->GetActiveCamera()->DeepCopy(toCam); + for (const auto& a : actors) + if (a) a->GetProperty()->SetOpacity(1.0); + renderer->ResetCameraClippingRange(); + rw->Render(); + }); + anim->start(QAbstractAnimation::DeleteWhenStopped); +} + // 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。 std::string readPem(const std::string& path) { @@ -247,11 +329,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}" "QCheckBox:disabled{color:#9AA6B6;}")); auto* layerLayout = new QVBoxLayout(layerPanel); - layerLayout->setContentsMargins(13, 10, 15, 11); - layerLayout->setSpacing(6); + // 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。 + layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl, + geopro::app::space::kLg, geopro::app::space::kMl); + layerLayout->setSpacing(geopro::app::space::kSm); auto* layerTitle = new QLabel(QStringLiteral("视图详情")); layerTitle->setStyleSheet(QStringLiteral( - "font-weight:600;color:#2D6CB5;border:none;background:transparent;padding-bottom:3px;")); + "font-weight:%1;color:#2D6CB5;border:none;background:transparent;" + "padding-bottom:3px;font-size:%2px;") + .arg(geopro::app::type::kWeightSemibold) + .arg(geopro::app::type::kTitle)); auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)")); chkCurtain->setChecked(true); auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)")); @@ -278,6 +365,56 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re layerLayout->addWidget(chkTerrain); layerPanel->setVisible(false); // 默认二维,不显示图层浮层 + // ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。── + // 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中; + // 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。 + auto* emptyState = new QFrame(centerWidget); + emptyState->setObjectName(QStringLiteral("centralEmpty")); + emptyState->setAttribute(Qt::WA_TransparentForMouseEvents); + emptyState->setStyleSheet(QStringLiteral( + "#centralEmpty { background: transparent; }" + "#centralEmpty QLabel { background: transparent; }")); + auto* esLay = new QVBoxLayout(emptyState); + esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl, + geopro::app::space::kXl, geopro::app::space::kXl); + esLay->setSpacing(geopro::app::space::kMd); + esLay->setAlignment(Qt::AlignCenter); + + auto* esIcon = new QLabel(emptyState); + esIcon->setPixmap( + geopro::app::makeGlyph(geopro::app::Glyph::Dataset, QColor("#C2CCDA"), 56).pixmap(56, 56)); + esIcon->setAlignment(Qt::AlignCenter); + + auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState); + esTitle->setAlignment(Qt::AlignCenter); + esTitle->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") + .arg(geopro::app::type::kHeading) + .arg(geopro::app::type::kWeightSemibold)); + + auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n" + "切到「三维视图」可叠加帘面、体素与地形图层"), + emptyState); + esHint->setAlignment(Qt::AlignCenter); + esHint->setStyleSheet( + QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody)); + + esLay->addWidget(esIcon); + esLay->addWidget(esTitle); + esLay->addWidget(esHint); + + auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget); + emptyCentering->reposition(); + + // 引导层淡入(350ms,仅透明度,OutCubic):首屏空态出现的克制过渡,不阻塞任务。 + auto* esFx = new QGraphicsOpacityEffect(emptyState); + emptyState->setGraphicsEffect(esFx); + auto* esAnim = new QPropertyAnimation(esFx, "opacity", emptyState); + esAnim->setDuration(350); + esAnim->setStartValue(0.0); + esAnim->setEndValue(1.0); + esAnim->setEasingCurve(QEasingCurve::OutCubic); + esAnim->start(QAbstractAnimation::DeleteWhenStopped); + auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图")); vtkDock->setWidget(centerWidget); auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock); @@ -376,6 +513,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re {{geopro::app::Glyph::Filter, QStringLiteral("筛选")}, {geopro::app::Glyph::Plus, QStringLiteral("添加异常")}}); auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标 + // colorize(C):异常计数用语义 warning“需注意”变体(区别于普通中性计数徽标), + // 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。 + // 注:徽标的填充/显隐在 loadDataset 内(当前被 park),故此色与徽标本身同属休眠态, + // 接 dd 详情渲染那轮一并可见。 + if (anomalyBadge) { + anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn")); + anomalyBadge->style()->unpolish(anomalyBadge); + anomalyBadge->style()->polish(anomalyBadge); + } auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性")); rightDock->setWidget(anomalyPanel.container); @@ -393,12 +539,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。 // 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。 - // 注:AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。 - for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) { - d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures); - if (auto* area = d->dockAreaWidget()) - if (auto* bar = area->titleBar()) bar->setVisible(false); - } + // 抽成 lambda:ADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏, + // 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。 + const auto hideDockTitleBars = [&]() { + for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) { + d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures); + if (auto* area = d->dockAreaWidget()) + if (auto* bar = area->titleBar()) bar->setVisible(false); + } + }; + hideDockTitleBars(); // 中央编排已解耦到 CentralScene::rebuildCentralScene(数据驱动)。本轮空 sections → 空背景占位。 // 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。 @@ -416,16 +566,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto showElectrodes = std::make_shared(true); // 默认显示电极 ▼ auto showContour = std::make_shared(true); // 默认显示等值线 auto hiddenAnoms = std::make_shared>(); // 异常列表中被取消勾选(隐藏)的异常下标 + auto prevDsId = std::make_shared(); // 上次渲染的 DS id:判定“切换数据集”以触发揭示过渡 // 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。 // 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。 - auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode, - showAnomalies, showElectrodes, showContour, hiddenAnoms]() { + // overdrive(A):仅“切换数据集”这一加载时刻播放相机补间 + actor 淡入揭示;模式/叠加层开关 + // 属同一数据集内微调,直接落定不放动画(特殊时刻才特殊,避免每次交互都动的疲劳)。 + auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, detailWidget, currentDsId, + prevDsId, detailMode, showAnomalies, showElectrodes, showContour, + hiddenAnoms]() { + const bool dsChanged = (*currentDsId != *prevDsId); + const bool animate = dsChanged && !prevDsId->isEmpty() && !currentDsId->isEmpty(); + + // 过渡起点:清场景前先快照当前相机位姿。 + auto fromCam = vtkSmartPointer::New(); + fromCam->DeepCopy(detailRendererPtr->GetActiveCamera()); + detailRendererPtr->RemoveAllViewProps(); if (currentDsId->isEmpty()) { // 未选数据集:清空即可 + *prevDsId = *currentDsId; detailRenderWindowPtr->Render(); return; } + std::vector> added; // 本次加入的 actor,供淡入 const std::string id = currentDsId->toStdString(); if (*detailMode == DetailMode::Section18) { // 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。 @@ -435,10 +598,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (actors.bands) { actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0); detailRendererPtr->AddViewProp(actors.bands); + added.push_back(actors.bands); } if (actors.edges && *showContour) { actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0); detailRendererPtr->AddViewProp(actors.edges); + added.push_back(actors.edges); } // 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。 if (*showElectrodes) { @@ -446,6 +611,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (elec) { elec->SetScale(1.0, kVerticalExaggeration, 1.0); detailRendererPtr->AddViewProp(elec); + added.push_back(elec); } } } else { @@ -456,6 +622,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (a) { a->SetScale(1.0, kVerticalExaggeration, 1.0); detailRendererPtr->AddViewProp(a); + added.push_back(a); } } // 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。 @@ -466,12 +633,26 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) { act->SetScale(1.0, kVerticalExaggeration, 1.0); detailRendererPtr->AddViewProp(act); + added.push_back(act); } } } geopro::render::applyTop2D(detailRendererPtr); detailRendererPtr->ResetCamera(); - detailRenderWindowPtr->Render(); + *prevDsId = *currentDsId; + + if (animate) { + // 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。 + auto toCam = vtkSmartPointer::New(); + toCam->DeepCopy(detailRendererPtr->GetActiveCamera()); + for (const auto& a : added) a->GetProperty()->SetOpacity(0.0); + detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam); + detailRendererPtr->ResetCameraClippingRange(); + animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450, + detailWidget); + } else { + detailRenderWindowPtr->Render(); + } }; // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 @@ -503,9 +684,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re .arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax) .arg(anomalies.size())); }; - (void)loadDataset; // 暂未触发:保留待下一轮真实 DS 详情渲染复用 + // 暂未触发:保留待下一轮真实 DS 详情渲染复用。 + // TODO(overdrive-A 依赖):把下面数据集单击处理改调 loadDataset(dsId, name) 接通真实详情 + // 渲染后,rebuildDetail 里已就绪的“相机补间 + actor 淡入”揭示动画会在切换数据集时自动激活 + // (见 rebuildDetail 的 animate 分支与 animateReveal)。在此之前该动画为休眠态、不可见。 + (void)loadDataset; // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── + // 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。 QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) { if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { @@ -713,8 +899,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (stage == QStringLiteral("structure") || stage == QStringLiteral("projects")) objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg)); - window.statusBar()->showMessage( - QStringLiteral("加载失败(%1):%2").arg(stage, msg), 8000); + // 状态栏错误反馈:临时染 danger 红,8s 后随消息超时还原全局中性色。 + auto* sb = window.statusBar(); + sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}") + .arg(QString::fromUtf8(geopro::app::semantic::kDanger))); + sb->showMessage(QStringLiteral("加载失败(%1):%2").arg(stage, msg), 8000); + QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); }); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window, [](bool busy) { @@ -738,7 +928,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray(); if (!geo.isEmpty()) window.restoreGeometry(geo); const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray(); - if (!dockState.isEmpty()) dockManager->restoreState(dockState); + if (!dockState.isEmpty()) { + dockManager->restoreState(dockState); + // restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。 + hideDockTitleBars(); + } } // 退出时保存当前布局与几何(aboutToQuit 早于 window 析构,dockManager/window 仍存活)。 QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() { diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 4d14f23..2e330d4 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -50,7 +50,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { } lay->addWidget(tree_, 1); - hint_ = new QLabel(QStringLiteral("(加载中…)"), this); + hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); hint_->setAlignment(Qt::AlignCenter); hint_->setStyleSheet(QStringLiteral("color:#9AA6B6; padding:16px;")); hint_->setVisible(false); From 6df2c4832c47143a95296904f69f536b160a94d1 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 21:23:14 +0800 Subject: [PATCH 03/56] =?UTF-8?q?chore(ela):=20ElaWidgetTools=20=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=20spike=20+=20=E5=85=A8=E9=9D=A2=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=AE=A1=E5=88=92=20+=20=E6=9E=84=E5=BB=BA=20TEMP=20=E5=85=9C?= =?UTF-8?q?=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spike/ela: 隔离 demo 验证 ElaWindow + ADS 内嵌 + QVTK + 明暗切换(Qt6.11.1/MSVC 构建通过) - CMakeLists: FetchContent 引入 ElaWidgetTools(fork,SOURCE_SUBDIR 仅编库) + 挂 spike - build.bat: TEMP/TMP 重定向到 D: 构建目录,规避 C: 盘满导致的 LNK1108 - docs: 全面 Ela 化迁移计划(P0-P4 + 控件映射表 + 风险登记) --- CMakeLists.txt | 14 ++ build.bat | 6 + .../2026-06-09-elawidgettools-migration.md | 83 ++++++++++++ spike/ela/CMakeLists.txt | 18 +++ spike/ela/main.cpp | 121 ++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-elawidgettools-migration.md create mode 100644 spike/ela/CMakeLists.txt create mode 100644 spike/ela/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index da3abd8..7d53f9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,21 @@ 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 main + SOURCE_SUBDIR ElaWidgetTools) +FetchContent_MakeAvailable(elawidgettools) + add_subdirectory(src) +# ElaWidgetTools 评估 spike(隔离 demo,不属于产品 geopro_desktop;评估完删分支即弃)。 +add_subdirectory(spike/ela) + enable_testing() add_subdirectory(tests) diff --git a/build.bat b/build.bat index 86db412..90898ea 100644 --- a/build.bat +++ b/build.bat @@ -19,6 +19,12 @@ set "ROOT=%~dp0" set "BUILDDIR=%ROOT%build\release" set "PRESET=msvc-release" +REM 把临时目录指向 D: 的构建目录,规避 C: 盘满导致链接器写 %TEMP% 失败(LNK1108)。 +REM 仅作用于本次构建(setlocal 作用域),不污染用户 shell。注意:仍建议尽快清理 C: 盘。 +set "TEMP=%BUILDDIR%\tmp" +set "TMP=%BUILDDIR%\tmp" +if not exist "%TEMP%" mkdir "%TEMP%" + REM --- locate Visual Studio (vswhere lives at a fixed path) --- set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" if not exist "%VSWHERE%" ( diff --git a/docs/superpowers/plans/2026-06-09-elawidgettools-migration.md b/docs/superpowers/plans/2026-06-09-elawidgettools-migration.md new file mode 100644 index 0000000..cc00f11 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-elawidgettools-migration.md @@ -0,0 +1,83 @@ +# geopro_desktop → ElaWidgetTools (Fluent) 迁移计划 + +**分支**:`feat/elawidgettools` **日期**:2026-06-09 **决策**:全面 Ela 化(最彻底),支持明/暗主题。 +**前置评估已完成**:spike(`spike/ela/`) 证明 ElaWidgetTools(RainbowCandyX fork) 可用官方 Qt 6.11.1 + MSVC 经 FetchContent 构建,ElaWindow + ADS 内嵌 + QVTK 渲染均可行;ElaTheme 明暗切换可用,但**只自动覆盖 Ela\* 控件与 ElaWindow 外壳**,标准 QWidget/ADS/VTK 需手工主题联动。 + +--- + +## 0. 硬前提(动手前必须满足) + +- **P0-a 清理 C: 盘**:当前 C: 0 GB 可用,链接器写 `%TEMP%` 失败(`LNK1108`)。迁移需大量构建验证。需用户清理 C:;我同时把 `TEMP/TMP→D:` 兜底固化进 `build.bat`,避免反复手动重定向。 +- **P0-b 验证靠用户**:每阶段我构建通过后,用户运行 + 截图,我据反馈迭代(登录门槛 + GUI,我无法目视)。 +- **P0-c 回退保障**:全程保留 `GEOPRO_UI_SHELL=classic|ela` 环境变量开关,可在「现 QMainWindow 壳」与「ElaWindow 壳」间切换,便于 A/B 与回退;迁移稳定后再移除。 + +## 1. 依赖固化(P0 工程) + +- ElaWidgetTools 从 spike 提升为**正式依赖**:`FetchContent` 的 `GIT_TAG` 由 `main` **钉到具体 commit**(可复现)。 +- **静/动态**:先静态(MIT,省 DLL);若遇静态资源(字体/SVG 图标 .qrc)被剥离导致图标缺失,改动态(`ELAWIDGETTOOLS_BUILD_STATIC_LIB OFF` + DLL 随 `TARGET_RUNTIME_DLLS` 拷贝)。 +- **插件部署**:把 `platforms / styles / imageformats / iconengines`(含 SVG 图标用的 `qsvg`/`qsvgicon`)部署接进 `geopro_desktop` 的 post-build(今天 spike 是手动拷的,要正式化;windeployqt 会被 ADS 的 DLL 依赖卡住,改为显式 copy Qt plugins)。 + +## 2. P1 — 换壳(带开关) + +- 新建 `ElaShellWindow`(继承 `ElaWindow`)或在 `main()` 分支构造。把现有 `buildWorkbench()` 产出的中心内容(ADS `CDockManager` + 工具条)作为 ElaWindow 的一个 page/central content 挂入(`addPageNode` / `setCentralCustomWidget`)。 +- `eApp->init()` 在 `QApplication` 后调用;保留高 DPI 与 QVTK surface format 设置顺序。 +- **dock 持久化注意**:ElaWindow 接管后,`restoreState` 后重隐藏 ADS 标题栏的时序修复(已在主分支)需在新壳下复核。 +- **验收**:`GEOPRO_UI_SHELL=ela` 启动 → 登录 → 工作台;ADS 停靠可拖动;中央/详情 VTK 正常;导航/标题栏 Fluent 外观。截图确认。 + +## 3. P2 — 主题桥(明/暗覆盖所有非 Ela 面) + +- 新建 `ThemeBridge`:监听 `ElaTheme::themeModeChanged`,把明/暗同步到: + 1. **全局 QSS**:把 `Theme.cpp` 的 `kStyleSheet` 拆成「明」与「暗」两版(用已有 `type/space/radius/semantic` 令牌派生暗色盘),按主题切换。 + 2. **ADS 停靠区**:`CDockManager::setStyleSheet` 明/暗两套。 + 3. **VTK 背景**:中央 + 详情 renderer 背景随主题切深/浅底并 `Render()`。 + 4. **内联样式面板**:PanelHeader / TopBar / LoginWindow / main 浮层 的内联 QSS 改为「跟随主题」(去硬编码色,引用桥提供的明/暗令牌)。 +- **暗色盘设计**:在 `Theme.hpp` 增加暗色语义(surface/ink/border/accent 的暗版),保持品牌蓝在暗底的可读性与对比度(≥4.5:1)。 +- **验收**:明/暗一键切换,全界面(外壳+停靠+面板+VTK)协调一致、无残留亮/暗块;对比度达标。截图明、暗各一。 + +## 4. P3 — 全面控件 Ela 化(工作量主体) + +逐面替换标准控件为 `Ela*` 等价物,"白嫖"明暗与 Fluent 观感。映射(精确 Ela 类名在实施时按头文件确认): + +| 现状 | → Ela 等价 | 所在 | +|---|---|---| +| QPushButton | ElaPushButton | LoginWindow / 各处 | +| QLineEdit | ElaLineEdit | LoginWindow / ProjectListDialog 过滤 | +| QCheckBox | ElaCheckBox | LoginWindow / 图层浮层 / 异常列表 | +| QComboBox | ElaComboBox | ProjectListDialog / 全局 | +| QLabel(文本) | ElaText | 各处文本/标题 | +| QToolButton(Tab/操作) | ElaToolButton / ElaIconButton | PanelHeader / TopBar | +| QMenuBar / QMenu | ElaMenuBar / ElaMenu | TopBar | +| QTreeWidget | ElaTreeView(+model) 或保留+主题联动 | ObjectTreePanel | +| QListWidget | ElaListView(+model) 或保留+联动 | Dataset/Anomaly 面板 | +| QTableWidget | ElaTableView 或保留+联动 | ProjectListDialog | +| QProgressBar | ElaProgressBar | 全局 | +| QStatusBar | ElaStatusBar 或保留+联动 | main | +| QTabWidget/分段 | ElaTabWidget / ElaToggleSwitch | PanelHeader 数据/文件 | +| QDialog(登录) | ElaWidget/ElaWindow 风格弹窗 | LoginWindow | +| **保留(无替代)** | QVTKOpenGLStereoWidget、ADS CDockManager | 中央/详情/停靠 | + +- 树/列表/表若用 Ela 的 View 需改 model(成本高),可分两步:先保留 widget 版做主题联动,后续再评估换 View。 +- 登录窗重做为 Fluent 风格(沿用现有令牌与文案)。 + +## 5. P4 — 收尾 + +- 插件部署正式化、ElaWidgetTools 版本锁定、静态资源核验。 +- 去掉 `GEOPRO_UI_SHELL` 过渡开关(确认稳定后)。 +- 开源声明:ElaWidgetTools(MIT)、ADS(LGPLv2.1)、Qt(LGPL) NOTICE 归集。 +- 回归:登录、项目切换、对象树、数据集/文件分页、异常、VTK 各视图、dock 持久化。 + +## 风险登记 + +| 风险 | 缓解 | +|---|---| +| Qt 6.11 Windows Popup 渲染(作者红旗) | spike 已初验;P1 重点复核菜单/下拉/提示;必要时打 fork 的条件补丁 | +| ADS 在 ElaWindow 内主题/交互异常 | spike 已验内嵌;P2 专门做 ADS 明暗 QSS | +| Ela View 需 model 重写(树/列表/表) | 分步:先 widget 版主题联动,再评估换 View | +| 静态库资源剥离(图标/字体缺失) | 改动态库 | +| 我无法目视 | 每阶段用户运行+截图验收 | +| C: 满导致构建反复失败 | 清理 C: + TEMP→D: 固化进 build.bat | +| 大重构回归 | 全程 env 开关可回退;主分支零影响 | + +## 执行顺序 + +P0 → P1(验收)→ P2(验收)→ P3(按面板分批,每批验收)→ P4。每步构建通过后由用户运行+截图确认再进下一步。 diff --git a/spike/ela/CMakeLists.txt b/spike/ela/CMakeLists.txt new file mode 100644 index 0000000..4d48f0b --- /dev/null +++ b/spike/ela/CMakeLists.txt @@ -0,0 +1,18 @@ +# ElaWidgetTools 评估 spike(隔离 demo,独立 exe)。仅 feat/elawidgettools 分支评估用, +# 与产品 geopro_desktop 完全解耦:链 ElaWidgetTools(Fluent 库) + ADS + VTK,验证可行性与观感。 +add_executable(geopro_ela_spike WIN32 main.cpp) + +target_link_libraries(geopro_ela_spike PRIVATE + Qt6::Core Qt6::Gui Qt6::Widgets + ElaWidgetTools + ads::qt6advanceddocking + ${VTK_LIBRARIES}) + +vtk_module_autoinit(TARGETS geopro_ela_spike MODULES ${VTK_LIBRARIES}) + +if(WIN32) + add_custom_command(TARGET geopro_ela_spike POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ $ + COMMAND_EXPAND_LISTS) +endif() diff --git a/spike/ela/main.cpp b/spike/ela/main.cpp new file mode 100644 index 0000000..77f3793 --- /dev/null +++ b/spike/ela/main.cpp @@ -0,0 +1,121 @@ +// ElaWidgetTools 评估 spike(隔离 demo,不属于产品 app,仅 feat/elawidgettools 分支评估用)。 +// 一锤定音验证四件事: +// ① 用你们官方 Qt 6.11.1 + MSVC 经 FetchContent 能否构建 ElaWidgetTools(RainbowCandyX fork); +// ② ElaWindow 的 Fluent 观感在你们机器上渲染是否正常(重点看 Qt6.11 的 Popup/弹窗); +// ③ Qt Advanced Docking System(ADS) 能否内嵌进 ElaWindow; +// ④ QVTKOpenGLStereoWidget 视口在 ElaWindow + ADS 内能否正常渲染。 +// 结论决定是否值得对真实 app 做外壳重构。 + +#include +#include +#include +#include +#include + +#include "ElaApplication.h" +#include "ElaDef.h" +#include "ElaPushButton.h" +#include "ElaText.h" +#include "ElaTheme.h" +#include "ElaWindow.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) +{ + QApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); + + QApplication app(argc, argv); + eApp->init(); // ElaApplication 初始化(Fluent 主题/字体/动画基建) + + ElaWindow window; + window.setWindowTitle(QStringLiteral("ElaWidgetTools Spike — Fluent + ADS + VTK")); + window.resize(1200, 760); + + // 「工作台」页内嵌 ADS 停靠管理器:验证 ADS 能否在 ElaWindow 内正常工作。 + auto* dockHost = new QWidget; + auto* hostLay = new QVBoxLayout(dockHost); + hostLay->setContentsMargins(0, 0, 0, 0); + auto* dockManager = new ads::CDockManager(dockHost); + hostLay->addWidget(dockManager); + + // dock1:Fluent 控件样例(看观感:ElaText / ElaPushButton 与标准控件对照)。 + auto* sample = new QWidget; + sample->setObjectName(QStringLiteral("sampleHost")); + auto* sLay = new QVBoxLayout(sample); + sLay->setContentsMargins(16, 16, 16, 16); + sLay->setSpacing(12); + sLay->addWidget(new ElaText(QStringLiteral("ElaText —— Fluent 文本"), sample)); + sLay->addWidget(new ElaPushButton(QStringLiteral("ElaPushButton 主操作"), sample)); + sLay->addWidget(new QLabel(QStringLiteral("(对照)标准 QLabel"), sample)); + + // 浅/深主题切换:ElaWidgetTools 内置 ElaTheme,运行期一键切换整套 Fluent 主题。 + auto* themeBtn = new ElaPushButton(QStringLiteral("切换 浅色 / 深色"), sample); + QObject::connect(themeBtn, &QPushButton::clicked, themeBtn, [] { + eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark + : ElaThemeType::Light); + }); + sLay->addWidget(themeBtn); + sLay->addStretch(); + auto* d1 = new ads::CDockWidget(QStringLiteral("Fluent 控件")); + d1->setWidget(sample); + dockManager->addDockWidget(ads::LeftDockWidgetArea, d1); + + // dock2:QVTK 视口(蓝色锥体),验证 VTK 在 ElaWindow + ADS 内渲染。 + auto* vtkWidget = new QVTKOpenGLStereoWidget; + vtkNew renderWindow; + vtkNew renderer; + renderer->SetBackground(1.0, 1.0, 1.0); + vtkWidget->setRenderWindow(renderWindow); + renderWindow->AddRenderer(renderer); + vtkNew cone; + cone->SetResolution(48); + vtkNew mapper; + mapper->SetInputConnection(cone->GetOutputPort()); + vtkNew actor; + actor->SetMapper(mapper); + actor->GetProperty()->SetColor(0.18, 0.42, 0.71); // 品牌蓝 #2D6CB5 近似 + renderer->AddActor(actor); + renderer->ResetCamera(); + auto* d2 = new ads::CDockWidget(QStringLiteral("VTK 视口(锥体)")); + d2->setWidget(vtkWidget); + dockManager->addDockWidget(ads::RightDockWidgetArea, d2); + + // 关键发现演示:ADS 停靠区与普通 QWidget 不会自动跟随 ElaTheme(只有 Ela* 控件与 + // ElaWindow 外壳跟随)。这里手动把「停靠区背景 + 普通容器 + VTK 背景」同步到当前主题, + // 并监听 ElaTheme::themeModeChanged。这段「同步」正是真集成时要为每个非 Ela 面板付出的成本。 + auto* rendererPtr = renderer.Get(); + auto* rwPtr = renderWindow.Get(); + auto applyContentTheme = [dockManager, sample, rendererPtr, rwPtr](ElaThemeType::ThemeMode mode) { + const bool dark = (mode == ElaThemeType::Dark); + const QString bg = dark ? QStringLiteral("#1E1F22") : QStringLiteral("#FFFFFF"); + const QString fg = dark ? QStringLiteral("#E3E3E3") : QStringLiteral("#1F2A3D"); + dockManager->setStyleSheet( + QStringLiteral("ads--CDockAreaWidget, ads--CDockContainerWidget { background:%1; }") + .arg(bg)); + sample->setStyleSheet( + QStringLiteral("#sampleHost { background:%1; } #sampleHost QLabel { color:%2; }") + .arg(bg, fg)); + rendererPtr->SetBackground(dark ? 0.11 : 1.0, dark ? 0.12 : 1.0, dark ? 0.14 : 1.0); + rwPtr->Render(); + }; + QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager, + [applyContentTheme](ElaThemeType::ThemeMode m) { applyContentTheme(m); }); + applyContentTheme(eTheme->getThemeMode()); // 初始同步当前主题 + + window.addPageNode(QStringLiteral("工作台"), dockHost); + window.show(); + return app.exec(); +} From c4d76f57b6c9c1eed3f47141ae0213a2186f1f17 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 21:24:28 +0800 Subject: [PATCH 04/56] =?UTF-8?q?=E6=96=B0=E5=A2=9Eclaude.md(karpathy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..daced9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. From fc282824b98243163cb2a9d2b3cc76e13dcb0eb1 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 21:34:27 +0800 Subject: [PATCH 05/56] =?UTF-8?q?feat(ela):=20P1=20=E6=8D=A2=E5=A3=B3=20?= =?UTF-8?q?=E2=80=94=20ElaWindow=20=E5=8C=85=E8=A3=B9=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0(GEOPRO=5FUI=5FSHELL=3Dela=20=E5=BC=80=E5=85=B3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/app/CMakeLists: 链接 ElaWidgetTools + 部署 Qt 插件(platforms/imageformats/iconengines/styles)到 exe 旁 - main.cpp: GEOPRO_UI_SHELL=ela 时用 ElaWindow.setCentralCustomWidget 包裹内层 QMainWindow 承载工作台; 经典壳为默认且行为不变(仅改堆分配); buildWorkbench 零改动; 全程可回退 - build.bat: 还原(撤销上次破坏脚本解析的中文 TEMP 注释); 构建临时目录改由调用方 TEMP->D: 设置 --- build.bat | 6 ------ src/app/CMakeLists.txt | 11 +++++++++++ src/app/main.cpp | 37 ++++++++++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/build.bat b/build.bat index 90898ea..86db412 100644 --- a/build.bat +++ b/build.bat @@ -19,12 +19,6 @@ set "ROOT=%~dp0" set "BUILDDIR=%ROOT%build\release" set "PRESET=msvc-release" -REM 把临时目录指向 D: 的构建目录,规避 C: 盘满导致链接器写 %TEMP% 失败(LNK1108)。 -REM 仅作用于本次构建(setlocal 作用域),不污染用户 shell。注意:仍建议尽快清理 C: 盘。 -set "TEMP=%BUILDDIR%\tmp" -set "TMP=%BUILDDIR%\tmp" -if not exist "%TEMP%" mkdir "%TEMP%" - REM --- locate Visual Studio (vswhere lives at a fixed path) --- set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" if not exist "%VSWHERE%" ( diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index fa3f1e2..11fc9a0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -35,6 +35,7 @@ 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 上色 @@ -51,4 +52,14 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ COMMAND_EXPAND_LISTS) + + # 运行期 Qt 插件部署:platforms 必需;imageformats/iconengines 供 ElaWidgetTools 的 SVG 图标; + # styles 备用。从链接的 Qt6::Core 推导 Qt 安装的 plugins 目录,拷到 exe 旁(windeployqt 会被 + # ADS 的非 Qt DLL 依赖卡住,故改为显式 copy)。 + foreach(_pl platforms styles imageformats iconengines) + add_custom_command(TARGET geopro_desktop POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "$/../plugins/${_pl}" + "$/${_pl}") + endforeach() endif() diff --git a/src/app/main.cpp b/src/app/main.cpp index f9296e4..193ea73 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -58,6 +58,9 @@ #include #include +#include +#include + #include "model/ColorScale.hpp" #include "model/Field.hpp" #include "repo/LocalSampleRepository.hpp" @@ -1007,13 +1010,33 @@ int main(int argc, char* argv[]) geopro::data::ApiProjectRepository projectRepo(api); geopro::controller::WorkbenchNavController nav(projectRepo); - QMainWindow window; - window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); - window.resize(1280, 800); - window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸 - - buildWorkbench(window, repo, projectRepo, nav); - window.show(); + // ── 外壳选择(P1 迁移,可回退):GEOPRO_UI_SHELL=ela → Fluent ElaWindow 壳; + // 未设/其它 → 经典 QMainWindow 壳。两者都复用同一 buildWorkbench(QMainWindow&)。 + // Ela 壳策略:ElaWindow 用 setCentralCustomWidget 包裹一个承载工作台的内层 QMainWindow + // (buildWorkbench 依赖 QMainWindow 的 setCentralWidget/setMenuWidget/statusBar, + // ElaWindow 自身将其设为私有,故用内层 QMainWindow 承接,零改 buildWorkbench)。 + const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"); + QWidget* topLevel = nullptr; + if (qEnvironmentVariable("GEOPRO_UI_SHELL") == QLatin1String("ela")) { + eApp->init(); // ElaApplication:Fluent 主题/字体/动画基建 + auto* inner = new QMainWindow; + buildWorkbench(*inner, repo, projectRepo, nav); + auto* ela = new ElaWindow; + ela->setWindowTitle(kTitle); + ela->resize(1280, 800); + ela->setMinimumSize(1024, 680); + ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏 + ela->setCentralCustomWidget(inner); // 工作台作为 ElaWindow 中心内容 + topLevel = ela; + } else { + auto* window = new QMainWindow; + window->setWindowTitle(kTitle); + window->resize(1280, 800); + window->setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸 + buildWorkbench(*window, repo, projectRepo, nav); + topLevel = window; + } + topLevel->show(); nav.start(); // 进入工作台后拉真实 空间/项目/结构 From 8d938dd848fcd8616b06718a89fe48636c226b8e Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 21:39:47 +0800 Subject: [PATCH 06/56] =?UTF-8?q?fix(ela):=20P1=20code=20review=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H1: ElaWidgetTools GIT_TAG 钉到 b80eadc(可复现) - H3: 内层 QMainWindow 以 ElaWindow 为父构造(避免无父期调色板/DPI 抖动) - M4: spike 改 option(GEOPRO_BUILD_ELA_SPIKE) 守卫,默认不编 - 备注: ctest 53/53 通过; C2(applyTheme 顺序)为 P1 有意共存,P2 主题桥统一; H2/M1 记入 P4 --- CMakeLists.txt | 9 ++++++--- src/app/main.cpp | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d53f9b..2357669 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,14 +70,17 @@ FetchContent_MakeAvailable(qtkeychain) set(ELAWIDGETTOOLS_BUILD_STATIC_LIB ON CACHE BOOL "" FORCE) FetchContent_Declare(elawidgettools GIT_REPOSITORY https://github.com/RainbowCandyX/ElaWidgetTools.git - GIT_TAG main + GIT_TAG b80eadc4a199186e14656dce09959b3216a593be # 钉定提交,构建可复现(review H1) SOURCE_SUBDIR ElaWidgetTools) FetchContent_MakeAvailable(elawidgettools) add_subdirectory(src) -# ElaWidgetTools 评估 spike(隔离 demo,不属于产品 geopro_desktop;评估完删分支即弃)。 -add_subdirectory(spike/ela) +# 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/src/app/main.cpp b/src/app/main.cpp index 193ea73..e842a3b 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1019,13 +1019,13 @@ int main(int argc, char* argv[]) QWidget* topLevel = nullptr; if (qEnvironmentVariable("GEOPRO_UI_SHELL") == QLatin1String("ela")) { eApp->init(); // ElaApplication:Fluent 主题/字体/动画基建 - auto* inner = new QMainWindow; - buildWorkbench(*inner, repo, projectRepo, nav); auto* ela = new ElaWindow; ela->setWindowTitle(kTitle); ela->resize(1280, 800); ela->setMinimumSize(1024, 680); ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏 + auto* inner = new QMainWindow(ela); // 以 ela 为父,避免无父期调色板/DPI 抖动(review H3) + buildWorkbench(*inner, repo, projectRepo, nav); ela->setCentralCustomWidget(inner); // 工作台作为 ElaWindow 中心内容 topLevel = ela; } else { From af0012fd7008e95af6e0a4e42577dfcd15fe37a9 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 07:38:41 +0800 Subject: [PATCH 07/56] =?UTF-8?q?fix(ela):=20P1=20=E5=BA=95=E9=83=A8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=8F=E8=B4=B4=E5=BA=95=E8=BE=B9=20?= =?UTF-8?q?=E2=80=94=20setCentralCustomWidget=20=E6=94=B9=20addPageNode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ElaCentralStackedWidget::setCustomWidget 用 insertWidget(0,...) 把控件插到页栈容器之上, 被禁用的空导航页栈仍占底部空间,状态栏不贴底边。改用 addPageNode 把工作台作为唯一页面 放进中心页栈,填满到底边。 --- src/app/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index e842a3b..51a9377 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1026,7 +1026,10 @@ int main(int argc, char* argv[]) ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏 auto* inner = new QMainWindow(ela); // 以 ela 为父,避免无父期调色板/DPI 抖动(review H3) buildWorkbench(*inner, repo, projectRepo, nav); - ela->setCentralCustomWidget(inner); // 工作台作为 ElaWindow 中心内容 + // 用 addPageNode 把工作台作为唯一页面放进中心页栈(填满到底边)。 + // 注意:不能用 setCentralCustomWidget——它把控件插到页栈容器“之上”,空页栈仍占底部, + // 导致状态栏不贴底边(见 ElaCentralStackedWidget::setCustomWidget 的 insertWidget(0,...))。 + ela->addPageNode(kTitle, inner); topLevel = ela; } else { auto* window = new QMainWindow; From 26404cee2f17068da8c4f0c5b9432dfb3c221969 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 07:43:46 +0800 Subject: [PATCH 08/56] =?UTF-8?q?feat(ela):=20P2=20=E6=9A=97=E8=89=B2?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E6=A1=A5=20=E2=80=94=20ElaTheme=20=E6=98=8E/?= =?UTF-8?q?=E6=9A=97=20=E2=86=92=20=E5=85=A8=E5=B1=80=20QSS+=E8=B0=83?= =?UTF-8?q?=E8=89=B2=E6=9D=BF=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Theme.cpp: kDarkMap 浅→暗 hex 映射, 复用 kStyleSheet 结构生成暗色 QSS(含 ads--*); buildPalette(dark) 暗色调色板; applyThemeMode(app,dark); applyTheme=浅色快捷入口 - main.cpp(ela 分支): 连 ElaTheme::themeModeChanged → applyThemeMode 同步非 Ela 面; 初始对齐(解 review C2); Ctrl+Shift+T 切换(正式按钮待 P3 TopBar Ela 化) - VTK 视口背景暗色联动记入 P4(渲染器在 buildWorkbench 内, 需另接) --- src/app/Theme.cpp | 152 ++++++++++++++++++++++++++++++++-------------- src/app/Theme.hpp | 6 +- src/app/main.cpp | 17 ++++++ 3 files changed, 130 insertions(+), 45 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index ede5b4d..599c329 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -369,67 +369,131 @@ ads--CDockWidgetTab[activeTab="true"] QLabel { } )QSS"; -// 浅色专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。 -QPalette buildPalette() +// ── 暗色映射(P2 主题桥):把浅色 QSS 里每个浅色令牌就近替换为暗色等价, +// 复用同一 kStyleSheet 结构(选择器/度量不变),仅换色。ads--* 规则同在 kStyleSheet 内, +// 故停靠区也随之暗化。品牌蓝在暗底略提亮以保对比度;danger 同理。 +struct HexPair { + const char* light; + const char* dark; +}; +constexpr HexPair kDarkMap[] = { + {"#1F2A3D", "#E3E5E8"}, // 主文字 → 亮 + {"#F4F6FA", "#1E1F22"}, // 外壳底 + {"#FFFFFF", "#2B2D30"}, // 面板白 + {"#EDF1F7", "#323539"}, // 抬升/表头 + {"#EAEEF4", "#3A3D42"}, // 细分隔/边框线 + {"#EEF3FB", "#3A3D42"}, // 悬停底 + {"#DCE9F8", "#2E3F54"}, // 选中行底 + {"#1B3D67", "#CFE0F5"}, // 选中行文字 + {"#3A475C", "#C4C9D0"}, // 表头文字 + {"#5A6B85", "#A9B0BC"}, // 次要文字 + {"#D5DBE5", "#3A3D42"}, // 边框 + {"#C2CCDA", "#4A4E55"}, // 强边框/滚动条柄 + {"#C7D2E0", "#4A4E55"}, // 输入边框 + {"#2D6CB5", "#4A90E2"}, // 强调(品牌蓝,暗底提亮) + {"#2862A6", "#5C9CE8"}, // 强调悬停 + {"#234F87", "#3A7BC8"}, // 强调按下 + {"#9AA6B6", "#6E747C"}, // 禁用文字 + {"#F0F2F6", "#26282B"}, // 禁用底 + {"#8A93A3", "#7A8088"}, // 禁用文字2 + {"#DCE0E7", "#3A3D42"}, // 禁用边框 + {"#A7B4C7", "#5A5F66"}, // 滚动条柄悬停 + {"#E1E6EE", "#3A3D42"}, // 菜单分隔 + {"#DCE6F4", "#34373C"}, // 菜单栏选中 + {"#E6EBF3", "#3A3D42"}, // 进度条底 + {"#EAF1FB", "#2E3F54"}, // 工具按钮选中底 + {"#C0392B", "#E0685C"}, // 危险(暗底提亮) + {"#E6EAF1", "#3A3D42"}, // 面板表头分隔(与 PanelHeader 一致) +}; + +// 浅/暗专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。 +QPalette buildPalette(bool dark) { QPalette p; - const QColor shell("#F4F6FA"); - const QColor panel("#FFFFFF"); - const QColor text("#1F2A3D"); - const QColor mutedText("#5A6B85"); - const QColor accent("#2D6CB5"); - - p.setColor(QPalette::Window, shell); - p.setColor(QPalette::WindowText, text); - p.setColor(QPalette::Base, panel); - p.setColor(QPalette::AlternateBase, QColor("#F0F3F8")); - p.setColor(QPalette::Text, text); - p.setColor(QPalette::Button, QColor("#EDF1F7")); - p.setColor(QPalette::ButtonText, text); - p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D")); - p.setColor(QPalette::ToolTipText, shell); - p.setColor(QPalette::Highlight, accent); - p.setColor(QPalette::HighlightedText, panel); - p.setColor(QPalette::PlaceholderText, mutedText); - p.setColor(QPalette::Link, accent); - - // 关键:把 Fusion 用于绘制 3D 凹凸(斜角/凹槽/分隔条阴影)的明暗角色统一压成相近浅灰, - // 立体效果即塌成平面。ADS 分隔条用 palette(dark),这样也变成一条扁平浅灰细线(无 3D)。 - p.setColor(QPalette::Light, QColor("#FFFFFF")); - p.setColor(QPalette::Midlight, QColor("#EEF1F5")); - p.setColor(QPalette::Mid, QColor("#E1E6EE")); - p.setColor(QPalette::Dark, QColor("#D7DEE8")); - p.setColor(QPalette::Shadow, QColor("#D7DEE8")); - - // 禁用态:统一灰化,避免 Fusion 默认禁用色偏暗看不清。 - p.setColor(QPalette::Disabled, QPalette::Text, QColor("#9AA6B6")); - p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#9AA6B6")); - p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#9AA6B6")); + if (!dark) { + const QColor shell("#F4F6FA"), panel("#FFFFFF"), text("#1F2A3D"), + mutedText("#5A6B85"), accent("#2D6CB5"); + p.setColor(QPalette::Window, shell); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, panel); + p.setColor(QPalette::AlternateBase, QColor("#F0F3F8")); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, QColor("#EDF1F7")); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D")); + p.setColor(QPalette::ToolTipText, shell); + p.setColor(QPalette::Highlight, accent); + p.setColor(QPalette::HighlightedText, panel); + p.setColor(QPalette::PlaceholderText, mutedText); + p.setColor(QPalette::Link, accent); + // 把 Fusion 的明暗 3D 角色压成相近浅灰,立体效果塌成平面。 + p.setColor(QPalette::Light, QColor("#FFFFFF")); + p.setColor(QPalette::Midlight, QColor("#EEF1F5")); + p.setColor(QPalette::Mid, QColor("#E1E6EE")); + p.setColor(QPalette::Dark, QColor("#D7DEE8")); + p.setColor(QPalette::Shadow, QColor("#D7DEE8")); + p.setColor(QPalette::Disabled, QPalette::Text, QColor("#9AA6B6")); + p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#9AA6B6")); + p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#9AA6B6")); + } else { + const QColor shell("#1E1F22"), panel("#2B2D30"), text("#E3E5E8"), + mutedText("#A9B0BC"), accent("#4A90E2"); + p.setColor(QPalette::Window, shell); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, panel); + p.setColor(QPalette::AlternateBase, QColor("#303236")); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, QColor("#323539")); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::ToolTipBase, QColor("#E3E5E8")); + p.setColor(QPalette::ToolTipText, QColor("#1E1F22")); + p.setColor(QPalette::Highlight, accent); + p.setColor(QPalette::HighlightedText, QColor("#0E1013")); + p.setColor(QPalette::PlaceholderText, mutedText); + p.setColor(QPalette::Link, accent); + p.setColor(QPalette::Light, QColor("#34373C")); + p.setColor(QPalette::Midlight, QColor("#303236")); + p.setColor(QPalette::Mid, QColor("#3A3D42")); + p.setColor(QPalette::Dark, QColor("#3A3D42")); + p.setColor(QPalette::Shadow, QColor("#3A3D42")); + p.setColor(QPalette::Disabled, QPalette::Text, QColor("#6E747C")); + p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#6E747C")); + p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#6E747C")); + } return p; } +// 当前模式的全局 QSS:暗色 = 在浅色 kStyleSheet 上逐一替换色令牌。 +QString styleSheetForMode(bool dark) +{ + QString s = QString::fromUtf8(kStyleSheet); + if (dark) { + for (const auto& hp : kDarkMap) + s.replace(QLatin1String(hp.light), QLatin1String(hp.dark)); + } + return s; +} + } // namespace -void applyTheme(QApplication& app) +void applyThemeMode(QApplication& app, bool dark) { // Fusion:跨平台一致且对 QSS 友好(Windows 原生风对部分控件会忽略样式表)。 app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); - // 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。 - // 基准字号取排版令牌 type::kBody(13px)——统一为 px,与 QSS 同单位 - // (旧值 10pt≈13.3px,观感几乎不变);9pt 偏小显拥挤。抗锯齿优先,观感更精致。 + // 基础字体:微软雅黑 UI;基准字号取令牌 type::kBody(13px),与 QSS 同单位。 QFont base(QStringLiteral("Microsoft YaHei UI")); base.setPixelSize(type::kBody); base.setStyleStrategy(QFont::PreferAntialias); app.setFont(base); - app.setPalette(buildPalette()); + app.setPalette(buildPalette(dark)); + app.setStyleSheet(styleSheetForMode(dark)); +} - // 注意:不要给 ADS 停靠标题(ads--CDockWidgetTab QLabel)追加任何样式—— - // 这些子窗口标题栏在 main.cpp 里被 setVisible(false) 刻意隐藏(表头由各面板 - // 自绘的 PanelHeader 承担)。改写其字号/内边距会变更度量并触发 ADS 重新 - // 评估标题栏可见性,把隐藏的标题又显示出来。字号统一只作用于可见控件。 - app.setStyleSheet(QString::fromUtf8(kStyleSheet)); +void applyTheme(QApplication& app) +{ + applyThemeMode(app, false); } } // namespace geopro::app diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 71a51d0..385b005 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -80,7 +80,11 @@ inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kW } // namespace semantic -// 应用浅色专业主题(Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。 +// 应用专业主题(Fusion + 调色板 + 全局样式表)。dark=true 走暗色(P2 主题桥用)。 +// 暗色复用同一 QSS 结构,仅按 kDarkMap 换色;幂等,可随主题切换重复调用。 +void applyThemeMode(QApplication& app, bool dark); + +// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。 void applyTheme(QApplication& app); } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 51a9377..3fb41bd 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -36,7 +36,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -59,6 +61,8 @@ #include #include +#include +#include #include #include "model/ColorScale.hpp" @@ -1030,6 +1034,19 @@ int main(int argc, char* argv[]) // 注意:不能用 setCentralCustomWidget——它把控件插到页栈容器“之上”,空页栈仍占底部, // 导致状态栏不贴底边(见 ElaCentralStackedWidget::setCustomWidget 的 insertWidget(0,...))。 ela->addPageNode(kTitle, inner); + + // ── P2 主题桥:ElaTheme 明/暗切换 → 同步全局 QSS+调色板(覆盖所有标准控件与 ADS)。 + // Ela 自家控件由 ElaTheme 自动换肤;这里补齐非 Ela 面。也确定了主题最终态(解 review C2)。 + QObject::connect(eTheme, &ElaTheme::themeModeChanged, ela, [&app](ElaThemeType::ThemeMode m) { + geopro::app::applyThemeMode(app, m == ElaThemeType::Dark); + }); + geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // 初始对齐 + // 主题切换快捷键 Ctrl+Shift+T(正式切换入口待 P3 TopBar 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); + }); topLevel = ela; } else { auto* window = new QMainWindow; From c8812aa8a665f7d8144ade75f4dcc32ec7e8e732 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 07:49:15 +0800 Subject: [PATCH 09/56] =?UTF-8?q?feat(ela):=20P3-a=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=AA=97=20Ela=20=E5=8C=96=20+=20eApp->init=20=E6=97=A0?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.cpp: eApp->init() 提到登录前无条件调用(Ela 控件在登录窗/各面板都要用,登录在选壳前) - LoginWindow: 输入框→ElaLineEdit, 记住登录→ElaCheckBox, 登录按钮→ElaPushButton; 去掉它们的自定义 QSS 让 Ela 自绘 Fluent+自动明暗; 品牌头/字段标签/验证码/刷新链接保留 - Ela 控件均继承对应 Qt 基类, 故成员仍用 Qt 指针(多态), 不改 .hpp; 现有方法调用照常 --- src/app/login/LoginWindow.cpp | 33 +++++++++++---------------------- src/app/main.cpp | 7 +++++-- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index 10594ef..0badd6f 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -20,6 +20,10 @@ #include "AuthService.hpp" #include "Theme.hpp" +#include +#include +#include + namespace geopro::app { namespace { @@ -95,12 +99,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) "#brandTitle { color: #FFFFFF; font-size: %1px; font-weight: %2; }" "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }" "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" - "QLineEdit {" - " background: #FFFFFF; color: #1F2A3D;" - " border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;" - " selection-background-color: #2D6CB5; selection-color: #FFFFFF; }" - "QLineEdit:focus { border: 1px solid #2D6CB5; }" - "QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }" + // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") .arg(type::kDisplay) .arg(type::kWeightBold) @@ -146,12 +145,12 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) form->addSpacing(6); }; - userEdit_ = new QLineEdit(body); + userEdit_ = new ElaLineEdit(body); userEdit_->setPlaceholderText(QStringLiteral("请输入用户名")); userEdit_->setClearButtonEnabled(true); addField(QStringLiteral("用户名"), userEdit_); - pwdEdit_ = new QLineEdit(body); + pwdEdit_ = new ElaLineEdit(body); pwdEdit_->setEchoMode(QLineEdit::Password); pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码")); addField(QStringLiteral("密码"), pwdEdit_); @@ -163,7 +162,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) auto* captchaRow = new QHBoxLayout(); captchaRow->setSpacing(10); - codeEdit_ = new QLineEdit(body); + codeEdit_ = new ElaLineEdit(body); codeEdit_->setMinimumHeight(40); codeEdit_->setPlaceholderText(QStringLiteral("请输入验证码")); captchaLabel_ = new QLabel(body); @@ -187,10 +186,8 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) form->addLayout(refreshRow); // 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。 - rememberChk_ = new QCheckBox(QStringLiteral("记住登录(30 天内免登录)"), body); - rememberChk_->setCursor(Qt::PointingHandCursor); - rememberChk_->setStyleSheet( - QStringLiteral("color:#5A6B85; font-size:%1px;").arg(type::kBody)); + rememberChk_ = new ElaCheckBox(QStringLiteral("记住登录(30 天内免登录)"), body); + rememberChk_->setCursor(Qt::PointingHandCursor); // ElaCheckBox 自绘 Fluent + 自动明暗 form->addWidget(rememberChk_); // 错误提示:固定占位高度,避免出现时整体布局跳动。 @@ -204,17 +201,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) form->addStretch(); // 主操作:满宽强调主按钮(von Restorff:唯一高强调元素引导主流程)。 - loginBtn_ = new QPushButton(QStringLiteral("登 录"), body); + loginBtn_ = new ElaPushButton(QStringLiteral("登 录"), body); // Fluent 主按钮(自动明暗) loginBtn_->setMinimumHeight(44); loginBtn_->setCursor(Qt::PointingHandCursor); - loginBtn_->setStyleSheet(QStringLiteral( - "QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; " - "font-size: %1px; font-weight: %2; }" - "QPushButton:hover { background: #2862A6; }" - "QPushButton:pressed { background: #234F87; }" - "QPushButton:disabled { background: #9FB4CC; }") - .arg(type::kTitle) - .arg(type::kWeightSemibold)); loginBtn_->setDefault(true); form->addWidget(loginBtn_); diff --git a/src/app/main.cpp b/src/app/main.cpp index 3fb41bd..1d6c1a6 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -966,7 +966,11 @@ int main(int argc, char* argv[]) QCoreApplication::setOrganizationName(QStringLiteral("Geomative")); QCoreApplication::setApplicationName(QStringLiteral("Geopro3")); - // 浅色专业主题(Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。 + // ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化, + // 两种壳都需要它(登录发生在选壳之前)。Ela 控件跟随 ElaTheme;标准控件仍由下面 QSS 接管。 + eApp->init(); + + // 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。 geopro::app::applyTheme(app); // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量; @@ -1022,7 +1026,6 @@ int main(int argc, char* argv[]) const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"); QWidget* topLevel = nullptr; if (qEnvironmentVariable("GEOPRO_UI_SHELL") == QLatin1String("ela")) { - eApp->init(); // ElaApplication:Fluent 主题/字体/动画基建 auto* ela = new ElaWindow; ela->setWindowTitle(kTitle); ela->resize(1280, 800); From ef278ac335b8fd0b8c3f193a1d9cb5a681257b17 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 07:51:12 +0800 Subject: [PATCH 10/56] =?UTF-8?q?feat(ela):=20P3-b=20=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=BC=B9=E7=AA=97=20Ela=20=E5=8C=96=20?= =?UTF-8?q?=E2=80=94=20=E8=BE=93=E5=85=A5/=E4=B8=8B=E6=8B=89/=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E2=86=92Ela*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nameEdit_→ElaLineEdit, typeCombo_→ElaComboBox, 搜索/重置/上一页/下一页→ElaPushButton; 表格(QTableWidget)保留并靠 P2 暗色 QSS 联动 --- src/app/ProjectListDialog.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index 23c28a6..cbc7531 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -16,6 +16,10 @@ #include "Theme.hpp" +#include +#include +#include + namespace geopro::app { namespace { QString statusText(int s) { @@ -45,18 +49,18 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa auto* filter = new QHBoxLayout(); filter->addWidget(new QLabel(QStringLiteral("项目名称"), this)); - nameEdit_ = new QLineEdit(this); + nameEdit_ = new ElaLineEdit(this); nameEdit_->setPlaceholderText(QStringLiteral("输入项目名称")); nameEdit_->setFixedWidth(200); filter->addWidget(nameEdit_); filter->addSpacing(8); filter->addWidget(new QLabel(QStringLiteral("项目类型"), this)); - typeCombo_ = new QComboBox(this); + typeCombo_ = new ElaComboBox(this); typeCombo_->setFixedWidth(160); filter->addWidget(typeCombo_); filter->addSpacing(8); - auto* searchBtn = new QPushButton(QStringLiteral("搜索"), this); - auto* resetBtn = new QPushButton(QStringLiteral("重置"), this); + auto* searchBtn = new ElaPushButton(QStringLiteral("搜索"), this); + auto* resetBtn = new ElaPushButton(QStringLiteral("重置"), this); filter->addWidget(searchBtn); filter->addWidget(resetBtn); filter->addStretch(); @@ -81,8 +85,8 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa pageLabel_ = new QLabel(this); bottom->addWidget(pageLabel_); bottom->addStretch(); - prevBtn_ = new QPushButton(QStringLiteral("上一页"), this); - nextBtn_ = new QPushButton(QStringLiteral("下一页"), this); + prevBtn_ = new ElaPushButton(QStringLiteral("上一页"), this); + nextBtn_ = new ElaPushButton(QStringLiteral("下一页"), this); bottom->addWidget(prevBtn_); bottom->addWidget(nextBtn_); root->addLayout(bottom); From 9091d8c929b5e53fd5e652688047e2956f9f31b5 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 07:57:41 +0800 Subject: [PATCH 11/56] =?UTF-8?q?fix(ela):=20=E5=90=AF=E5=8A=A8=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E8=B7=9F=E9=9A=8F=20ElaTheme=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F(review=20M2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 避免系统暗色启动时登录窗(标准控件)与 Ela 控件明暗错配。 --- src/app/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 1d6c1a6..59f137e 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -971,7 +971,8 @@ int main(int argc, char* argv[]) eApp->init(); // 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。 - geopro::app::applyTheme(app); + // 跟随 ElaTheme 初始模式(可能随系统为暗),使登录窗与标准控件明暗一致(review M2)。 + geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量; // 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。 From 8a8202955395c6bbdb9d3c82c485d1bf5c6d0c84 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 08:53:00 +0800 Subject: [PATCH 12/56] =?UTF-8?q?fix(ela):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E9=85=8D=E8=89=B2=E6=94=B9=E4=B8=BA=E5=8F=96=E8=87=AA=20ElaThe?= =?UTF-8?q?me(=E9=87=8C=E5=A4=96=E4=B8=80=E8=87=B4)=20=E2=80=94=20?= =?UTF-8?q?=E4=BF=AE=E6=98=8E=E6=9A=97=E9=83=BD=E4=B8=8D=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: 我那套自挑的浅/暗配色与 ElaWindow 外壳的 Fluent 配色不一致, 里外两种色, 明暗都割裂。 改法: kRoleMap 把设计稿色令牌按语义角色映射到 ElaTheme 颜色角色, styleSheetForMode/ buildPalette 用 eTheme->getThemeColor(当前模式,角色) 取真实颜色, 与外壳同源。 (已知: #FFFFFF 兼作面板底与按钮文字, 全局替换后暗色下默认按钮文字对比度略弱, 影响很小) --- src/app/Theme.cpp | 172 +++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 93 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 599c329..078920d 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -6,6 +6,9 @@ #include #include +#include +#include + namespace geopro::app { namespace { @@ -369,111 +372,94 @@ ads--CDockWidgetTab[activeTab="true"] QLabel { } )QSS"; -// ── 暗色映射(P2 主题桥):把浅色 QSS 里每个浅色令牌就近替换为暗色等价, -// 复用同一 kStyleSheet 结构(选择器/度量不变),仅换色。ads--* 规则同在 kStyleSheet 内, -// 故停靠区也随之暗化。品牌蓝在暗底略提亮以保对比度;danger 同理。 -struct HexPair { - const char* light; - const char* dark; +// ── 主题桥配色:工作台标准控件的 QSS/调色板颜色直接取自 ElaTheme, +// 保证里外(ElaWindow 外壳 ↔ 内部工作台)在明/暗两模下完全一致——这是关键, +// 否则外壳一种灰、工作台另一种灰,明暗都显得割裂。 +// 做法:把浅色设计稿里每个色令牌按语义角色替换为 ElaTheme 当前模式的真实颜色。 +struct RoleMap { + const char* token; // 浅色设计稿中的占位 hex + ElaThemeType::ThemeColor role; // 对应 ElaTheme 颜色角色 }; -constexpr HexPair kDarkMap[] = { - {"#1F2A3D", "#E3E5E8"}, // 主文字 → 亮 - {"#F4F6FA", "#1E1F22"}, // 外壳底 - {"#FFFFFF", "#2B2D30"}, // 面板白 - {"#EDF1F7", "#323539"}, // 抬升/表头 - {"#EAEEF4", "#3A3D42"}, // 细分隔/边框线 - {"#EEF3FB", "#3A3D42"}, // 悬停底 - {"#DCE9F8", "#2E3F54"}, // 选中行底 - {"#1B3D67", "#CFE0F5"}, // 选中行文字 - {"#3A475C", "#C4C9D0"}, // 表头文字 - {"#5A6B85", "#A9B0BC"}, // 次要文字 - {"#D5DBE5", "#3A3D42"}, // 边框 - {"#C2CCDA", "#4A4E55"}, // 强边框/滚动条柄 - {"#C7D2E0", "#4A4E55"}, // 输入边框 - {"#2D6CB5", "#4A90E2"}, // 强调(品牌蓝,暗底提亮) - {"#2862A6", "#5C9CE8"}, // 强调悬停 - {"#234F87", "#3A7BC8"}, // 强调按下 - {"#9AA6B6", "#6E747C"}, // 禁用文字 - {"#F0F2F6", "#26282B"}, // 禁用底 - {"#8A93A3", "#7A8088"}, // 禁用文字2 - {"#DCE0E7", "#3A3D42"}, // 禁用边框 - {"#A7B4C7", "#5A5F66"}, // 滚动条柄悬停 - {"#E1E6EE", "#3A3D42"}, // 菜单分隔 - {"#DCE6F4", "#34373C"}, // 菜单栏选中 - {"#E6EBF3", "#3A3D42"}, // 进度条底 - {"#EAF1FB", "#2E3F54"}, // 工具按钮选中底 - {"#C0392B", "#E0685C"}, // 危险(暗底提亮) - {"#E6EAF1", "#3A3D42"}, // 面板表头分隔(与 PanelHeader 一致) +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}, // 危险 }; -// 浅/暗专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。 -QPalette buildPalette(bool dark) +QColor roleColor(bool dark, ElaThemeType::ThemeColor role) { - QPalette p; - if (!dark) { - const QColor shell("#F4F6FA"), panel("#FFFFFF"), text("#1F2A3D"), - mutedText("#5A6B85"), accent("#2D6CB5"); - p.setColor(QPalette::Window, shell); - p.setColor(QPalette::WindowText, text); - p.setColor(QPalette::Base, panel); - p.setColor(QPalette::AlternateBase, QColor("#F0F3F8")); - p.setColor(QPalette::Text, text); - p.setColor(QPalette::Button, QColor("#EDF1F7")); - p.setColor(QPalette::ButtonText, text); - p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D")); - p.setColor(QPalette::ToolTipText, shell); - p.setColor(QPalette::Highlight, accent); - p.setColor(QPalette::HighlightedText, panel); - p.setColor(QPalette::PlaceholderText, mutedText); - p.setColor(QPalette::Link, accent); - // 把 Fusion 的明暗 3D 角色压成相近浅灰,立体效果塌成平面。 - p.setColor(QPalette::Light, QColor("#FFFFFF")); - p.setColor(QPalette::Midlight, QColor("#EEF1F5")); - p.setColor(QPalette::Mid, QColor("#E1E6EE")); - p.setColor(QPalette::Dark, QColor("#D7DEE8")); - p.setColor(QPalette::Shadow, QColor("#D7DEE8")); - p.setColor(QPalette::Disabled, QPalette::Text, QColor("#9AA6B6")); - p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#9AA6B6")); - p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#9AA6B6")); - } else { - const QColor shell("#1E1F22"), panel("#2B2D30"), text("#E3E5E8"), - mutedText("#A9B0BC"), accent("#4A90E2"); - p.setColor(QPalette::Window, shell); - p.setColor(QPalette::WindowText, text); - p.setColor(QPalette::Base, panel); - p.setColor(QPalette::AlternateBase, QColor("#303236")); - p.setColor(QPalette::Text, text); - p.setColor(QPalette::Button, QColor("#323539")); - p.setColor(QPalette::ButtonText, text); - p.setColor(QPalette::ToolTipBase, QColor("#E3E5E8")); - p.setColor(QPalette::ToolTipText, QColor("#1E1F22")); - p.setColor(QPalette::Highlight, accent); - p.setColor(QPalette::HighlightedText, QColor("#0E1013")); - p.setColor(QPalette::PlaceholderText, mutedText); - p.setColor(QPalette::Link, accent); - p.setColor(QPalette::Light, QColor("#34373C")); - p.setColor(QPalette::Midlight, QColor("#303236")); - p.setColor(QPalette::Mid, QColor("#3A3D42")); - p.setColor(QPalette::Dark, QColor("#3A3D42")); - p.setColor(QPalette::Shadow, QColor("#3A3D42")); - p.setColor(QPalette::Disabled, QPalette::Text, QColor("#6E747C")); - p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#6E747C")); - p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#6E747C")); - } - return p; + return eTheme->getThemeColor(dark ? ElaThemeType::Dark : ElaThemeType::Light, role); } -// 当前模式的全局 QSS:暗色 = 在浅色 kStyleSheet 上逐一替换色令牌。 +// 当前模式的全局 QSS:按 kRoleMap 把设计稿色令牌换成 ElaTheme 真实颜色(明/暗同源于外壳)。 QString styleSheetForMode(bool dark) { QString s = QString::fromUtf8(kStyleSheet); - if (dark) { - for (const auto& hp : kDarkMap) - s.replace(QLatin1String(hp.light), QLatin1String(hp.dark)); - } + for (const auto& rm : kRoleMap) + s.replace(QLatin1String(rm.token), roleColor(dark, rm.role).name()); return s; } +// 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。 +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); + + 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::Text, text); + p.setColor(QPalette::Button, roleColor(dark, ElaThemeType::BasicBaseDeep)); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::ToolTipBase, text); + p.setColor(QPalette::ToolTipText, shell); + p.setColor(QPalette::Highlight, accent); + p.setColor(QPalette::HighlightedText, panel); + p.setColor(QPalette::PlaceholderText, muted); + p.setColor(QPalette::Link, accent); + // Fusion 的明暗 3D 角色统一压成边框色,立体效果塌成平面(去斜角/凹槽)。 + p.setColor(QPalette::Light, panel); + p.setColor(QPalette::Midlight, border); + p.setColor(QPalette::Mid, border); + p.setColor(QPalette::Dark, border); + p.setColor(QPalette::Shadow, border); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + return p; +} + } // namespace void applyThemeMode(QApplication& app, bool dark) From 4a785ede8839d86906a8b3abd1e544468bed6681 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 08:59:59 +0800 Subject: [PATCH 13/56] =?UTF-8?q?fix(ela):=20=E5=86=85=E8=81=94=20chrome?= =?UTF-8?q?=20=E4=B9=9F=E8=B7=9F=E9=9A=8F=E4=B8=BB=E9=A2=98=20=E2=80=94=20?= =?UTF-8?q?=E4=BF=AE=E6=9A=97=E8=89=B2=E4=B8=8B=E5=88=87=E6=8D=A2=E5=99=A8?= =?UTF-8?q?=E6=96=87=E5=AD=97=E7=9C=8B=E4=B8=8D=E6=B8=85=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 截图问题根因: TopBar/PanelHeader/3D浮层 用每控件内联 setStyleSheet(硬编码浅色 hex), 未走全局 ElaTheme 替换 → 暗色下保持浅色令牌(深字落深底→看不清; 白底面板表头等)。 新增 applyThemedStyleSheet(widget,设计稿QSS): 按 ElaTheme 角色着色 + 随明暗自动重着色。 TopBar/PanelHeader/layerPanel/layerTitle 内联样式全改走它; 补 #EEF1F5/#E6EAF1/#EAEEF5 角色。 --- src/app/PanelHeader.cpp | 4 ++-- src/app/Theme.cpp | 33 +++++++++++++++++++++++--- src/app/Theme.hpp | 11 +++++++++ src/app/TopBar.cpp | 52 +++++++++++++++++++++-------------------- src/app/main.cpp | 17 +++++++------- 5 files changed, 79 insertions(+), 38 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 30c342d..5e4a6e3 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -81,7 +81,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - header->setStyleSheet(headerQss()); + geopro::app::applyThemedStyleSheet(header, headerQss()); auto* lay = new QHBoxLayout(header); lay->setContentsMargins(12, 0, 8, 0); @@ -114,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - header->setStyleSheet(headerQss()); + geopro::app::applyThemedStyleSheet(header, headerQss()); auto* hlay = new QHBoxLayout(header); hlay->setContentsMargins(10, 0, 8, 0); hlay->setSpacing(2); diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 078920d..d9bc575 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -407,6 +409,10 @@ const RoleMap kRoleMap[] = { {"#E6EBF3", ElaThemeType::BasicDisable}, // 进度条底 {"#EAF1FB", ElaThemeType::BasicHover}, // 工具按钮选中底 {"#C0392B", ElaThemeType::StatusDanger}, // 危险 + // 内联 chrome(TopBar/PanelHeader)额外用到的令牌: + {"#EEF1F5", ElaThemeType::BasicBorder}, // 菜单栏底边线 + {"#E6EAF1", ElaThemeType::BasicBorder}, // 面板表头底边线 + {"#EAEEF5", ElaThemeType::BasicBaseDeep}, // 面板徽标底 }; QColor roleColor(bool dark, ElaThemeType::ThemeColor role) @@ -414,15 +420,21 @@ QColor roleColor(bool dark, ElaThemeType::ThemeColor role) return eTheme->getThemeColor(dark ? ElaThemeType::Dark : ElaThemeType::Light, role); } -// 当前模式的全局 QSS:按 kRoleMap 把设计稿色令牌换成 ElaTheme 真实颜色(明/暗同源于外壳)。 -QString styleSheetForMode(bool dark) +// 把一段设计稿 QSS 按 ElaTheme 当前模式着色(浅色令牌→真实角色色)。供全局与内联样式共用。 +QString themedQss(const QString& designQss, bool dark) { - QString s = QString::fromUtf8(kStyleSheet); + QString s = designQss; for (const auto& rm : kRoleMap) s.replace(QLatin1String(rm.token), roleColor(dark, rm.role).name()); return s; } +// 当前模式的全局 QSS。 +QString styleSheetForMode(bool dark) +{ + return themedQss(QString::fromUtf8(kStyleSheet), dark); +} + // 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。 QPalette buildPalette(bool dark) { @@ -482,4 +494,19 @@ void applyTheme(QApplication& app) applyThemeMode(app, false); } +bool isDarkTheme() +{ + return eTheme->getThemeMode() == ElaThemeType::Dark; +} + +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)); + }); +} + } // namespace geopro::app diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 385b005..295b7d0 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -9,7 +9,10 @@ // 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA // 危险 #C0392B +#include + class QApplication; +class QWidget; namespace geopro::app { @@ -87,4 +90,12 @@ void applyThemeMode(QApplication& app, bool dark); // 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。 void applyTheme(QApplication& app); +// 当前 ElaTheme 是否暗色(供内联样式判断)。 +bool isDarkTheme(); + +// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色应用到 widget,并随明/暗切换自动重着色。 +// 用于 TopBar/PanelHeader/浮层 等带内联 setStyleSheet 的自定义 chrome——让它们也跟随主题 +// (设计稿里用浅色令牌 #1F2A3D/#FFFFFF/#2D6CB5… 书写即可,与全局 QSS 同一套角色映射)。 +void applyThemedStyleSheet(QWidget* w, const QString& designQss); + } // namespace geopro::app diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 3a9824f..77811c9 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -116,12 +116,13 @@ QWidget* buildMenuBar(QWidget* parent) auto* mb = new QMenuBar(parent); mb->setObjectName(QStringLiteral("appMenuBar")); // 自带样式(覆盖全局),加大字号/内边距,专业观感。 - mb->setStyleSheet(QStringLiteral( - "#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }" - "#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:%1px; color:#1F2A3D; }" - "#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }" - "#appMenuBar::item:pressed { background:#DCE6F4; }") - .arg(type::kBody)); + geopro::app::applyThemedStyleSheet( + mb, QStringLiteral( + "#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }" + "#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:%1px; color:#1F2A3D; }" + "#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }" + "#appMenuBar::item:pressed { background:#DCE6F4; }") + .arg(type::kBody)); mb->addMenu(buildViewMenu(mb)); mb->addMenu(buildProjectMenu(mb)); mb->addMenu(buildToolsMenu(mb)); @@ -134,25 +135,26 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { setFixedHeight(56); // 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、 // 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。 - setStyleSheet(QStringLiteral( - "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" - "#topDivider { color:#E1E6EE; }" - "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" - " font-size:%1px; font-weight:%5; }" - "#wsSwitcher:hover { background:#EEF3FB; }" - "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" - "QToolButton#iconBtn:hover { background:#EEF3FB; }" - "QToolButton::menu-indicator { image:none; }" - "#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:%6;" - " font-size:%2px; }" - "#userName { color:#1F2A3D; font-size:%3px; font-weight:%5; }" - "#userRole { color:#8A93A3; font-size:%4px; }") - .arg(type::kTitle) - .arg(type::kBody) - .arg(type::kLabel) - .arg(type::kCaption) - .arg(type::kWeightSemibold) - .arg(type::kWeightBold)); + geopro::app::applyThemedStyleSheet( + this, QStringLiteral( + "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" + "#topDivider { color:#E1E6EE; }" + "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" + " font-size:%1px; font-weight:%5; }" + "#wsSwitcher:hover { background:#EEF3FB; }" + "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" + "QToolButton#iconBtn:hover { background:#EEF3FB; }" + "QToolButton::menu-indicator { image:none; }" + "#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:%6;" + " font-size:%2px; }" + "#userName { color:#1F2A3D; font-size:%3px; font-weight:%5; }" + "#userRole { color:#8A93A3; font-size:%4px; }") + .arg(type::kTitle) + .arg(type::kBody) + .arg(type::kLabel) + .arg(type::kCaption) + .arg(type::kWeightSemibold) + .arg(type::kWeightBold)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); diff --git a/src/app/main.cpp b/src/app/main.cpp index 59f137e..786d983 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -331,9 +331,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。 auto* layerPanel = new QFrame(centerWidget); layerPanel->setFrameShape(QFrame::StyledPanel); - layerPanel->setStyleSheet( - QStringLiteral("QFrame{background:rgba(255,255,255,0.96);border:1px solid #D5DBE5;" - "border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}" + geopro::app::applyThemedStyleSheet( + layerPanel, + QStringLiteral("QFrame{background:#FFFFFF;border:1px solid #D5DBE5;border-radius:8px;}" + "QCheckBox{padding:2px 1px;color:#1F2A3D;}" "QCheckBox:disabled{color:#9AA6B6;}")); auto* layerLayout = new QVBoxLayout(layerPanel); // 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。 @@ -341,11 +342,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re geopro::app::space::kLg, geopro::app::space::kMl); layerLayout->setSpacing(geopro::app::space::kSm); auto* layerTitle = new QLabel(QStringLiteral("视图详情")); - layerTitle->setStyleSheet(QStringLiteral( - "font-weight:%1;color:#2D6CB5;border:none;background:transparent;" - "padding-bottom:3px;font-size:%2px;") - .arg(geopro::app::type::kWeightSemibold) - .arg(geopro::app::type::kTitle)); + geopro::app::applyThemedStyleSheet( + layerTitle, QStringLiteral("font-weight:%1;color:#2D6CB5;border:none;background:transparent;" + "padding-bottom:3px;font-size:%2px;") + .arg(geopro::app::type::kWeightSemibold) + .arg(geopro::app::type::kTitle)); auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)")); chkCurtain->setChecked(true); auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)")); From 57c452e2d345ad9db93d33b059e320ab37f22031 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 09:12:05 +0800 Subject: [PATCH 14/56] =?UTF-8?q?feat(ela):=20=E8=8F=9C=E5=8D=95=E5=85=A8?= =?UTF-8?q?=E6=8D=A2=20ElaMenu/ElaMenuBar(=E6=A0=B9=E6=B2=BB=E5=9C=86?= =?UTF-8?q?=E8=A7=92=E9=9C=B2=E7=9B=B4=E8=A7=92)=20+=20=E7=99=BB=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TopBar: 4 主菜单/子菜单/切换器菜单 → ElaMenu, 菜单栏 → ElaMenuBar(自绘透明圆角弹窗+随主题); 去掉 #appMenuBar 内联 QSS; Theme.cpp 删除 QMenuBar/QMenu QSS(否则 border-radius 仍露直角) - 登出: 头像改可点击 QToolButton + ElaMenu「退出登录」→ logoutRequested 信号; main 接信号 → forgetSession() 清凭证 + QProcess 重启回登录页(撤销之前的 GEOPRO_FORCE_LOGIN 补丁) --- src/app/Theme.cpp | 36 ++---------------------------------- src/app/TopBar.cpp | 42 ++++++++++++++++++++++-------------------- src/app/TopBar.hpp | 1 + src/app/main.cpp | 8 ++++++++ 4 files changed, 33 insertions(+), 54 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index d9bc575..9a9a314 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -250,40 +250,8 @@ QStatusBar QLabel { padding: 0 4px; } -/* ── 菜单栏 / 菜单(按需出现时也与主题一致)────────────────── */ -QMenuBar { - background: #EDF1F7; - color: #1F2A3D; - border-bottom: 1px solid #D5DBE5; -} -QMenuBar::item { - background: transparent; - padding: 6px 12px; - border-radius: 6px; -} -QMenuBar::item:selected { - background: #DCE6F4; -} -QMenu { - background: #FFFFFF; - color: #1F2A3D; - border: 1px solid #D5DBE5; - border-radius: 8px; - padding: 6px; -} -QMenu::item { - padding: 6px 24px 6px 14px; - border-radius: 6px; -} -QMenu::item:selected { - background: #DCE9F8; - color: #1B3D67; -} -QMenu::separator { - height: 1px; - background: #E1E6EE; - margin: 6px 8px; -} +/* ── 菜单栏 / 菜单:已全部改用 ElaMenuBar/ElaMenu(自绘透明圆角弹窗+随主题), + 这里不再写 QMenuBar/QMenu 的 QSS——否则 border-radius 会让弹窗圆角露出后面的直角。 */ /* ── 下拉框(按需出现时也与主题一致)──────────────────────── */ QComboBox { diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 77811c9..00f7125 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -16,6 +16,9 @@ #include "Glyphs.hpp" #include "Theme.hpp" +#include +#include + namespace geopro::app { namespace { @@ -51,7 +54,7 @@ QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip) // ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)── QMenu* buildViewMenu(QWidget* p) { - auto* m = new QMenu(QStringLiteral("视图"), p); + auto* m = new ElaMenu(QStringLiteral("视图"), p); m->addAction(QStringLiteral("分析视图")); m->addAction(QStringLiteral("大屏视图")); return m; @@ -59,7 +62,7 @@ QMenu* buildViewMenu(QWidget* p) QMenu* buildProjectMenu(QWidget* p) { - auto* m = new QMenu(QStringLiteral("项目管理"), p); + auto* m = new ElaMenu(QStringLiteral("项目管理"), p); m->addAction(QStringLiteral("数据视图")); auto* cfg = m->addMenu(QStringLiteral("项目配置")); cfg->addAction(QStringLiteral("基本信息")); @@ -93,7 +96,7 @@ QMenu* buildProjectMenu(QWidget* p) QMenu* buildToolsMenu(QWidget* p) { - auto* m = new QMenu(QStringLiteral("业务工具"), p); + auto* m = new ElaMenu(QStringLiteral("业务工具"), p); m->addAction(QStringLiteral("ERT 思维分析")); m->addAction(QStringLiteral("电法脚本与装置")); m->addAction(QStringLiteral("Geo 反演")); @@ -103,7 +106,7 @@ QMenu* buildToolsMenu(QWidget* p) QMenu* buildDeviceMenu(QWidget* p) { - auto* m = new QMenu(QStringLiteral("设备"), p); + auto* m = new ElaMenu(QStringLiteral("设备"), p); m->addAction(QStringLiteral("连接设备")); m->addAction(QStringLiteral("设备管理")); return m; @@ -113,16 +116,9 @@ QMenu* buildDeviceMenu(QWidget* p) QWidget* buildMenuBar(QWidget* parent) { - auto* mb = new QMenuBar(parent); + auto* mb = new ElaMenuBar(parent); mb->setObjectName(QStringLiteral("appMenuBar")); - // 自带样式(覆盖全局),加大字号/内边距,专业观感。 - geopro::app::applyThemedStyleSheet( - mb, QStringLiteral( - "#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }" - "#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:%1px; color:#1F2A3D; }" - "#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }" - "#appMenuBar::item:pressed { background:#DCE6F4; }") - .arg(type::kBody)); + // ElaMenuBar 自绘 Fluent 外观并自动随 ElaTheme 明暗,不再写内联 QSS。 mb->addMenu(buildViewMenu(mb)); mb->addMenu(buildProjectMenu(mb)); mb->addMenu(buildToolsMenu(mb)); @@ -169,7 +165,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { wsBtn_->setPopupMode(QToolButton::InstantPopup); wsBtn_->setCursor(Qt::PointingHandCursor); wsBtn_->setText(QStringLiteral("正在加载工作空间…")); - wsBtn_->setMenu(new QMenu(wsBtn_)); + wsBtn_->setMenu(new ElaMenu(wsBtn_)); lay->addWidget(wsBtn_); lay->addSpacing(10); @@ -185,7 +181,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { projBtn_->setPopupMode(QToolButton::InstantPopup); projBtn_->setCursor(Qt::PointingHandCursor); projBtn_->setText(QStringLiteral("正在加载项目…")); - projBtn_->setMenu(new QMenu(projBtn_)); + projBtn_->setMenu(new ElaMenu(projBtn_)); lay->addWidget(projBtn_); lay->addStretch(); @@ -197,11 +193,17 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(12); - // 用户区(本轮静态)。 - auto* avatar = new QLabel(QStringLiteral("ZL"), this); + // 用户区:头像可点击 → 弹出菜单(退出登录)。 + auto* avatar = new QToolButton(this); avatar->setObjectName(QStringLiteral("avatar")); + avatar->setText(QStringLiteral("ZL")); avatar->setFixedSize(34, 34); - avatar->setAlignment(Qt::AlignCenter); + avatar->setCursor(Qt::PointingHandCursor); + avatar->setPopupMode(QToolButton::InstantPopup); + auto* userMenu = new ElaMenu(avatar); + QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, + [this] { emit logoutRequested(); }); + avatar->setMenu(userMenu); lay->addWidget(avatar); lay->addSpacing(8); @@ -219,7 +221,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { } void TopBar::setWorkspaces(const std::vector& list, const QString& currentId) { - auto* menu = new QMenu(wsBtn_); + auto* menu = new ElaMenu(wsBtn_); auto* header = menu->addAction(QStringLiteral("切换空间")); header->setEnabled(false); menu->addSeparator(); @@ -250,7 +252,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 QMenu(projBtn_); + auto* menu = new ElaMenu(projBtn_); auto* header = menu->addAction(QStringLiteral("切换项目")); header->setEnabled(false); menu->addSeparator(); diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index b6388ae..662bed6 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -25,6 +25,7 @@ signals: void workspaceSwitchRequested(const QString& tenantId); void projectSwitchRequested(const QString& projectId); void allProjectsRequested(); // 点击"全部项目…" + void logoutRequested(); // 头像菜单「退出登录」 private: QToolButton* wsBtn_ = nullptr; diff --git a/src/app/main.cpp b/src/app/main.cpp index 786d983..1b5e087 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -841,6 +842,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re &geopro::controller::WorkbenchNavController::switchWorkspace); QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav, &geopro::controller::WorkbenchNavController::switchProject); + // 退出登录:清除记住的凭证(QtKeychain+QSettings) → 重启应用回到登录页。 + QObject::connect(topBar, &geopro::app::TopBar::logoutRequested, &window, []() { + geopro::app::forgetSession(); + QProcess::startDetached(QCoreApplication::applicationFilePath(), + QCoreApplication::arguments().mid(1)); + qApp->quit(); + }); QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window, [&projectRepo, &nav, topBar, &window]() { auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window); From f5eff9e185a339a9485e7e953ed5bc96a8872771 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 09:17:45 +0800 Subject: [PATCH 15/56] =?UTF-8?q?feat(ela):=20C=20VTK=20=E8=A7=86=E5=8F=A3?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E9=9A=8F=E4=B8=BB=E9=A2=98=20+=20A=20?= =?UTF-8?q?=E6=B5=AE=E5=B1=82=E5=8B=BE=E9=80=89=E6=A1=86=20Ela=20=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vtkBackground(): 取 ElaTheme 窗口底色; CentralScene/详情渲染器初始用它; 切主题时中央+详情重设+刷新 - 浮层图层勾选(帘面/体素/切片/地形) → ElaCheckBox - 说明: 自定义 chrome(工作空间/项目切换器/图标按钮/PanelHeader tab/工具条)保留并随主题(强换 Ela 会丢设计); 状态栏由 QSS 随主题 --- src/app/CentralScene.cpp | 7 ++++++- src/app/Theme.cpp | 8 ++++++++ src/app/Theme.hpp | 3 +++ src/app/main.cpp | 26 +++++++++++++++++++++----- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/app/CentralScene.cpp b/src/app/CentralScene.cpp index 2579151..a80e951 100644 --- a/src/app/CentralScene.cpp +++ b/src/app/CentralScene.cpp @@ -6,6 +6,7 @@ #include "CameraPreset.hpp" #include "Scene.hpp" +#include "Theme.hpp" #include "actors/CurtainActor.hpp" #include "actors/MapLineActor.hpp" #include "geo/GeoLocalFrame.hpp" @@ -18,7 +19,11 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer, const geopro::core::GeoLocalFrame& frame, double verticalExaggeration) { scene.clear(); const bool is2D = (mode == ViewMode::Map2D); - renderer->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0); + (void)is2D; + // 背景随主题(取 ElaTheme 窗口底色),暗色下不再是刺眼白底。 + double bgR, bgG, bgB; + geopro::app::vtkBackground(bgR, bgG, bgB); + renderer->SetBackground(bgR, bgG, bgB); for (const auto& s : sections) { if (is2D) { diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 9a9a314..a27b0fb 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -467,6 +467,14 @@ bool isDarkTheme() return eTheme->getThemeMode() == ElaThemeType::Dark; } +void vtkBackground(double& r, double& g, double& b) +{ + const QColor c = roleColor(isDarkTheme(), ElaThemeType::WindowBase); + r = c.redF(); + g = c.greenF(); + b = c.blueF(); +} + void applyThemedStyleSheet(QWidget* w, const QString& designQss) { if (!w) return; diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 295b7d0..3fa7ab7 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -93,6 +93,9 @@ void applyTheme(QApplication& app); // 当前 ElaTheme 是否暗色(供内联样式判断)。 bool isDarkTheme(); +// VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b(0–1)。 +void vtkBackground(double& r, double& g, double& b); + // 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色应用到 widget,并随明/暗切换自动重着色。 // 用于 TopBar/PanelHeader/浮层 等带内联 setStyleSheet 的自定义 chrome——让它们也跟随主题 // (设计稿里用浅色令牌 #1F2A3D/#FFFFFF/#2D6CB5… 书写即可,与全局 QSS 同一套角色映射)。 diff --git a/src/app/main.cpp b/src/app/main.cpp index 1b5e087..5117475 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -62,6 +62,7 @@ #include #include +#include #include #include #include @@ -348,13 +349,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "padding-bottom:3px;font-size:%2px;") .arg(geopro::app::type::kWeightSemibold) .arg(geopro::app::type::kTitle)); - auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)")); + auto* chkCurtain = new ElaCheckBox(QStringLiteral("帘面(断面墙)")); chkCurtain->setChecked(true); - auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)")); + auto* chkVoxel = new ElaCheckBox(QStringLiteral("体素(dd_voxel)")); chkVoxel->setChecked(false); - auto* chkTerrain = new QCheckBox(QStringLiteral("地形(DEM+影像)")); + auto* chkTerrain = new ElaCheckBox(QStringLiteral("地形(DEM+影像)")); chkTerrain->setChecked(false); - auto* chkSlice = new QCheckBox(QStringLiteral("切片(dd_slice)")); + auto* chkSlice = new ElaCheckBox(QStringLiteral("切片(dd_slice)")); chkSlice->setChecked(false); if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示 const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用"); @@ -433,12 +434,27 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* detailWidget = new QVTKOpenGLStereoWidget(); vtkNew detailRenderWindow; vtkNew detailRenderer; - detailRenderer->SetBackground(1.0, 1.0, 1.0); // 白底 + { + double r, g, b; + geopro::app::vtkBackground(r, g, b); // 背景随主题 + detailRenderer->SetBackground(r, g, b); + } detailWidget->setRenderWindow(detailRenderWindow); detailRenderWindow->AddRenderer(detailRenderer); vtkRenderer* detailRendererPtr = detailRenderer.Get(); vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get(); + // VTK 背景随主题切换:ElaTheme 明/暗切换时,中央+详情渲染器重设底色并刷新。 + QObject::connect(eTheme, &ElaTheme::themeModeChanged, detailWidget, + [rendererPtr, renderWindowPtr, detailRendererPtr, detailRenderWindowPtr]() { + double r, g, b; + geopro::app::vtkBackground(r, g, b); + rendererPtr->SetBackground(r, g, b); + renderWindowPtr->Render(); + detailRendererPtr->SetBackground(r, g, b); + detailRenderWindowPtr->Render(); + }); + // 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。 auto* detailContainer = new QWidget(); auto* detailLayout = new QVBoxLayout(detailContainer); From 68d832c57b4af73ef36d09a183b14bfb9141de75 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 09:32:22 +0800 Subject: [PATCH 16/56] =?UTF-8?q?fix(ela):=20=E7=99=BB=E5=BD=95=E7=AA=97?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=8C=96=20+=20VTK=20=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E7=A8=B3=E5=81=A5=E9=9A=8F=E4=B8=BB=E9=A2=98=20+=20=E6=89=AB?= =?UTF-8?q?=E6=B8=85=E6=89=80=E6=9C=89=E7=A1=AC=E7=BC=96=E7=A0=81=E6=B5=85?= =?UTF-8?q?=E8=89=B2=20QSS=20=E9=81=97=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 登录窗: setStyleSheet → applyThemedStyleSheet(整窗随主题, 品牌带文字用 white 关键字恒白); refreshBtn/errorLabel 也随主题。修暗系统下浅窗+暗 Ela 控件割裂、占位文字看不清 - VTK: 改为主题切换时重跑 rebuildCentral/rebuildDetail(走完整渲染必重绘, 兼顾 syncSystemTheme 异步切暗时序); rebuildDetail 也按 vtkBackground 设背景 - 主动扫描修掉遗漏: dockManager 分隔条、数据集/文件列表、对象树枝/hint、空状态标题/提示 全走主题 - themed() 公共助手(给需拼接 ADS 样式的 dockManager) --- src/app/Theme.cpp | 5 +++ src/app/Theme.hpp | 4 ++ src/app/login/LoginWindow.cpp | 45 +++++++++++--------- src/app/main.cpp | 67 ++++++++++++++++++------------ src/app/panels/ObjectTreePanel.cpp | 17 ++++---- 5 files changed, 83 insertions(+), 55 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index a27b0fb..1f4c24a 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -467,6 +467,11 @@ bool isDarkTheme() return eTheme->getThemeMode() == ElaThemeType::Dark; } +QString themed(const QString& designQss) +{ + return themedQss(designQss, isDarkTheme()); +} + void vtkBackground(double& r, double& g, double& b) { const QColor c = roleColor(isDarkTheme(), ElaThemeType::WindowBase); diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 3fa7ab7..a4c4e08 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -101,4 +101,8 @@ void vtkBackground(double& r, double& g, double& b); // (设计稿里用浅色令牌 #1F2A3D/#FFFFFF/#2D6CB5… 书写即可,与全局 QSS 同一套角色映射)。 void applyThemedStyleSheet(QWidget* w, const QString& designQss); +// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色后返回(供需要拼接/手动 setStyleSheet 的场景, +// 如 ADS dockManager 在其自带样式后追加规则)。不自动随主题切换,调用方需自行在切换时重取。 +QString themed(const QString& designQss); + } // namespace geopro::app diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index 0badd6f..fbd43ae 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -91,21 +91,24 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。 // QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。 // 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。 - setStyleSheet(QStringLiteral( - "QDialog { background: #F4F6FA; }" - "#headerBand {" - " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," - " stop:0 #2D6CB5, stop:1 #234F87); }" - "#brandTitle { color: #FFFFFF; font-size: %1px; font-weight: %2; }" - "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }" - "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" - // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 - "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") - .arg(type::kDisplay) - .arg(type::kWeightBold) - .arg(type::kCaption) - .arg(type::kCaption) - .arg(type::kWeightSemibold)); + // 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。 + // 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。 + geopro::app::applyThemedStyleSheet( + this, QStringLiteral( + "QDialog { background: #F4F6FA; }" + "#headerBand {" + " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #2D6CB5, stop:1 #234F87); }" + "#brandTitle { color: white; font-size: %1px; font-weight: %2; }" + "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }" + "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" + // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 + "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") + .arg(type::kDisplay) + .arg(type::kWeightBold) + .arg(type::kCaption) + .arg(type::kCaption) + .arg(type::kWeightSemibold)); auto* root = new QVBoxLayout(this); root->setContentsMargins(0, 0, 0, 0); @@ -179,9 +182,11 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body); refreshBtn_->setFlat(true); refreshBtn_->setCursor(Qt::PointingHandCursor); - refreshBtn_->setStyleSheet(QStringLiteral( - "QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }" - "QPushButton:hover { color: #234F87; text-decoration: underline; }")); + geopro::app::applyThemedStyleSheet( + refreshBtn_, + QStringLiteral( + "QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }" + "QPushButton:hover { color: #234F87; text-decoration: underline; }")); refreshRow->addWidget(refreshBtn_); form->addLayout(refreshRow); @@ -192,8 +197,8 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 错误提示:固定占位高度,避免出现时整体布局跳动。 errorLabel_ = new QLabel(body); - errorLabel_->setStyleSheet( - QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption)); + geopro::app::applyThemedStyleSheet( + errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption)); errorLabel_->setWordWrap(true); errorLabel_->setMinimumHeight(18); form->addWidget(errorLabel_); diff --git a/src/app/main.cpp b/src/app/main.cpp index 5117475..2e8fb92 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -289,12 +289,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re window.setCentralWidget(dockManager); // 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线, - // 且设在管理器自身、选择器更具体(优先级高于全局主题)。这里在其后追加同选择器规则覆盖为极淡分隔。 - dockManager->setStyleSheet( - dockManager->styleSheet() + - QStringLiteral( - "ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }" - "ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }")); + // 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。 + // 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。 + const QString dockBaseQss = dockManager->styleSheet(); + auto applyDockSplitter = [dockManager, dockBaseQss]() { + dockManager->setStyleSheet( + dockBaseQss + + geopro::app::themed(QStringLiteral( + "ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }" + "ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"))); + }; + applyDockSplitter(); + QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager, + [applyDockSplitter](ElaThemeType::ThemeMode) { applyDockSplitter(); }); // 面板包装:内容顶部加自绘表头(图标+标题+操作按钮),ADS 自带标题栏随后隐藏, // 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。 @@ -397,16 +404,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState); esTitle->setAlignment(Qt::AlignCenter); - esTitle->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") - .arg(geopro::app::type::kHeading) - .arg(geopro::app::type::kWeightSemibold)); + geopro::app::applyThemedStyleSheet( + esTitle, QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") + .arg(geopro::app::type::kHeading) + .arg(geopro::app::type::kWeightSemibold)); auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n" "切到「三维视图」可叠加帘面、体素与地形图层"), emptyState); esHint->setAlignment(Qt::AlignCenter); - esHint->setStyleSheet( - QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody)); + geopro::app::applyThemedStyleSheet( + esHint, QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody)); esLay->addWidget(esIcon); esLay->addWidget(esTitle); @@ -443,17 +451,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailRenderWindow->AddRenderer(detailRenderer); vtkRenderer* detailRendererPtr = detailRenderer.Get(); vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get(); - - // VTK 背景随主题切换:ElaTheme 明/暗切换时,中央+详情渲染器重设底色并刷新。 - QObject::connect(eTheme, &ElaTheme::themeModeChanged, detailWidget, - [rendererPtr, renderWindowPtr, detailRendererPtr, detailRenderWindowPtr]() { - double r, g, b; - geopro::app::vtkBackground(r, g, b); - rendererPtr->SetBackground(r, g, b); - renderWindowPtr->Render(); - detailRendererPtr->SetBackground(r, g, b); - detailRenderWindowPtr->Render(); - }); + // 注:VTK 背景随主题切换的连接放在 rebuildCentral/rebuildDetail 定义之后(直接重跑它们, + // 走完整渲染路径必重绘,比手动 SetBackground+Render 稳)。 // 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。 auto* detailContainer = new QWidget(); @@ -504,15 +503,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 auto* datasetTabs = new QTabWidget(); auto* datasetList = new QListWidget(); - // 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。 - datasetList->setStyleSheet(QStringLiteral( + // 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。随主题着色。 + const QString kListQss = QStringLiteral( "QListWidget{ background:#FFFFFF; border:none; outline:none; }" "QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }" - "QListWidget::item:hover{ background:#F5F8FD; }" - "QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }")); + "QListWidget::item:hover{ background:#EEF3FB; }" + "QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"); + geopro::app::applyThemedStyleSheet(datasetList, kListQss); datasetTabs->addTab(datasetList, QStringLiteral("数据")); auto* fileList = new QListWidget(); - fileList->setStyleSheet(datasetList->styleSheet()); // 与数据页签同款简洁分割 + geopro::app::applyThemedStyleSheet(fileList, kListQss); // 与数据页签同款简洁分割 datasetTabs->addTab(fileList, QStringLiteral("文件")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetBox = wrapWithHeader( @@ -608,6 +608,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re fromCam->DeepCopy(detailRendererPtr->GetActiveCamera()); detailRendererPtr->RemoveAllViewProps(); + { // 背景随主题 + double r, g, b; + geopro::app::vtkBackground(r, g, b); + detailRendererPtr->SetBackground(r, g, b); + } if (currentDsId->isEmpty()) { // 未选数据集:清空即可 *prevDsId = *currentDsId; detailRenderWindowPtr->Render(); @@ -822,6 +827,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。 rebuildCentral(); + // VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail(走完整渲染路径、末尾必 Render, + // 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。 + QObject::connect(eTheme, &ElaTheme::themeModeChanged, &window, + [rebuildCentral, rebuildDetail]() { + rebuildCentral(); + rebuildDetail(); + }); + // 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备), // 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。 geopro::app::TopBar* topBar = nullptr; diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 2e330d4..66c1ea7 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -8,6 +8,7 @@ #include #include "Glyphs.hpp" +#include "Theme.hpp" #include "dto/NavDto.hpp" namespace geopro::app { @@ -40,19 +41,19 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { { const QString openArrow = writeChevronIcon(true, QColor("#8A93A3")); const QString closedArrow = writeChevronIcon(false, QColor("#8A93A3")); - tree_->setStyleSheet( - QStringLiteral("QTreeView::branch { background: #FFFFFF; }" - "QTreeView::branch:has-children:!has-siblings:closed," - "QTreeView::branch:closed:has-children:has-siblings { image: url(%1); }" - "QTreeView::branch:open:has-children:!has-siblings," - "QTreeView::branch:open:has-children:has-siblings { image: url(%2); }") - .arg(closedArrow, openArrow)); + geopro::app::applyThemedStyleSheet( + tree_, QStringLiteral("QTreeView::branch { background: #FFFFFF; }" + "QTreeView::branch:has-children:!has-siblings:closed," + "QTreeView::branch:closed:has-children:has-siblings { image: url(%1); }" + "QTreeView::branch:open:has-children:!has-siblings," + "QTreeView::branch:open:has-children:has-siblings { image: url(%2); }") + .arg(closedArrow, openArrow)); } lay->addWidget(tree_, 1); hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); hint_->setAlignment(Qt::AlignCenter); - hint_->setStyleSheet(QStringLiteral("color:#9AA6B6; padding:16px;")); + geopro::app::applyThemedStyleSheet(hint_, QStringLiteral("color:#9AA6B6; padding:16px;")); hint_->setVisible(false); lay->addWidget(hint_); From 389a2da7443b7feda74c3be04eb70ac58e4739e2 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 09:40:28 +0800 Subject: [PATCH 17/56] =?UTF-8?q?feat(ela):=20=E8=A1=A8=E6=A0=BC=E2=86=92E?= =?UTF-8?q?laTableWidget(=E7=9B=B4=E6=9B=BF)=20+=20=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E6=A0=91=E2=86=92ElaTreeView+QStandardItemModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProjectListDialog: QTableWidget → ElaTableWidget(item 版直接继承 QTableWidget, 1 行) - ObjectTreePanel: QTreeWidget → ElaTreeView + Qt 自带 QStandardItemModel(非手写 model); QTreeWidgetItem→QStandardItem, 勾选/点击逻辑改按 QModelIndex/itemChanged; ElaTreeView 自绘 展开折叠指示(去掉自定义 chevron QSS); 行为(TM 勾选/点击发 tmClicked/tmCheckToggled)保持 --- src/app/ProjectListDialog.cpp | 3 +- src/app/panels/ObjectTreePanel.cpp | 59 ++++++++++++++---------------- src/app/panels/ObjectTreePanel.hpp | 6 ++- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index cbc7531..46ccffa 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace geopro::app { namespace { @@ -66,7 +67,7 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa filter->addStretch(); root->addLayout(filter); - table_ = new QTableWidget(this); + table_ = new ElaTableWidget(this); // Ela item 版表格(继承 QTableWidget),直替 table_->setColumnCount(8); table_->setHorizontalHeaderLabels(QStringList{ QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"), diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 66c1ea7..a550089 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -1,13 +1,15 @@ #include "panels/ObjectTreePanel.hpp" -#include #include +#include #include -#include -#include +#include +#include +#include #include -#include "Glyphs.hpp" +#include + #include "Theme.hpp" #include "dto/NavDto.hpp" @@ -17,15 +19,16 @@ namespace { // TM 节点把 tmObjectId 存在该角色;GS/项目根节点为空。 constexpr int kRoleTmId = Qt::UserRole + 2; -void addNodes(QTreeWidgetItem* parent, const std::vector& nodes) { +void addNodes(QStandardItem* parent, const std::vector& nodes) { for (const auto& n : nodes) { - auto* item = new QTreeWidgetItem(parent); - item->setText(0, QString::fromStdString(n.node.name)); + auto* item = new QStandardItem(QString::fromStdString(n.node.name)); + item->setEditable(false); if (n.isTm) { - item->setData(0, kRoleTmId, QString::fromStdString(n.node.id)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, Qt::Unchecked); // 真实数据渲染下一轮接入,默认不勾 + item->setData(QString::fromStdString(n.node.id), kRoleTmId); + item->setCheckable(true); + item->setCheckState(Qt::Unchecked); // 真实数据渲染下一轮接入,默认不勾 } + parent->appendRow(item); addNodes(item, n.children); } } @@ -36,19 +39,10 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { lay->setContentsMargins(0, 0, 0, 0); lay->setSpacing(0); - tree_ = new QTreeWidget(this); + tree_ = new ElaTreeView(this); // Fluent 树视图(自绘展开/折叠指示,随主题) tree_->setHeaderHidden(true); - { - const QString openArrow = writeChevronIcon(true, QColor("#8A93A3")); - const QString closedArrow = writeChevronIcon(false, QColor("#8A93A3")); - geopro::app::applyThemedStyleSheet( - tree_, QStringLiteral("QTreeView::branch { background: #FFFFFF; }" - "QTreeView::branch:has-children:!has-siblings:closed," - "QTreeView::branch:closed:has-children:has-siblings { image: url(%1); }" - "QTreeView::branch:open:has-children:!has-siblings," - "QTreeView::branch:open:has-children:has-siblings { image: url(%2); }") - .arg(closedArrow, openArrow)); - } + model_ = new QStandardItemModel(tree_); + tree_->setModel(model_); lay->addWidget(tree_, 1); hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); @@ -57,21 +51,22 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { hint_->setVisible(false); lay->addWidget(hint_); - QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) { - const QString tmId = item->data(0, kRoleTmId).toString(); + // 单击 TM → tmClicked(按 index 取 tmId)。 + QObject::connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& idx) { + const QString tmId = idx.data(kRoleTmId).toString(); if (!tmId.isEmpty()) emit tmClicked(tmId); }); - QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item, int) { - const QString tmId = item->data(0, kRoleTmId).toString(); - if (!tmId.isEmpty()) - emit tmCheckToggled(tmId, item->checkState(0) == Qt::Checked); + // 勾选变化 → tmCheckToggled。 + QObject::connect(model_, &QStandardItemModel::itemChanged, this, [this](QStandardItem* item) { + const QString tmId = item->data(kRoleTmId).toString(); + if (!tmId.isEmpty()) emit tmCheckToggled(tmId, item->checkState() == Qt::Checked); }); } void ObjectTreePanel::setStructure(const QString& projectName, const std::vector& nodes) { - const QSignalBlocker block(tree_); // 重建触发 itemChanged,先屏蔽 - tree_->clear(); + const QSignalBlocker block(model_); // 重建触发 itemChanged,先屏蔽 + model_->clear(); const auto roots = data::dto::buildStructTree(nodes); if (roots.empty()) { showMessage(projectName.isEmpty() ? QStringLiteral("(暂无项目)") @@ -80,12 +75,12 @@ void ObjectTreePanel::setStructure(const QString& projectName, } hint_->setVisible(false); tree_->setVisible(true); - addNodes(tree_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染 + addNodes(model_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染 tree_->expandAll(); } void ObjectTreePanel::showMessage(const QString& message) { - tree_->clear(); + model_->clear(); tree_->setVisible(false); hint_->setText(message); hint_->setVisible(true); diff --git a/src/app/panels/ObjectTreePanel.hpp b/src/app/panels/ObjectTreePanel.hpp index 3621386..33cfff4 100644 --- a/src/app/panels/ObjectTreePanel.hpp +++ b/src/app/panels/ObjectTreePanel.hpp @@ -3,7 +3,8 @@ #include #include "repo/RepoTypes.hpp" -class QTreeWidget; +class QTreeView; +class QStandardItemModel; class QLabel; namespace geopro::app { @@ -24,7 +25,8 @@ signals: void tmCheckToggled(const QString& tmObjectId, bool checked); private: - QTreeWidget* tree_ = nullptr; + QTreeView* tree_ = nullptr; // ElaTreeView(继承 QTreeView) + QStandardItemModel* model_ = nullptr; // 标准 model(Qt 自带) QLabel* hint_ = nullptr; }; From 8e7563c0f50d692b58a126ce63ea1d7fc16d7278 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 09:48:24 +0800 Subject: [PATCH 18/56] =?UTF-8?q?feat(ela):=20=E6=95=B0=E6=8D=AE=E9=9B=86/?= =?UTF-8?q?=E6=96=87=E4=BB=B6/=E5=BC=82=E5=B8=B8=E5=88=97=E8=A1=A8=20?= =?UTF-8?q?=E2=86=92=20ElaListView=20+=20QStandardItemModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DatasetListPanel/AnomalyListPanel: populate 签名 QListWidget*→QStandardItemModel*, QListWidgetItem→QStandardItem(setData(value,role)/setCheckable/setIcon/setForeground) - main.cpp: 3 列表 → ElaListView + QStandardItemModel; removeLoadMore/addLoadMore 改 model (rowCount/item/removeRow/appendRow); itemClicked→clicked(QModelIndex); anomaly itemChanged→model itemChanged; 加载更多/勾选显隐/点击 行为保持 - 注: 列表交互(异常显隐/加载更多/数据集点击)为活逻辑, 需运行验证 --- src/app/main.cpp | 97 ++++++++++++++--------------- src/app/panels/AnomalyListPanel.cpp | 18 +++--- src/app/panels/AnomalyListPanel.hpp | 4 +- src/app/panels/DatasetListPanel.cpp | 37 ++++++----- src/app/panels/DatasetListPanel.hpp | 10 +-- 5 files changed, 88 insertions(+), 78 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 2e8fb92..0d80ffd 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -34,8 +34,9 @@ #include #include #include -#include -#include +#include +#include +#include #include #include #include @@ -64,6 +65,7 @@ #include #include #include +#include #include #include @@ -502,17 +504,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 auto* datasetTabs = new QTabWidget(); - auto* datasetList = new QListWidget(); - // 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。随主题着色。 - const QString kListQss = QStringLiteral( - "QListWidget{ background:#FFFFFF; border:none; outline:none; }" - "QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }" - "QListWidget::item:hover{ background:#EEF3FB; }" - "QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"); - geopro::app::applyThemedStyleSheet(datasetList, kListQss); + auto* datasetList = new ElaListView(); // Fluent 列表(自绘 hover/选中, 随主题) + auto* datasetModel = new QStandardItemModel(datasetList); + datasetList->setModel(datasetModel); datasetTabs->addTab(datasetList, QStringLiteral("数据")); - auto* fileList = new QListWidget(); - geopro::app::applyThemedStyleSheet(fileList, kListQss); // 与数据页签同款简洁分割 + auto* fileList = new ElaListView(); + auto* fileModel = new QStandardItemModel(fileList); + fileList->setModel(fileModel); datasetTabs->addTab(fileList, QStringLiteral("文件")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetBox = wrapWithHeader( @@ -525,8 +523,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea); // 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 - auto* anomalyList = new QListWidget(); - anomalyList->setAlternatingRowColors(true); + auto* anomalyList = new ElaListView(); + auto* anomalyModel = new QStandardItemModel(anomalyList); + anomalyList->setModel(anomalyModel); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); objAttrLabel->setWordWrap(true); objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); @@ -686,7 +685,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 - auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms, + auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyModel, hiddenAnoms, anomalyBadge](const QString& dsId, const QString& name) { if (dsId.isEmpty()) return; *currentDsId = dsId; @@ -695,8 +694,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const auto anomalies = repo.loadAnomalies(dsId.toStdString()); hiddenAnoms->clear(); { - const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽 - geopro::app::populateAnomalyList(anomalyList, anomalies); + const QSignalBlocker block(anomalyModel); // 重填触发 itemChanged,先屏蔽 + geopro::app::populateAnomalyList(anomalyModel, anomalies); } // 异常列表 Tab 数量徽标。 if (anomalyBadge) { @@ -722,14 +721,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── // 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。 - QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, - [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) { - if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { + QObject::connect(datasetList, &QAbstractItemView::clicked, datasetList, + [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](const QModelIndex& idx) { + if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) { nav.loadMoreData(); return; } - const QString name = - item->data(Qt::DisplayRole).toString().section('\n', 0, 0); + const QString name = idx.data(Qt::DisplayRole).toString().section('\n', 0, 0); detailRendererPtr->RemoveAllViewProps(); detailRenderWindowPtr->Render(); propLabel->setText(QStringLiteral( @@ -737,8 +735,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); // ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ── - QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList, - [hiddenAnoms, rebuildDetail](QListWidgetItem* item) { + QObject::connect(anomalyModel, &QStandardItemModel::itemChanged, anomalyList, + [hiddenAnoms, rebuildDetail](QStandardItem* item) { const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt(); if (item->checkState() == Qt::Checked) hiddenAnoms->erase(idx); @@ -851,19 +849,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 控制器 ↔ UI 信号接线(导航壳)────────────────────────────────────── // "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。 - auto removeLoadMore = [](QListWidget* lw) { - if (lw->count() > 0 && - lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) - delete lw->takeItem(lw->count() - 1); + auto removeLoadMore = [](QStandardItemModel* mdl) { + const int n = mdl->rowCount(); + if (n > 0 && mdl->item(n - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) + mdl->removeRow(n - 1); }; - auto addLoadMore = [](QListWidget* lw, int total) { - const int loaded = lw->count(); + auto addLoadMore = [](QStandardItemModel* mdl, int total) { + const int loaded = mdl->rowCount(); if (loaded < total) { - auto* m = new QListWidgetItem( - QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw); - m->setData(geopro::app::kDsLoadMoreRole, true); - m->setTextAlignment(Qt::AlignCenter); - m->setForeground(QColor("#2D6CB5")); + auto* it = new QStandardItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total)); + it->setData(true, geopro::app::kDsLoadMoreRole); + it->setTextAlignment(Qt::AlignCenter); + it->setForeground(QColor("#2D6CB5")); + it->setEditable(false); + mdl->appendRow(it); } return loaded; }; @@ -902,42 +901,42 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re topBar->setProjects(list, cur, total > static_cast(list.size())); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree, - [objectTree, datasetList, fileList, datasetTitle, datasetTabs]( + [objectTree, datasetModel, fileModel, datasetTitle, datasetTabs]( const QString& projectName, const std::vector& nodes) { objectTree->setStructure(projectName, nodes); - datasetList->clear(); - fileList->clear(); + datasetModel->clear(); + fileModel->clear(); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); datasetTabs->setTabText(0, QStringLiteral("数据")); datasetTabs->setTabText(1, QStringLiteral("文件")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, - [removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs]( + [removeLoadMore, addLoadMore, datasetModel, datasetTitle, datasetTabs]( const QString&, const std::vector& rows, int total, bool append) { - removeLoadMore(datasetList); - geopro::app::populateDatasetList(datasetList, rows, append); - const int loaded = addLoadMore(datasetList, total); + removeLoadMore(datasetModel); + geopro::app::populateDatasetList(datasetModel, rows, append); + const int loaded = addLoadMore(datasetModel, total); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); datasetTabs->setTabText( 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) : QStringLiteral("数据")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList, - [removeLoadMore, addLoadMore, fileList, datasetTabs]( + [removeLoadMore, addLoadMore, fileModel, datasetTabs]( const QString&, const std::vector& rows, int total, bool append) { - removeLoadMore(fileList); - geopro::app::populateFileList(fileList, rows, append); - const int loaded = addLoadMore(fileList, total); + removeLoadMore(fileModel); + geopro::app::populateFileList(fileModel, rows, append); + const int loaded = addLoadMore(fileModel, total); datasetTabs->setTabText( 1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total) : QStringLiteral("文件")); }); - QObject::connect(fileList, &QListWidget::itemClicked, fileList, - [&nav](QListWidgetItem* item) { - if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); + QObject::connect(fileList, &QAbstractItemView::clicked, fileList, + [&nav](const QModelIndex& idx) { + if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree, [objectTree, &window](const QString& stage, const QString& msg) { diff --git a/src/app/panels/AnomalyListPanel.cpp b/src/app/panels/AnomalyListPanel.cpp index 6f5fe2d..198a83d 100644 --- a/src/app/panels/AnomalyListPanel.cpp +++ b/src/app/panels/AnomalyListPanel.cpp @@ -5,9 +5,9 @@ #include #include -#include -#include #include +#include +#include #include #include "model/ColorScale.hpp" @@ -54,10 +54,10 @@ QPixmap swatch(const std::string& colorStr) } // namespace -void populateAnomalyList(QListWidget* list, const std::vector& anomalies) +void populateAnomalyList(QStandardItemModel* model, const std::vector& anomalies) { - if (!list) return; - list->clear(); + if (!model) return; + model->clear(); for (std::size_t i = 0; i < anomalies.size(); ++i) { const auto& a = anomalies[i]; const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name); @@ -66,10 +66,12 @@ void populateAnomalyList(QListWidget* list, const std::vectorsetData(kAnomalyIndexRole, static_cast(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + auto* item = new QStandardItem(QIcon(swatch(a.lineColor)), text); + item->setEditable(false); + item->setData(static_cast(i), kAnomalyIndexRole); + item->setCheckable(true); item->setCheckState(Qt::Checked); // 默认显示 + model->appendRow(item); } } diff --git a/src/app/panels/AnomalyListPanel.hpp b/src/app/panels/AnomalyListPanel.hpp index e5566ed..326d488 100644 --- a/src/app/panels/AnomalyListPanel.hpp +++ b/src/app/panels/AnomalyListPanel.hpp @@ -3,7 +3,7 @@ #include "model/Anomaly.hpp" -class QListWidget; +class QStandardItemModel; namespace geopro::app { @@ -14,6 +14,6 @@ constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole // 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。 // 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。 // 清空旧条目后重填。 -void populateAnomalyList(QListWidget* list, const std::vector& anomalies); +void populateAnomalyList(QStandardItemModel* model, const std::vector& anomalies); } // namespace geopro::app diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 2ca911f..5022835 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -1,8 +1,8 @@ #include "panels/DatasetListPanel.hpp" #include -#include -#include +#include +#include #include namespace geopro::app { @@ -16,29 +16,34 @@ QString humanSize(long long b) { } } // namespace -void populateDatasetList(QListWidget* list, const std::vector& rows, bool append) { - if (!list) return; - if (!append) list->clear(); +void populateDatasetList(QStandardItemModel* model, const std::vector& rows, + bool append) { + if (!model) return; + if (!append) model->clear(); for (const auto& d : rows) { QString text = QString::fromStdString(d.dsName); QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 if (!d.typeName.empty()) sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); - auto* item = new QListWidgetItem(text, list); - item->setData(kDsIdRole, QString::fromStdString(d.id)); - item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode)); + auto* item = new QStandardItem(text); + item->setEditable(false); + item->setData(QString::fromStdString(d.id), kDsIdRole); + item->setData(QString::fromStdString(d.ddCode), kDsDdTypeRole); + model->appendRow(item); } } -void populateFileList(QListWidget* list, const std::vector& rows, bool append) { - if (!list) return; - if (!append) list->clear(); +void populateFileList(QStandardItemModel* model, const std::vector& rows, + bool append) { + if (!model) return; + if (!append) model->clear(); if (!append && rows.empty()) { - auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); + auto* hint = new QStandardItem(QStringLiteral("(暂无文件)")); hint->setFlags(Qt::NoItemFlags); hint->setForeground(QColor("#9AA6B6")); hint->setTextAlignment(Qt::AlignCenter); + model->appendRow(hint); return; } for (const auto& d : rows) { @@ -47,9 +52,11 @@ void populateFileList(QListWidget* list, const std::vector& QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小 const QString text = fname + QStringLiteral("\n%1").arg(sub); - auto* item = new QListWidgetItem(text, list); - item->setData(kDsIdRole, QString::fromStdString(d.id)); - item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl)); + auto* item = new QStandardItem(text); + item->setEditable(false); + item->setData(QString::fromStdString(d.id), kDsIdRole); + item->setData(QString::fromStdString(d.fileUrl), kDsFileUrlRole); + model->appendRow(item); } } diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index 0356760..f512c2c 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -3,7 +3,7 @@ #include "repo/RepoTypes.hpp" -class QListWidget; +class QStandardItemModel; namespace geopro::app { @@ -13,9 +13,11 @@ constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1 constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2(文件下载 url,备用) constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行 -// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。 -void populateDatasetList(QListWidget* list, const std::vector& rows, bool append); +// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。填入标准 model。 +void populateDatasetList(QStandardItemModel* model, const std::vector& rows, + bool append); // 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。 -void populateFileList(QListWidget* list, const std::vector& rows, bool append); +void populateFileList(QStandardItemModel* model, const std::vector& rows, + bool append); } // namespace geopro::app From b6e0142d06c7ead8d870e17bf1009ce865301613 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 10:23:09 +0800 Subject: [PATCH 19/56] =?UTF-8?q?feat(ela):=20TopBar=20=E5=9B=BE=E6=A0=87/?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=99=A8=20+=20PanelHeader=20=E9=A1=B5?= =?UTF-8?q?=E7=AD=BE/=E6=93=8D=E4=BD=9C=E6=8C=89=E9=92=AE=20=E5=85=A8=20El?= =?UTF-8?q?a=20=E5=8C=96(=E5=A4=B4=E5=83=8F=E9=99=A4=E5=A4=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TopBar: help/bell/gear → ElaIconButton(Fluent 图标字体, 自动主题); 工作空间/项目切换器 → ElaToolButton; 去掉 #wsSwitcher/#iconBtn QSS(Ela 自绘); 头像保留自定义圆形(白字用 white 关键字恒白) - PanelHeader: 数据/文件等页签 → ElaToolButton(选中态走 Ela checked 高亮); 表头操作按钮 → ElaIconButton; headerQss 去掉 tabBtn/panelAction 规则 --- src/app/PanelHeader.cpp | 35 ++++++++++++------------------ src/app/TopBar.cpp | 47 ++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 5e4a6e3..bae2467 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -2,6 +2,9 @@ #include "Theme.hpp" +#include +#include + #include #include #include @@ -26,25 +29,19 @@ constexpr int kTabIcon = 19; // Tab 图标 // 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。 // #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色), // 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。 +// 表头底/标题/徽标样式。Tab(ElaToolButton)与操作按钮(ElaIconButton)自绘 Fluent, +// 不再写它们的 QSS(选中态由 Ela 的 checked 高亮表达,而非自定义下划线)。 QString headerQss() { return QStringLiteral( "#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }" - "#panelTitle { color:#1F2A3D; font-size:%1px; font-weight:%4; }" + "#panelTitle { color:#1F2A3D; font-size:%1px; font-weight:%3; }" "#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;" - " padding:1px 7px; font-size:%2px; font-weight:%4; }" - "#panelBadgeWarn { background:%5; color:%6; border-radius:9px;" - " padding:1px 7px; font-size:%2px; font-weight:%4; }" - "QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }" - "QToolButton#panelAction:hover { background:#EEF3FB; }" - "QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;" - " padding:8px 4px; font-size:%3px; }" - "QToolButton#tabBtn:hover { color:#1F2A3D; }" - "QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:%4;" - " border-bottom:2px solid #2D6CB5; }") + " 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; }") .arg(type::kTitle) .arg(type::kCaption) - .arg(type::kBody) .arg(type::kWeightSemibold) .arg(QString::fromUtf8(semantic::kWarningFill)) .arg(QString::fromUtf8(semantic::kWarning)); @@ -61,16 +58,13 @@ QLabel* makeBadge(QWidget* parent) return badge; } -// 表头操作按钮(静态占位)。 -QToolButton* makeActionButton(QWidget* parent, const HeaderAction& a) +// 表头操作按钮(Fluent ElaIconButton;图标沿用项目 glyph 像素图)。 +QWidget* makeActionButton(QWidget* parent, const HeaderAction& a) { - auto* btn = new QToolButton(parent); - btn->setObjectName(QStringLiteral("panelAction")); - btn->setIcon(makeGlyph(a.first, QColor("#5A6B85"), kActionIcon)); - btn->setIconSize(QSize(kActionIcon, kActionIcon)); + auto* btn = new ElaIconButton( + makeGlyph(a.first, QColor("#5A6B85"), kActionIcon).pixmap(kActionIcon, kActionIcon), parent); btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(a.second + QStringLiteral("(占位)")); - btn->setAutoRaise(true); return btn; } @@ -128,7 +122,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("tabBtn")); btn->setText(t.title); btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), kTabIcon)); @@ -136,7 +130,6 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setToolButtonStyle(Qt::ToolButtonTextBesideIcon); btn->setCheckable(true); btn->setCursor(Qt::PointingHandCursor); - btn->setAutoRaise(true); group->addButton(btn, i); hlay->addWidget(btn); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 00f7125..54eb752 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -16,8 +16,11 @@ #include "Glyphs.hpp" #include "Theme.hpp" +#include +#include #include #include +#include namespace geopro::app { @@ -38,16 +41,12 @@ QFrame* makeDivider(QWidget* parent) return line; } -// 右侧图标按钮(仅图标,悬停显示文本)。 -QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip) +// 右侧图标按钮(Fluent ElaIconButton:自带图标字体 + 悬停 + 随主题着色)。 +QWidget* makeIconButton(QWidget* parent, ElaIconType::IconName icon, const QString& tip) { - auto* btn = new QToolButton(parent); - btn->setObjectName(QStringLiteral("iconBtn")); - btn->setIcon(makeGlyph(g, QColor("#5A6B85"), kToolIcon)); - btn->setIconSize(QSize(kToolIcon, kToolIcon)); + auto* btn = new ElaIconButton(icon, kToolIcon, parent); btn->setToolTip(tip); btn->setCursor(Qt::PointingHandCursor); - btn->setAutoRaise(true); return btn; } @@ -131,33 +130,29 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { setFixedHeight(56); // 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、 // 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。 + // 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent,不再写它们的 QSS。 + // 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 white 关键字(恒白)。 geopro::app::applyThemedStyleSheet( this, QStringLiteral( "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" "#topDivider { color:#E1E6EE; }" - "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" - " font-size:%1px; font-weight:%5; }" - "#wsSwitcher:hover { background:#EEF3FB; }" - "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" - "QToolButton#iconBtn:hover { background:#EEF3FB; }" "QToolButton::menu-indicator { image:none; }" - "#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:%6;" - " font-size:%2px; }" - "#userName { color:#1F2A3D; font-size:%3px; font-weight:%5; }" - "#userRole { color:#8A93A3; font-size:%4px; }") - .arg(type::kTitle) + "#avatar { background:#2D6CB5; color:white; border-radius:17px; font-weight:%2;" + " font-size:%1px; }" + "#userName { color:#1F2A3D; font-size:%3px; font-weight:%4; }" + "#userRole { color:#8A93A3; font-size:%5px; }") .arg(type::kBody) + .arg(type::kWeightBold) .arg(type::kLabel) - .arg(type::kCaption) .arg(type::kWeightSemibold) - .arg(type::kWeightBold)); + .arg(type::kCaption)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); lay->setSpacing(0); - // 工作空间切换器(数据驱动;初始占位文本,待 setWorkspaces 填充)。 - wsBtn_ = new QToolButton(this); + // 工作空间切换器(Fluent ElaToolButton;数据驱动,初始占位文本待 setWorkspaces 填充)。 + wsBtn_ = new ElaToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); wsBtn_->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), kWorkspaceIcon)); wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); @@ -172,8 +167,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(10); - // 项目切换器(数据驱动)。 - projBtn_ = new QToolButton(this); + // 项目切换器(Fluent ElaToolButton;数据驱动)。 + projBtn_ = new ElaToolButton(this); projBtn_->setObjectName(QStringLiteral("wsSwitcher")); projBtn_->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), kWorkspaceIcon)); projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); @@ -186,9 +181,9 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addStretch(); - lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助"))); - lay->addWidget(makeIconButton(this, Glyph::Bell, QStringLiteral("通知"))); - lay->addWidget(makeIconButton(this, Glyph::Gear, QStringLiteral("设置"))); + lay->addWidget(makeIconButton(this, ElaIconType::CircleQuestion, QStringLiteral("帮助"))); + lay->addWidget(makeIconButton(this, ElaIconType::Bell, QStringLiteral("通知"))); + lay->addWidget(makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置"))); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(12); From 3aa4e1bfe206ba18a9b23ffcc776d0c536154efd Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 10:27:33 +0800 Subject: [PATCH 20/56] =?UTF-8?q?feat(ela):=20=E8=A7=86=E5=9B=BE/=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=B7=A5=E5=85=B7=E6=9D=A1=20=E2=86=92=20ElaToolButto?= =?UTF-8?q?n=20=E8=A1=8C(=E6=9B=BF=20QToolBar+QAction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 视图工具条(二维地图/三维视图): QToolBar+QActionGroup → QWidget+QHBoxLayout+ElaToolButton+QButtonGroup(互斥) - 详情工具条(原数据/网格数据 互斥 + 显示异常/电极/等值线 开关): 同上 + QFrame 分隔 - 连接: QAction::triggered→QAbstractButton::clicked, QAction::toggled→QAbstractButton::toggled; 视图模式/详情模式/叠加显隐 行为保持 - 注: 工具条交互(2D/3D 切换/详情模式/三个显隐开关)为活逻辑, 需运行验证 --- src/app/main.cpp | 86 +++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 0d80ffd..7bc58b6 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -31,7 +31,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -67,6 +69,7 @@ #include #include #include +#include #include #include "model/ColorScale.hpp" @@ -324,16 +327,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re centerLayout->setContentsMargins(0, 0, 0, 0); centerLayout->setSpacing(0); - // 工具条:「二维地图/三维视图」两个互斥可勾选 action。切换=按当前勾选集重建对应内容。默认二维地图。 - auto* viewToolBar = new QToolBar(); - auto* viewGroup = new QActionGroup(viewToolBar); + // 工具条:「二维地图/三维视图」两个互斥可勾选按钮(Fluent ElaToolButton)。默认二维地图。 + auto* viewToolBar = new QWidget(); + auto* viewBarLay = new QHBoxLayout(viewToolBar); + viewBarLay->setContentsMargins(8, 6, 8, 6); + viewBarLay->setSpacing(6); + auto* viewGroup = new QButtonGroup(viewToolBar); viewGroup->setExclusive(true); - auto* act2D = viewToolBar->addAction(QStringLiteral("二维地图")); - auto* act3D = viewToolBar->addAction(QStringLiteral("三维视图")); + auto* act2D = new ElaToolButton(viewToolBar); + act2D->setText(QStringLiteral("二维地图")); act2D->setCheckable(true); + auto* act3D = new ElaToolButton(viewToolBar); + act3D->setText(QStringLiteral("三维视图")); act3D->setCheckable(true); - viewGroup->addAction(act2D); - viewGroup->addAction(act3D); + viewGroup->addButton(act2D); + viewGroup->addButton(act3D); + viewBarLay->addWidget(act2D); + viewBarLay->addWidget(act3D); + viewBarLay->addStretch(); act2D->setChecked(true); // 默认二维地图 centerLayout->addWidget(viewToolBar); centerLayout->addWidget(vtkWidget, 1); @@ -462,27 +473,42 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailLayout->setContentsMargins(0, 0, 0, 0); detailLayout->setSpacing(0); - // 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常」开关。 - auto* detailToolBar = new QToolBar(); - auto* detailGroup = new QActionGroup(detailToolBar); + // 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关(Fluent ElaToolButton)。 + auto* detailToolBar = new QWidget(); + auto* detailBarLay = new QHBoxLayout(detailToolBar); + detailBarLay->setContentsMargins(8, 6, 8, 6); + detailBarLay->setSpacing(6); + auto makeBarBtn = [detailToolBar](const QString& text, bool checkable) { + auto* b = new ElaToolButton(detailToolBar); + b->setText(text); + b->setCheckable(checkable); + return b; + }; + auto* detailGroup = new QButtonGroup(detailToolBar); detailGroup->setExclusive(true); - auto* actScatter = detailToolBar->addAction(QStringLiteral("原数据")); - auto* actSection = detailToolBar->addAction(QStringLiteral("网格数据")); - actScatter->setCheckable(true); - actSection->setCheckable(true); - detailGroup->addAction(actScatter); - detailGroup->addAction(actSection); + auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true); + auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true); + detailGroup->addButton(actScatter); + detailGroup->addButton(actSection); + detailBarLay->addWidget(actScatter); + detailBarLay->addWidget(actSection); actSection->setChecked(true); // 默认网格数据 (#18) - detailToolBar->addSeparator(); - auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常")); - actShowAnomaly->setCheckable(true); + auto* barSep = new QFrame(detailToolBar); + barSep->setFrameShape(QFrame::VLine); + barSep->setObjectName(QStringLiteral("topDivider")); + detailBarLay->addSpacing(4); + detailBarLay->addWidget(barSep); + detailBarLay->addSpacing(4); + auto* actShowAnomaly = makeBarBtn(QStringLiteral("显示异常"), true); actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常) - auto* actShowElectrodes = detailToolBar->addAction(QStringLiteral("显示电极")); - actShowElectrodes->setCheckable(true); + auto* actShowElectrodes = makeBarBtn(QStringLiteral("显示电极"), true); actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型) - auto* actShowContour = detailToolBar->addAction(QStringLiteral("显示等值线")); - actShowContour->setCheckable(true); + auto* actShowContour = makeBarBtn(QStringLiteral("显示等值线"), true); actShowContour->setChecked(true); // 默认显示等值线(对齐原型) + detailBarLay->addWidget(actShowAnomaly); + detailBarLay->addWidget(actShowElectrodes); + detailBarLay->addWidget(actShowContour); + detailBarLay->addStretch(); detailLayout->addWidget(detailToolBar); detailLayout->addWidget(detailWidget, 1); @@ -746,29 +772,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); // ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ── - QObject::connect(actSection, &QAction::triggered, detailWidget, + QObject::connect(actSection, &QAbstractButton::clicked, detailWidget, [detailMode, rebuildDetail]() { *detailMode = DetailMode::Section18; rebuildDetail(); }); - QObject::connect(actScatter, &QAction::triggered, detailWidget, + QObject::connect(actScatter, &QAbstractButton::clicked, detailWidget, [detailMode, rebuildDetail]() { *detailMode = DetailMode::Scatter17; rebuildDetail(); }); // ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ── - QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget, + QObject::connect(actShowAnomaly, &QAbstractButton::toggled, detailWidget, [showAnomalies, rebuildDetail](bool on) { *showAnomalies = on; rebuildDetail(); }); - QObject::connect(actShowElectrodes, &QAction::toggled, detailWidget, + QObject::connect(actShowElectrodes, &QAbstractButton::toggled, detailWidget, [showElectrodes, rebuildDetail](bool on) { *showElectrodes = on; rebuildDetail(); }); - QObject::connect(actShowContour, &QAction::toggled, detailWidget, + QObject::connect(actShowContour, &QAbstractButton::toggled, detailWidget, [showContour, rebuildDetail](bool on) { *showContour = on; rebuildDetail(); @@ -787,13 +813,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; // ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ── - QObject::connect(act2D, &QAction::triggered, vtkWidget, + QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget, [viewMode, rebuildCentral, showLayerPanel]() { *viewMode = ViewMode::Map2D; showLayerPanel(false); rebuildCentral(); }); - QObject::connect(act3D, &QAction::triggered, vtkWidget, + QObject::connect(act3D, &QAbstractButton::clicked, vtkWidget, [viewMode, rebuildCentral, showLayerPanel]() { *viewMode = ViewMode::View3D; showLayerPanel(true); From 2be49b205cebf6b0c35a03cd4deb68ff30a45cc8 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 10:35:50 +0800 Subject: [PATCH 21/56] =?UTF-8?q?fix(ela):=20=E5=9B=BE=E6=A0=87=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=8F=98=E5=BD=A2=20+=20tooltip=20=E4=B8=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ElaIconButton(icon,pixelSize,parent) 不设固定尺寸→图标被压扁; 改用带固定宽高的构造 (TopBar help/bell/gear: icon,18,34,34); PanelHeader 操作按钮(QPixmap 构造)显式 setFixedSize(30,30) - 删全局 QToolTip QSS(深蓝底+蓝边框+圆角→弹窗露直角且不像原生)→ 用系统原生工具提示 --- src/app/PanelHeader.cpp | 1 + src/app/Theme.cpp | 8 +------- src/app/TopBar.cpp | 3 ++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index bae2467..25418dc 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -63,6 +63,7 @@ QWidget* makeActionButton(QWidget* parent, const HeaderAction& a) { auto* btn = new ElaIconButton( makeGlyph(a.first, QColor("#5A6B85"), kActionIcon).pixmap(kActionIcon, kActionIcon), parent); + btn->setFixedSize(30, 30); // QPixmap 构造不设尺寸,显式固定防变形 btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(a.second + QStringLiteral("(占位)")); return btn; diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 1f4c24a..b4da13f 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -27,13 +27,7 @@ QWidget { QMainWindow, QDialog { background: #F4F6FA; } -QToolTip { - background: #1F2A3D; - color: #F4F6FA; - border: 1px solid #2D6CB5; - border-radius: 6px; - padding: 4px 8px; -} +/* QToolTip 不写 QSS:用系统原生工具提示(自定义 QSS 会让弹窗圆角露直角、且不像原生)。 */ /* ── 视图内工具条(2D/3D、数据详情):白底分段控件,柔和不刺眼 ── */ QToolBar { diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 54eb752..9d3d1d4 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -42,9 +42,10 @@ QFrame* makeDivider(QWidget* parent) } // 右侧图标按钮(Fluent ElaIconButton:自带图标字体 + 悬停 + 随主题着色)。 +// 用带固定宽高的构造(icon, 字号, 宽, 高, parent)——否则图标会被压扁变形。 QWidget* makeIconButton(QWidget* parent, ElaIconType::IconName icon, const QString& tip) { - auto* btn = new ElaIconButton(icon, kToolIcon, parent); + auto* btn = new ElaIconButton(icon, 18, 34, 34, parent); btn->setToolTip(tip); btn->setCursor(Qt::PointingHandCursor); return btn; From 5a945e51a76ba44ff1e70f10298e62a46aac4e2a Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 10:47:16 +0800 Subject: [PATCH 22/56] =?UTF-8?q?fix(ela):=20=E9=9D=A2=E6=9D=BF=E8=A1=A8?= =?UTF-8?q?=E5=A4=B4/=E9=A1=B5=E7=AD=BE=E5=9B=BE=E6=A0=87=E9=9A=8F?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E7=9D=80=E8=89=B2(=E7=82=B93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 setThemedGlyph(QLabel*/QAbstractButton*, Glyph, px): 取 ElaTheme 主文本色(暗=浅/亮=深)绘制, 主题切换自动重绘 - PanelHeader 表头标题图标(原 #44546B 固定→暗色看不清)、页签图标(原 #5A6B85) 改走 setThemedGlyph → 对象显示栏/数据集显示栏/异常列表等所有面板图标在明暗下都清晰 --- src/app/Glyphs.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/app/Glyphs.hpp | 8 ++++++++ src/app/PanelHeader.cpp | 4 ++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/app/Glyphs.cpp b/src/app/Glyphs.cpp index 24255ad..ff901a5 100644 --- a/src/app/Glyphs.cpp +++ b/src/app/Glyphs.cpp @@ -1,8 +1,11 @@ #include "Glyphs.hpp" +#include #include #include #include +#include +#include #include #include #include @@ -11,6 +14,11 @@ #include #include +#include +#include + +#include "Theme.hpp" + namespace geopro::app { namespace { @@ -150,4 +158,33 @@ QString writeChevronIcon(bool open, const QColor& color) return path; } +namespace { +// 当前主题下的 chrome 图标色:取主题主文本色(暗=浅、亮=深),保证两种模式都清晰。 +QColor themedIconColor() +{ + return eTheme->getThemeColor( + isDarkTheme() ? ElaThemeType::Dark : ElaThemeType::Light, ElaThemeType::BasicText); +} +} // namespace + +void setThemedGlyph(QLabel* label, Glyph type, int px) +{ + if (!label) return; + auto apply = [label, type, px]() { + label->setPixmap(makeGlyph(type, themedIconColor(), px).pixmap(px, px)); + }; + apply(); + QObject::connect(eTheme, &ElaTheme::themeModeChanged, label, + [apply](ElaThemeType::ThemeMode) { apply(); }); +} + +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(); }); +} + } // namespace geopro::app diff --git a/src/app/Glyphs.hpp b/src/app/Glyphs.hpp index 9ac1347..90eeaba 100644 --- a/src/app/Glyphs.hpp +++ b/src/app/Glyphs.hpp @@ -7,6 +7,9 @@ #include #include +class QLabel; +class QAbstractButton; + namespace geopro::app { enum class Glyph { @@ -34,6 +37,11 @@ enum class Glyph { // 生成指定颜色、像素尺寸的图标(默认 16px,内部按 2x 绘制保证清晰)。 QIcon makeGlyph(Glyph type, const QColor& color, int px = 16); +// 随 ElaTheme 明暗自动着色的 glyph(取主题文本色:暗色用浅色、亮色用深色),主题切换时自动重绘。 +// 用于面板表头/页签等 chrome 图标,避免固定色在暗色下看不清。 +void setThemedGlyph(QLabel* label, Glyph type, int px); +void setThemedGlyph(QAbstractButton* button, Glyph type, int px); + // 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。 // 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。 QString writeChevronIcon(bool open, const QColor& color); diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 25418dc..e953b8c 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -83,7 +83,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector
setSpacing(8); auto* iconLbl = new QLabel(header); - iconLbl->setPixmap(makeGlyph(icon, QColor("#44546B"), kTitleIcon).pixmap(kTitleIcon, kTitleIcon)); + setThemedGlyph(iconLbl, icon, kTitleIcon); // 随主题着色(暗色下也清晰) lay->addWidget(iconLbl); auto* titleLbl = new QLabel(title, header); @@ -126,7 +126,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("tabBtn")); btn->setText(t.title); - btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), kTabIcon)); + setThemedGlyph(btn, t.icon, kTabIcon); // 随主题着色 btn->setIconSize(QSize(kTabIcon, kTabIcon)); btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); btn->setCheckable(true); From c5393e8ac3d5825590b0dc565b4aa3b388bcd957 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 10:51:36 +0800 Subject: [PATCH 23/56] =?UTF-8?q?fix(ela):=20=E6=A0=87=E9=A2=98=E6=A0=8F?= =?UTF-8?q?=E6=94=B6=E7=B4=A7=20+=20=E5=88=87=E6=8D=A2=E5=99=A8=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E9=87=8D=E5=A4=8D=E4=B8=8B=E6=8B=89=E7=AE=AD=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ElaWindow AppBarHeight 45→38(默认偏大, 更接近原生标题栏高度)(点1 过大) - 切换器去掉文字里手加的 '▾'(ElaToolButton 给带菜单按钮自绘展开箭头, 原来与文字▾重复)(点2) - 注: 显示为 100% 缩放, 无 DPI 模糊; 若 Ela 自带标题栏图标仍觉软, 属其 Fluent 渲染风格 --- src/app/TopBar.cpp | 12 +++++------- src/app/main.cpp | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 9d3d1d4..9903acf 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -233,7 +233,7 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈 + wsBtn_->setText(name); // 立即反馈(下拉箭头由 ElaToolButton 自绘) emit workspaceSwitchRequested(id); }); } @@ -242,8 +242,7 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri none->setEnabled(false); } wsBtn_->setMenu(menu); - wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) + - QStringLiteral(" ▾")); + wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName); } void TopBar::setProjects(const std::vector& list, const QString& currentId, @@ -264,7 +263,7 @@ void TopBar::setProjects(const std::vector& list, const QS group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - projBtn_->setText(name + QStringLiteral(" ▾")); + projBtn_->setText(name); emit projectSwitchRequested(id); }); } @@ -278,12 +277,11 @@ void TopBar::setProjects(const std::vector& list, const QS QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); }); } projBtn_->setMenu(menu); - projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) + - QStringLiteral(" ▾")); + projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName); } void TopBar::setProjectButtonText(const QString& name) { - projBtn_->setText(name + QStringLiteral(" ▾")); + projBtn_->setText(name); } } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 7bc58b6..7b3dd98 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1095,6 +1095,7 @@ int main(int argc, char* argv[]) ela->resize(1280, 800); ela->setMinimumSize(1024, 680); ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏 + ela->setAppBarHeight(38); // 默认 45 偏大,收紧标题栏更接近原生 auto* inner = new QMainWindow(ela); // 以 ela 为父,避免无父期调色板/DPI 抖动(review H3) buildWorkbench(*inner, repo, projectRepo, nav); // 用 addPageNode 把工作台作为唯一页面放进中心页栈(填满到底边)。 From 0867636ea4b610292b9522375f7bf7f96b00a4da Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 11:05:45 +0800 Subject: [PATCH 24/56] =?UTF-8?q?feat(ela):=20=E7=BB=9F=E4=B8=80=E5=93=81?= =?UTF-8?q?=E7=89=8C=E5=BC=BA=E8=B0=83=E8=89=B2=20+=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=99=A8=E7=AE=AD=E5=A4=B4=E4=BF=AE=E5=A4=8D=20+=20=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E4=B8=AD=E6=80=A7=E5=8C=96=20+=20=E6=A0=91=E7=BC=A9?= =?UTF-8?q?=E8=BF=9B=E6=94=B6=E7=B4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配色根因: Ela 默认主色(#0067C0亮/#4CC2FF暗)与项目品牌蓝 #2D6CB5 是两种蓝, 互相打架。 - applyBrandAccent(): 用 ElaTheme::setThemeColor 把 Primary 设成品牌蓝(亮 #2D6CB5/暗 #5E9BD6, 含 Hover/Press 三态) → 所有 Ela 原生控件选中/激活/标题栏强调 + 本项目 QSS 共用一套蓝 - 切换器图标: #2D6CB5 → setThemedGlyph 中性主题色(蓝只留给选中/激活), 与面板图标一致 - 切换器箭头: 成员 QToolButton* → ElaToolButton*, setMenu 走 Ela 重载 → Ela 自绘清晰展开箭头 (之前去掉手加的▾后没箭头, 是因为基类 setMenu 不触发自绘) - 对象树缩进 20→14, 更紧凑 --- src/app/Theme.cpp | 12 ++++++++++++ src/app/Theme.hpp | 4 ++++ src/app/TopBar.cpp | 4 ++-- src/app/TopBar.hpp | 6 +++--- src/app/main.cpp | 1 + src/app/panels/ObjectTreePanel.cpp | 1 + 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index b4da13f..f059bae 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -456,6 +456,18 @@ 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)); +} + bool isDarkTheme() { return eTheme->getThemeMode() == ElaThemeType::Dark; diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index a4c4e08..b80f69e 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -90,6 +90,10 @@ void applyThemeMode(QApplication& app, bool dark); // 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。 void applyTheme(QApplication& app); +// 统一强调色:把 ElaTheme 主色(Primary)设为本项目品牌蓝(亮/暗各一档),让所有 Ela 原生控件 +// (选中/激活/标题栏强调) 与本项目 QSS 共用同一套蓝,消除"多种蓝打架"。在 eApp->init() 后调一次。 +void applyBrandAccent(); + // 当前 ElaTheme 是否暗色(供内联样式判断)。 bool isDarkTheme(); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 9903acf..12dcf77 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -155,7 +155,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 工作空间切换器(Fluent ElaToolButton;数据驱动,初始占位文本待 setWorkspaces 填充)。 wsBtn_ = new ElaToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); - wsBtn_->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), kWorkspaceIcon)); + setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活) wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); wsBtn_->setPopupMode(QToolButton::InstantPopup); @@ -171,7 +171,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 项目切换器(Fluent ElaToolButton;数据驱动)。 projBtn_ = new ElaToolButton(this); projBtn_->setObjectName(QStringLiteral("wsSwitcher")); - projBtn_->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), kWorkspaceIcon)); + setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon); // 中性主题色 projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); projBtn_->setPopupMode(QToolButton::InstantPopup); diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index 662bed6..84dc25d 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -3,7 +3,7 @@ #include #include "repo/RepoTypes.hpp" -class QToolButton; +class ElaToolButton; namespace geopro::app { @@ -28,8 +28,8 @@ signals: void logoutRequested(); // 头像菜单「退出登录」 private: - QToolButton* wsBtn_ = nullptr; - QToolButton* projBtn_ = nullptr; + ElaToolButton* wsBtn_ = nullptr; + ElaToolButton* projBtn_ = nullptr; }; } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 7b3dd98..11d02a5 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1032,6 +1032,7 @@ int main(int argc, char* argv[]) // ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化, // 两种壳都需要它(登录发生在选壳之前)。Ela 控件跟随 ElaTheme;标准控件仍由下面 QSS 接管。 eApp->init(); + geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝 // 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。 // 跟随 ElaTheme 初始模式(可能随系统为暗),使登录窗与标准控件明暗一致(review M2)。 diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index a550089..8afce22 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -41,6 +41,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { tree_ = new ElaTreeView(this); // Fluent 树视图(自绘展开/折叠指示,随主题) tree_->setHeaderHidden(true); + tree_->setIndentation(14); // 默认缩进偏大,收紧更紧凑协调 model_ = new QStandardItemModel(tree_); tree_->setModel(model_); lay->addWidget(tree_, 1); From ec428ccaca1dce574f330ddf96f23cc8636866c2 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 11:23:54 +0800 Subject: [PATCH 25/56] =?UTF-8?q?fix(ela):=20=E9=9D=A2=E6=9D=BF=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E6=8C=89=E9=92=AE(=E7=AD=9B=E9=80=89/=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0/=E6=B7=BB=E5=8A=A0/=E5=AF=BC=E5=87=BA/=E6=8A=98?= =?UTF-8?q?=E5=8F=A0)=E6=94=B9=20Ela=20=E5=AD=97=E4=BD=93=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前用 makeGlyph 位图 + 30×30 → 偏大且发糊。改为 ElaIconButton 字体图标(actionElaIcon 映射 Glyph→ElaIconType: Filter/Upload/Plus/Download/ChevronUp), 16px 图标 + 28×28 按钮, 与顶栏 帮助/通知/设置 一致: 清晰、随主题、尺寸协调 --- src/app/PanelHeader.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index e953b8c..1969565 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -2,6 +2,7 @@ #include "Theme.hpp" +#include #include #include @@ -58,12 +59,23 @@ QLabel* makeBadge(QWidget* parent) return badge; } -// 表头操作按钮(Fluent ElaIconButton;图标沿用项目 glyph 像素图)。 +// 表头操作 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,与顶栏图标按钮一致)。 QWidget* makeActionButton(QWidget* parent, const HeaderAction& a) { - auto* btn = new ElaIconButton( - makeGlyph(a.first, QColor("#5A6B85"), kActionIcon).pixmap(kActionIcon, kActionIcon), parent); - btn->setFixedSize(30, 30); // QPixmap 构造不设尺寸,显式固定防变形 + auto* btn = new ElaIconButton(actionElaIcon(a.first), 16, 28, 28, parent); btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(a.second + QStringLiteral("(占位)")); return btn; From 107fed818272da165a241fcfb2403fa35ce02e3c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 11:39:20 +0800 Subject: [PATCH 26/56] =?UTF-8?q?fix(ela):=20=E5=B7=A5=E5=85=B7=E6=9D=A1?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E6=80=81=20+=20=E5=88=87=E6=8D=A2=E5=99=A8?= =?UTF-8?q?=E7=AE=AD=E5=A4=B4=E6=94=B9=E7=94=A8=20QToolButton+=E4=B8=BB?= =?UTF-8?q?=E9=A2=98QSS(=E6=B8=85=E6=99=B0=E5=8F=AF=E6=8E=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ElaToolButton 硬限制: 选中态只画极淡 BasicHover(看不清)、展开箭头不可靠、且自绘无法被 QSS 覆盖。 故交互态强的这两类退回 QToolButton + applyThemedStyleSheet(用统一强调色): - 视图/详情工具条(2D/3D, 原数据/网格数据, 显示异常/电极/等值线): 选中 = 强调色文字 + 2px 强调色下划线, 明暗都清晰可辨 - 切换器: QToolButton + 文字'▾'(清晰, 不再是 ElaToolButton 那个发糙/消失的箭头) + 悬停底 其余(菜单/列表/树/表单/图标按钮)仍 Ela。强调色已全局统一为品牌蓝 --- src/app/TopBar.cpp | 26 ++++++++++++++++---------- src/app/TopBar.hpp | 6 +++--- src/app/main.cpp | 26 +++++++++++++++++++++----- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 12dcf77..8272249 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -138,6 +138,9 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" "#topDivider { color:#E1E6EE; }" "QToolButton::menu-indicator { image:none; }" + "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" + " font-size:%6px; font-weight:%4; }" + "#wsSwitcher: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; }" @@ -146,14 +149,15 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { .arg(type::kWeightBold) .arg(type::kLabel) .arg(type::kWeightSemibold) - .arg(type::kCaption)); + .arg(type::kCaption) + .arg(type::kTitle)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); lay->setSpacing(0); - // 工作空间切换器(Fluent ElaToolButton;数据驱动,初始占位文本待 setWorkspaces 填充)。 - wsBtn_ = new ElaToolButton(this); + // 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用文字"▾"保证清晰;数据驱动)。 + wsBtn_ = new QToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活) wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); @@ -168,8 +172,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(10); - // 项目切换器(Fluent ElaToolButton;数据驱动)。 - projBtn_ = new ElaToolButton(this); + // 项目切换器(QToolButton + 主题化 QSS;数据驱动)。 + projBtn_ = new QToolButton(this); projBtn_->setObjectName(QStringLiteral("wsSwitcher")); setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon); // 中性主题色 projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); @@ -233,7 +237,7 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - wsBtn_->setText(name); // 立即反馈(下拉箭头由 ElaToolButton 自绘) + wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈 emit workspaceSwitchRequested(id); }); } @@ -242,7 +246,8 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri none->setEnabled(false); } wsBtn_->setMenu(menu); - wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName); + wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) + + QStringLiteral(" ▾")); } void TopBar::setProjects(const std::vector& list, const QString& currentId, @@ -263,7 +268,7 @@ void TopBar::setProjects(const std::vector& list, const QS group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - projBtn_->setText(name); + projBtn_->setText(name + QStringLiteral(" ▾")); emit projectSwitchRequested(id); }); } @@ -277,11 +282,12 @@ void TopBar::setProjects(const std::vector& list, const QS QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); }); } projBtn_->setMenu(menu); - projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName); + projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) + + QStringLiteral(" ▾")); } void TopBar::setProjectButtonText(const QString& name) { - projBtn_->setText(name); + projBtn_->setText(name + QStringLiteral(" ▾")); } } // namespace geopro::app diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index 84dc25d..662bed6 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -3,7 +3,7 @@ #include #include "repo/RepoTypes.hpp" -class ElaToolButton; +class QToolButton; namespace geopro::app { @@ -28,8 +28,8 @@ signals: void logoutRequested(); // 头像菜单「退出登录」 private: - ElaToolButton* wsBtn_ = nullptr; - ElaToolButton* projBtn_ = nullptr; + QToolButton* wsBtn_ = nullptr; + QToolButton* projBtn_ = nullptr; }; } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 11d02a5..2705d0e 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -327,17 +328,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re centerLayout->setContentsMargins(0, 0, 0, 0); centerLayout->setSpacing(0); - // 工具条:「二维地图/三维视图」两个互斥可勾选按钮(Fluent ElaToolButton)。默认二维地图。 + // 分段工具条按钮样式(QToolButton + 主题化 QSS):选中=强调色文字 + 强调色下划线,明暗都清晰。 + // ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。 + const QString kBarBtnQss = + QStringLiteral( + "QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:#1F2A3D;" + " font-size:%1px; }" + "QToolButton:hover{ background:#EEF3FB; }" + "QToolButton:checked{ color:#2D6CB5; font-weight:%2;" + " border-bottom:2px solid #2D6CB5; }") + .arg(geopro::app::type::kBody) + .arg(geopro::app::type::kWeightSemibold); + + // 工具条:「二维地图/三维视图」两个互斥可勾选按钮。默认二维地图。 auto* viewToolBar = new QWidget(); auto* viewBarLay = new QHBoxLayout(viewToolBar); viewBarLay->setContentsMargins(8, 6, 8, 6); viewBarLay->setSpacing(6); auto* viewGroup = new QButtonGroup(viewToolBar); viewGroup->setExclusive(true); - auto* act2D = new ElaToolButton(viewToolBar); + auto* act2D = new QToolButton(viewToolBar); act2D->setText(QStringLiteral("二维地图")); act2D->setCheckable(true); - auto* act3D = new ElaToolButton(viewToolBar); + auto* act3D = new QToolButton(viewToolBar); act3D->setText(QStringLiteral("三维视图")); act3D->setCheckable(true); viewGroup->addButton(act2D); @@ -346,6 +359,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re viewBarLay->addWidget(act3D); viewBarLay->addStretch(); act2D->setChecked(true); // 默认二维地图 + geopro::app::applyThemedStyleSheet(viewToolBar, kBarBtnQss); centerLayout->addWidget(viewToolBar); centerLayout->addWidget(vtkWidget, 1); @@ -473,13 +487,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailLayout->setContentsMargins(0, 0, 0, 0); detailLayout->setSpacing(0); - // 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关(Fluent ElaToolButton)。 + // 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关。 + // QToolButton + 主题化 QSS(选中=强调色文字+下划线,明暗都清晰)。 auto* detailToolBar = new QWidget(); auto* detailBarLay = new QHBoxLayout(detailToolBar); detailBarLay->setContentsMargins(8, 6, 8, 6); detailBarLay->setSpacing(6); auto makeBarBtn = [detailToolBar](const QString& text, bool checkable) { - auto* b = new ElaToolButton(detailToolBar); + auto* b = new QToolButton(detailToolBar); b->setText(text); b->setCheckable(checkable); return b; @@ -509,6 +524,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailBarLay->addWidget(actShowElectrodes); detailBarLay->addWidget(actShowContour); detailBarLay->addStretch(); + geopro::app::applyThemedStyleSheet(detailToolBar, kBarBtnQss); detailLayout->addWidget(detailToolBar); detailLayout->addWidget(detailWidget, 1); From 464911dc57c359336f666d11509bcdbc31aefb16 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 11:51:02 +0800 Subject: [PATCH 27/56] =?UTF-8?q?fix(ela):=20=E9=A1=B5=E7=AD=BE=E4=B8=8E?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=9D=A1=E7=BB=9F=E4=B8=80=E4=B8=BA=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E5=A5=97=E5=88=87=E6=8D=A2=E6=A0=B7=E5=BC=8F(?= =?UTF-8?q?=E6=B6=88=E9=99=A4=E4=B8=89=E5=A5=97=E4=B8=8D=E4=B8=80=E8=87=B4?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PanelHeader 页签(数据/文件, 异常列表/对象属性)从 ElaToolButton 退回 QToolButton, headerQss 恢复 tabBtn 下划线规则: 选中 = 强调色文字 + 2px 强调色下划线 — 与视图/详情工具条完全一致。 至此全 UI 的切换控件(页签 + 分段工具条)是同一种视觉语言。 --- src/app/PanelHeader.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 1969565..832555c 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -30,8 +30,8 @@ constexpr int kTabIcon = 19; // Tab 图标 // 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。 // #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色), // 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。 -// 表头底/标题/徽标样式。Tab(ElaToolButton)与操作按钮(ElaIconButton)自绘 Fluent, -// 不再写它们的 QSS(选中态由 Ela 的 checked 高亮表达,而非自定义下划线)。 +// 表头底/标题/徽标 + 页签样式。页签选中态 = 强调色文字 + 2px 强调色下划线, +// 与视图/详情工具条完全一致(全 UI 切换控件统一这一套)。操作按钮(ElaIconButton)自绘 Fluent。 QString headerQss() { return QStringLiteral( @@ -40,12 +40,18 @@ QString headerQss() "#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;" " 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; }") + " padding:1px 7px; font-size:%2px; font-weight:%3; }" + "QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;" + " padding:8px 6px; font-size:%6px; }" + "QToolButton#tabBtn:hover { color:#1F2A3D; }" + "QToolButton#tabBtn:checked { color:#2D6CB5; font-weight:%3;" + " border-bottom:2px solid #2D6CB5; }") .arg(type::kTitle) .arg(type::kCaption) .arg(type::kWeightSemibold) .arg(QString::fromUtf8(semantic::kWarningFill)) - .arg(QString::fromUtf8(semantic::kWarning)); + .arg(QString::fromUtf8(semantic::kWarning)) + .arg(type::kBody); } // 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。 @@ -135,7 +141,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("tabBtn")); btn->setText(t.title); setThemedGlyph(btn, t.icon, kTabIcon); // 随主题着色 From cfd39e5be80522ed629986de7c098929f3590f09 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 12:00:20 +0800 Subject: [PATCH 28/56] =?UTF-8?q?fix(ela):=20=E5=AF=B9=E8=B1=A1=E6=A0=91?= =?UTF-8?q?=E9=80=80=E5=9B=9E=E6=A0=87=E5=87=86=20QTreeWidget(=E5=A4=8D?= =?UTF-8?q?=E9=80=89=E6=A1=86=E6=B8=85=E6=99=B0)=20+=20=E8=A1=8C=E5=86=85?= =?UTF-8?q?=E8=BE=B9=E8=B7=9D=E6=94=B6=E7=B4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ElaTreeView 是 ElaWidgetTools 自绘控件(非 Qt 原生), light 下复选框对比度不足、选中渲染有局限。 - 对象树 ElaTreeView+QStandardItemModel → 标准 QTreeWidget+QTreeWidgetItem: 复选框/展开箭头由 Fusion 原生绘制, 明暗都清晰; 行为(TM 勾选/点击)不变 - 全局 QTreeView::item padding 8px→4px: 行间距过大修正 --- src/app/Theme.cpp | 2 +- src/app/panels/ObjectTreePanel.cpp | 48 +++++++++++++----------------- src/app/panels/ObjectTreePanel.hpp | 6 ++-- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index f059bae..6336338 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -74,7 +74,7 @@ QTreeWidget, QListWidget, QTreeView, QListView { outline: none; } QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item { - padding: 8px 8px; + padding: 4px 8px; border-radius: 6px; margin: 1px 4px; } diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 8afce22..5263fd2 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -1,15 +1,11 @@ #include "panels/ObjectTreePanel.hpp" #include -#include #include -#include -#include -#include +#include +#include #include -#include - #include "Theme.hpp" #include "dto/NavDto.hpp" @@ -19,16 +15,15 @@ namespace { // TM 节点把 tmObjectId 存在该角色;GS/项目根节点为空。 constexpr int kRoleTmId = Qt::UserRole + 2; -void addNodes(QStandardItem* parent, const std::vector& nodes) { +void addNodes(QTreeWidgetItem* parent, const std::vector& nodes) { for (const auto& n : nodes) { - auto* item = new QStandardItem(QString::fromStdString(n.node.name)); - item->setEditable(false); + auto* item = new QTreeWidgetItem(parent); + item->setText(0, QString::fromStdString(n.node.name)); if (n.isTm) { - item->setData(QString::fromStdString(n.node.id), kRoleTmId); - item->setCheckable(true); - item->setCheckState(Qt::Unchecked); // 真实数据渲染下一轮接入,默认不勾 + item->setData(0, kRoleTmId, QString::fromStdString(n.node.id)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Unchecked); // 真实数据渲染下一轮接入,默认不勾 } - parent->appendRow(item); addNodes(item, n.children); } } @@ -39,11 +34,10 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { lay->setContentsMargins(0, 0, 0, 0); lay->setSpacing(0); - tree_ = new ElaTreeView(this); // Fluent 树视图(自绘展开/折叠指示,随主题) + // Qt 原生标准树:复选框/展开箭头由 Fusion 绘制(明暗都清晰);配色取自全局主题 QSS。 + tree_ = new QTreeWidget(this); tree_->setHeaderHidden(true); - tree_->setIndentation(14); // 默认缩进偏大,收紧更紧凑协调 - model_ = new QStandardItemModel(tree_); - tree_->setModel(model_); + tree_->setIndentation(14); // 收紧缩进 lay->addWidget(tree_, 1); hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); @@ -52,22 +46,20 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { hint_->setVisible(false); lay->addWidget(hint_); - // 单击 TM → tmClicked(按 index 取 tmId)。 - QObject::connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& idx) { - const QString tmId = idx.data(kRoleTmId).toString(); + QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) { + const QString tmId = item->data(0, kRoleTmId).toString(); if (!tmId.isEmpty()) emit tmClicked(tmId); }); - // 勾选变化 → tmCheckToggled。 - QObject::connect(model_, &QStandardItemModel::itemChanged, this, [this](QStandardItem* item) { - const QString tmId = item->data(kRoleTmId).toString(); - if (!tmId.isEmpty()) emit tmCheckToggled(tmId, item->checkState() == Qt::Checked); + QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item, int) { + const QString tmId = item->data(0, kRoleTmId).toString(); + if (!tmId.isEmpty()) emit tmCheckToggled(tmId, item->checkState(0) == Qt::Checked); }); } void ObjectTreePanel::setStructure(const QString& projectName, const std::vector& nodes) { - const QSignalBlocker block(model_); // 重建触发 itemChanged,先屏蔽 - model_->clear(); + const QSignalBlocker block(tree_); // 重建触发 itemChanged,先屏蔽 + tree_->clear(); const auto roots = data::dto::buildStructTree(nodes); if (roots.empty()) { showMessage(projectName.isEmpty() ? QStringLiteral("(暂无项目)") @@ -76,12 +68,12 @@ void ObjectTreePanel::setStructure(const QString& projectName, } hint_->setVisible(false); tree_->setVisible(true); - addNodes(model_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染 + addNodes(tree_->invisibleRootItem(), roots); // 结构已含项目根节点,直接渲染 tree_->expandAll(); } void ObjectTreePanel::showMessage(const QString& message) { - model_->clear(); + tree_->clear(); tree_->setVisible(false); hint_->setText(message); hint_->setVisible(true); diff --git a/src/app/panels/ObjectTreePanel.hpp b/src/app/panels/ObjectTreePanel.hpp index 33cfff4..1ba37e8 100644 --- a/src/app/panels/ObjectTreePanel.hpp +++ b/src/app/panels/ObjectTreePanel.hpp @@ -3,8 +3,7 @@ #include #include "repo/RepoTypes.hpp" -class QTreeView; -class QStandardItemModel; +class QTreeWidget; class QLabel; namespace geopro::app { @@ -25,8 +24,7 @@ signals: void tmCheckToggled(const QString& tmObjectId, bool checked); private: - QTreeView* tree_ = nullptr; // ElaTreeView(继承 QTreeView) - QStandardItemModel* model_ = nullptr; // 标准 model(Qt 自带) + QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控) QLabel* hint_ = nullptr; }; From e3a1b18efad4be9a8b789efad348657e916b4bec Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 12:02:43 +0800 Subject: [PATCH 29/56] =?UTF-8?q?chore(ela):=20=E5=8E=BB=E6=8E=89=20GEOPRO?= =?UTF-8?q?=5FUI=5FSHELL=20=E5=85=9C=E5=BA=95=E5=BC=80=E5=85=B3=20+=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BC=80=E6=BA=90=20NOTICE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main: 删除 env 选壳分支与经典 QMainWindow 回退, ElaWindow 成为唯一外壳(迁移已稳定) - NOTICE.md: 列出第三方组件与许可证(Qt LGPL / VTK BSD / ADS LGPL / ElaWidgetTools MIT / QtKeychain BSD) 及合规要点、Ela 版本钉定说明 --- NOTICE.md | 28 ++++++++++++++++++++ src/app/main.cpp | 68 +++++++++++++++++++----------------------------- 2 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 NOTICE.md diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..3b12ba3 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,28 @@ +# 第三方组件与许可证 (Third-Party Notices) + +本项目(geopro / Geopro 3.0 桌面端)使用了以下第三方开源组件。各组件版权归其各自作者所有, +按其各自许可证条款分发。 + +| 组件 | 用途 | 许可证 | 来源 | +|---|---|---|---| +| **Qt 6** (6.11.x) | GUI 框架(Widgets/Gui/Core/Network/Svg/OpenGL 等) | LGPL-3.0(亦提供商业许可) | https://www.qt.io | +| **VTK** (9.x) | 三维/二维可视化渲染 | BSD-3-Clause | https://vtk.org | +| **Qt-Advanced-Docking-System** (4.3.1) | 停靠面板布局 | LGPL-2.1 | https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System | +| **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 | + +## 许可证要点 + +- **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/src/app/main.cpp b/src/app/main.cpp index 2705d0e..7bd9163 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1099,49 +1099,35 @@ int main(int argc, char* argv[]) geopro::data::ApiProjectRepository projectRepo(api); geopro::controller::WorkbenchNavController nav(projectRepo); - // ── 外壳选择(P1 迁移,可回退):GEOPRO_UI_SHELL=ela → Fluent ElaWindow 壳; - // 未设/其它 → 经典 QMainWindow 壳。两者都复用同一 buildWorkbench(QMainWindow&)。 - // Ela 壳策略:ElaWindow 用 setCentralCustomWidget 包裹一个承载工作台的内层 QMainWindow - // (buildWorkbench 依赖 QMainWindow 的 setCentralWidget/setMenuWidget/statusBar, - // ElaWindow 自身将其设为私有,故用内层 QMainWindow 承接,零改 buildWorkbench)。 + // ── 外壳:Fluent ElaWindow(唯一路径)。ElaWindow 用 addPageNode 包裹一个承载工作台的内层 + // QMainWindow(buildWorkbench 依赖 QMainWindow 的 setCentralWidget/setMenuWidget/statusBar, + // ElaWindow 自身将其设为私有,故用内层 QMainWindow 承接,零改 buildWorkbench)。 const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"); - QWidget* topLevel = nullptr; - if (qEnvironmentVariable("GEOPRO_UI_SHELL") == QLatin1String("ela")) { - 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 抖动(review H3) - buildWorkbench(*inner, repo, projectRepo, nav); - // 用 addPageNode 把工作台作为唯一页面放进中心页栈(填满到底边)。 - // 注意:不能用 setCentralCustomWidget——它把控件插到页栈容器“之上”,空页栈仍占底部, - // 导致状态栏不贴底边(见 ElaCentralStackedWidget::setCustomWidget 的 insertWidget(0,...))。 - ela->addPageNode(kTitle, inner); + 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); - // ── P2 主题桥:ElaTheme 明/暗切换 → 同步全局 QSS+调色板(覆盖所有标准控件与 ADS)。 - // Ela 自家控件由 ElaTheme 自动换肤;这里补齐非 Ela 面。也确定了主题最终态(解 review C2)。 - QObject::connect(eTheme, &ElaTheme::themeModeChanged, ela, [&app](ElaThemeType::ThemeMode m) { - geopro::app::applyThemeMode(app, m == ElaThemeType::Dark); - }); - geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // 初始对齐 - // 主题切换快捷键 Ctrl+Shift+T(正式切换入口待 P3 TopBar 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); - }); - topLevel = ela; - } else { - auto* window = new QMainWindow; - window->setWindowTitle(kTitle); - window->resize(1280, 800); - window->setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸 - buildWorkbench(*window, repo, projectRepo, nav); - topLevel = window; - } - topLevel->show(); + // 主题桥:ElaTheme 明/暗切换 → 同步全局 QSS+调色板(覆盖所有标准控件与 ADS)。 + QObject::connect(eTheme, &ElaTheme::themeModeChanged, ela, [&app](ElaThemeType::ThemeMode m) { + geopro::app::applyThemeMode(app, m == ElaThemeType::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(); nav.start(); // 进入工作台后拉真实 空间/项目/结构 From 934e25be54681c92062b76b96806b621b029da2f Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 12:21:01 +0800 Subject: [PATCH 30/56] =?UTF-8?q?fix(tree):=20=E8=87=AA=E7=BB=98=E6=B8=85?= =?UTF-8?q?=E6=99=B0=E5=A4=8D=E9=80=89=E6=A1=86(=E6=98=8E=E6=9A=97?= =?UTF-8?q?=E9=83=BD=E5=8F=AF=E8=A7=81)=20+=20=E9=80=89=E4=B8=AD=E6=95=B4?= =?UTF-8?q?=E8=A1=8C=E8=BF=9E=E7=BB=AD(=E5=8E=BB=E7=A2=8E=E7=89=87?= =?UTF-8?q?=E6=A1=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 多余选中框: 全局 QTreeView::item 去掉 border-radius+margin → 选中是整行连续一条, 不再浮动碎块 - light 复选框看不清: Fusion 原生复选框浅底边框过淡。writeCheckboxIcon 自绘 PNG (未选=明显边框空心框, 选中=强调色底+白勾), 明暗各一套, 经 QTreeView::indicator QSS 引用, 主题切换重绘 → 明暗都清晰 --- src/app/Glyphs.cpp | 41 ++++++++++++++++++++++++++++++ src/app/Glyphs.hpp | 5 ++++ src/app/Theme.cpp | 2 -- src/app/panels/ObjectTreePanel.cpp | 25 ++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/app/Glyphs.cpp b/src/app/Glyphs.cpp index ff901a5..edd4a20 100644 --- a/src/app/Glyphs.cpp +++ b/src/app/Glyphs.cpp @@ -158,6 +158,47 @@ QString writeChevronIcon(bool open, const QColor& color) return path; } +QString writeCheckboxIcon(bool checked, const QColor& border, const QColor& fill, const QColor& check, + const QString& tag) +{ + constexpr int px = 16; + constexpr int kScale = 3; + QPixmap pm(px * kScale, px * kScale); + pm.fill(Qt::transparent); + + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing, true); + const double s = px * kScale; + const double inset = s * 0.14; + const QRectF box(inset, inset, s - 2 * inset, s - 2 * inset); + const double r = s * 0.14; + + if (checked) { + p.setPen(Qt::NoPen); + p.setBrush(fill); + p.drawRoundedRect(box, r, r); + QPen cpen(check, s * 0.12); + cpen.setCapStyle(Qt::RoundCap); + cpen.setJoinStyle(Qt::RoundJoin); + p.setPen(cpen); + p.drawLine(QPointF(s * 0.30, s * 0.52), QPointF(s * 0.44, s * 0.66)); + p.drawLine(QPointF(s * 0.44, s * 0.66), QPointF(s * 0.70, s * 0.34)); + } else { + QPen bpen(border, s * 0.085); + p.setPen(bpen); + p.setBrush(fill); + p.drawRoundedRect(box, r, r); + } + p.end(); + + const QString path = QDir(QDir::tempPath()) + .filePath(QStringLiteral("geopro_chk_%1_%2.png") + .arg(tag, checked ? QStringLiteral("on") + : QStringLiteral("off"))); + pm.save(path, "PNG"); + return path; +} + namespace { // 当前主题下的 chrome 图标色:取主题主文本色(暗=浅、亮=深),保证两种模式都清晰。 QColor themedIconColor() diff --git a/src/app/Glyphs.hpp b/src/app/Glyphs.hpp index 90eeaba..303208d 100644 --- a/src/app/Glyphs.hpp +++ b/src/app/Glyphs.hpp @@ -46,4 +46,9 @@ void setThemedGlyph(QAbstractButton* button, Glyph type, int px); // 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。 QString writeChevronIcon(bool open, const QColor& color); +// 生成清晰复选框 PNG(供 QTreeView::indicator 的 QSS `image:url(...)`): +// 未选=明显边框空心方框;选中=填充色方框 + 白色对勾。明暗各传一套色,tag 区分文件名。 +QString writeCheckboxIcon(bool checked, const QColor& border, const QColor& fill, const QColor& check, + const QString& tag); + } // namespace geopro::app diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 6336338..4dc6774 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -75,8 +75,6 @@ QTreeWidget, QListWidget, QTreeView, QListView { } QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item { padding: 4px 8px; - border-radius: 6px; - margin: 1px 4px; } QTreeWidget::item:hover, QListWidget::item:hover, QTreeView::item:hover, QListView::item:hover { diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 5263fd2..3bafc72 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -6,6 +6,10 @@ #include #include +#include +#include + +#include "Glyphs.hpp" #include "Theme.hpp" #include "dto/NavDto.hpp" @@ -38,6 +42,27 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { tree_ = new QTreeWidget(this); tree_->setHeaderHidden(true); tree_->setIndentation(14); // 收紧缩进 + + // 清晰复选框:自绘 PNG(未选=明显边框空心框,选中=强调色底+白勾),明暗各一套、切换重绘。 + // 规避 Fusion 原生复选框在浅底下边框过淡、看不清的问题。 + auto applyCheckboxStyle = [this]() { + 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 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); + tree_->setStyleSheet(QStringLiteral("QTreeView::indicator{ width:16px; height:16px; }" + "QTreeView::indicator:unchecked{ image:url(%1); }" + "QTreeView::indicator:checked{ image:url(%2); }") + .arg(off, on)); + }; + applyCheckboxStyle(); + QObject::connect(eTheme, &ElaTheme::themeModeChanged, tree_, + [applyCheckboxStyle](ElaThemeType::ThemeMode) { applyCheckboxStyle(); }); + lay->addWidget(tree_, 1); hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); From 66cf432a98f15b698a7e1a4de4c809faad991994 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 12:26:00 +0800 Subject: [PATCH 31/56] =?UTF-8?q?fix(theme):=20=E9=80=89=E4=B8=AD=E5=BA=95?= =?UTF-8?q?=E8=89=B2=E7=BB=9F=E4=B8=80=E5=8A=A0=E5=BC=BA(=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=A0=91+=E6=89=80=E6=9C=89=E5=88=97=E8=A1=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: ElaListView 的 BasicSelectedAlpha 默认是半透明灰、对比度弱; 树 QSS 选中也偏淡。 设计统一的强调蓝选中底(明 #C2D9F2 / 暗 #33527A, 强对比): - applyBrandAccent: 设 BasicSelectedAlpha/BasicSelectedHoverAlpha 为该实色 → 所有 Ela 列表 选中底变清晰(并保留其 3px 强调色左竖条) - 对象树: 本地 QSS item:selected 设同款蓝 + 适配文字色, :!active 防失焦变淡 --- src/app/Theme.cpp | 7 +++++++ src/app/panels/ObjectTreePanel.cpp | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 4dc6774..c287dce 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -464,6 +464,13 @@ void applyBrandAccent() 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)); } bool isDarkTheme() diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 3bafc72..5b520ae 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -54,10 +54,15 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { 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); + // 选中底色与 Ela 列表(BasicSelectedAlpha)保持一致:明=浅蓝、暗=中深蓝;:!active 防失焦变淡。 + const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2"); + const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F"); tree_->setStyleSheet(QStringLiteral("QTreeView::indicator{ width:16px; height:16px; }" "QTreeView::indicator:unchecked{ image:url(%1); }" - "QTreeView::indicator:checked{ image:url(%2); }") - .arg(off, on)); + "QTreeView::indicator:checked{ image:url(%2); }" + "QTreeView::item:selected{ background:%3; color:%4; }" + "QTreeView::item:selected:!active{ background:%3; color:%4; }") + .arg(off, on, selBg, selFg)); }; applyCheckboxStyle(); QObject::connect(eTheme, &ElaTheme::themeModeChanged, tree_, From 52bdf054a64e3e27fc9a91f75aa0d8221d2f8cd6 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 12:39:23 +0800 Subject: [PATCH 32/56] =?UTF-8?q?fix(list):=20=E6=95=B0=E6=8D=AE/=E6=96=87?= =?UTF-8?q?=E4=BB=B6/=E5=BC=82=E5=B8=B8=E5=88=97=E8=A1=A8=E9=80=80?= =?UTF-8?q?=E5=9B=9E=E6=A0=87=E5=87=86=20QListWidget=20+=20=E5=86=99?= =?UTF-8?q?=E6=AD=BB=E5=BC=BA=E9=80=89=E4=B8=AD=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: ElaListView 的选中底走 BasicSelectedAlpha, setThemeColor 改它对 ElaListView 不生效(自绘控件坑), 选中色无法变强。与对象树同理, 退回 Qt 原生 QListWidget: - 3 列表 ElaListView+QStandardItemModel → QListWidget+QListWidgetItem(populate/加载更多/点击/勾选 全回退) - applyListSelection: 本地 QSS 写死强调蓝选中(明 #C2D9F2 / 暗 #33527A + 适配文字, :!active 防失焦淡), 与对象树选中色完全一致, 100% 可控、明暗都清晰 - 行为(加载更多/数据集点击/异常勾选显隐)保持 --- src/app/main.cpp | 107 +++++++++++++++------------- src/app/panels/AnomalyListPanel.cpp | 18 +++-- src/app/panels/AnomalyListPanel.hpp | 4 +- src/app/panels/DatasetListPanel.cpp | 37 ++++------ src/app/panels/DatasetListPanel.hpp | 10 ++- 5 files changed, 88 insertions(+), 88 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 7bd9163..6e6a92d 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -36,9 +36,8 @@ #include #include #include -#include -#include -#include +#include +#include #include #include #include @@ -68,7 +67,6 @@ #include #include #include -#include #include #include #include @@ -544,15 +542,30 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re {{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}})); auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); + // 列表选中色:写死的强调蓝(明 #C2D9F2 / 暗 #33527A)+ 适配文字,:!active 防失焦变淡; + // 与对象树选中色一致。本地 QSS 覆盖全局弱选中色,随主题重设。 + auto applyListSelection = [](QListWidget* lw) { + auto styleIt = [lw]() { + const bool dark = geopro::app::isDarkTheme(); + const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2"); + const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F"); + lw->setStyleSheet(QStringLiteral("QListWidget::item:selected{ background:%1; color:%2; }" + "QListWidget::item:selected:!active{ background:%1;" + " color:%2; }") + .arg(selBg, selFg)); + }; + styleIt(); + QObject::connect(eTheme, &ElaTheme::themeModeChanged, lw, + [styleIt](ElaThemeType::ThemeMode) { styleIt(); }); + }; + // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 auto* datasetTabs = new QTabWidget(); - auto* datasetList = new ElaListView(); // Fluent 列表(自绘 hover/选中, 随主题) - auto* datasetModel = new QStandardItemModel(datasetList); - datasetList->setModel(datasetModel); + auto* datasetList = new QListWidget(); + applyListSelection(datasetList); datasetTabs->addTab(datasetList, QStringLiteral("数据")); - auto* fileList = new ElaListView(); - auto* fileModel = new QStandardItemModel(fileList); - fileList->setModel(fileModel); + auto* fileList = new QListWidget(); + applyListSelection(fileList); datasetTabs->addTab(fileList, QStringLiteral("文件")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetBox = wrapWithHeader( @@ -565,9 +578,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea); // 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 - auto* anomalyList = new ElaListView(); - auto* anomalyModel = new QStandardItemModel(anomalyList); - anomalyList->setModel(anomalyModel); + auto* anomalyList = new QListWidget(); + applyListSelection(anomalyList); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); objAttrLabel->setWordWrap(true); objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); @@ -727,7 +739,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; // 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。 - auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyModel, hiddenAnoms, + auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms, anomalyBadge](const QString& dsId, const QString& name) { if (dsId.isEmpty()) return; *currentDsId = dsId; @@ -736,8 +748,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const auto anomalies = repo.loadAnomalies(dsId.toStdString()); hiddenAnoms->clear(); { - const QSignalBlocker block(anomalyModel); // 重填触发 itemChanged,先屏蔽 - geopro::app::populateAnomalyList(anomalyModel, anomalies); + const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽 + geopro::app::populateAnomalyList(anomalyList, anomalies); } // 异常列表 Tab 数量徽标。 if (anomalyBadge) { @@ -763,13 +775,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── // 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。 - QObject::connect(datasetList, &QAbstractItemView::clicked, datasetList, - [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](const QModelIndex& idx) { - if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) { + QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, + [propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) { + if (item->data(geopro::app::kDsLoadMoreRole).toBool()) { nav.loadMoreData(); return; } - const QString name = idx.data(Qt::DisplayRole).toString().section('\n', 0, 0); + const QString name = + item->data(Qt::DisplayRole).toString().section('\n', 0, 0); detailRendererPtr->RemoveAllViewProps(); detailRenderWindowPtr->Render(); propLabel->setText(QStringLiteral( @@ -777,8 +790,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); // ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ── - QObject::connect(anomalyModel, &QStandardItemModel::itemChanged, anomalyList, - [hiddenAnoms, rebuildDetail](QStandardItem* item) { + QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList, + [hiddenAnoms, rebuildDetail](QListWidgetItem* item) { const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt(); if (item->checkState() == Qt::Checked) hiddenAnoms->erase(idx); @@ -891,20 +904,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ── 控制器 ↔ UI 信号接线(导航壳)────────────────────────────────────── // "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。 - auto removeLoadMore = [](QStandardItemModel* mdl) { - const int n = mdl->rowCount(); - if (n > 0 && mdl->item(n - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) - mdl->removeRow(n - 1); + auto removeLoadMore = [](QListWidget* lw) { + if (lw->count() > 0 && + lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool()) + delete lw->takeItem(lw->count() - 1); }; - auto addLoadMore = [](QStandardItemModel* mdl, int total) { - const int loaded = mdl->rowCount(); + auto addLoadMore = [](QListWidget* lw, int total) { + const int loaded = lw->count(); if (loaded < total) { - auto* it = new QStandardItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total)); - it->setData(true, geopro::app::kDsLoadMoreRole); - it->setTextAlignment(Qt::AlignCenter); - it->setForeground(QColor("#2D6CB5")); - it->setEditable(false); - mdl->appendRow(it); + auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw); + m->setData(geopro::app::kDsLoadMoreRole, true); + m->setTextAlignment(Qt::AlignCenter); + m->setForeground(QColor("#2D6CB5")); } return loaded; }; @@ -943,42 +954,42 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re topBar->setProjects(list, cur, total > static_cast(list.size())); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree, - [objectTree, datasetModel, fileModel, datasetTitle, datasetTabs]( + [objectTree, datasetList, fileList, datasetTitle, datasetTabs]( const QString& projectName, const std::vector& nodes) { objectTree->setStructure(projectName, nodes); - datasetModel->clear(); - fileModel->clear(); + datasetList->clear(); + fileList->clear(); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); datasetTabs->setTabText(0, QStringLiteral("数据")); datasetTabs->setTabText(1, QStringLiteral("文件")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList, - [removeLoadMore, addLoadMore, datasetModel, datasetTitle, datasetTabs]( + [removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs]( const QString&, const std::vector& rows, int total, bool append) { - removeLoadMore(datasetModel); - geopro::app::populateDatasetList(datasetModel, rows, append); - const int loaded = addLoadMore(datasetModel, total); + removeLoadMore(datasetList); + geopro::app::populateDatasetList(datasetList, rows, append); + const int loaded = addLoadMore(datasetList, total); if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); datasetTabs->setTabText( 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) : QStringLiteral("数据")); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList, - [removeLoadMore, addLoadMore, fileModel, datasetTabs]( + [removeLoadMore, addLoadMore, fileList, datasetTabs]( const QString&, const std::vector& rows, int total, bool append) { - removeLoadMore(fileModel); - geopro::app::populateFileList(fileModel, rows, append); - const int loaded = addLoadMore(fileModel, total); + removeLoadMore(fileList); + geopro::app::populateFileList(fileList, rows, append); + const int loaded = addLoadMore(fileList, total); datasetTabs->setTabText( 1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total) : QStringLiteral("文件")); }); - QObject::connect(fileList, &QAbstractItemView::clicked, fileList, - [&nav](const QModelIndex& idx) { - if (idx.data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); + QObject::connect(fileList, &QListWidget::itemClicked, fileList, + [&nav](QListWidgetItem* item) { + if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles(); }); QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree, [objectTree, &window](const QString& stage, const QString& msg) { diff --git a/src/app/panels/AnomalyListPanel.cpp b/src/app/panels/AnomalyListPanel.cpp index 198a83d..6f5fe2d 100644 --- a/src/app/panels/AnomalyListPanel.cpp +++ b/src/app/panels/AnomalyListPanel.cpp @@ -5,9 +5,9 @@ #include #include +#include +#include #include -#include -#include #include #include "model/ColorScale.hpp" @@ -54,10 +54,10 @@ QPixmap swatch(const std::string& colorStr) } // namespace -void populateAnomalyList(QStandardItemModel* model, const std::vector& anomalies) +void populateAnomalyList(QListWidget* list, const std::vector& anomalies) { - if (!model) return; - model->clear(); + if (!list) return; + list->clear(); for (std::size_t i = 0; i < anomalies.size(); ++i) { const auto& a = anomalies[i]; const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name); @@ -66,12 +66,10 @@ void populateAnomalyList(QStandardItemModel* model, const std::vectorsetEditable(false); - item->setData(static_cast(i), kAnomalyIndexRole); - item->setCheckable(true); + auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list); + item->setData(kAnomalyIndexRole, static_cast(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Checked); // 默认显示 - model->appendRow(item); } } diff --git a/src/app/panels/AnomalyListPanel.hpp b/src/app/panels/AnomalyListPanel.hpp index 326d488..e5566ed 100644 --- a/src/app/panels/AnomalyListPanel.hpp +++ b/src/app/panels/AnomalyListPanel.hpp @@ -3,7 +3,7 @@ #include "model/Anomaly.hpp" -class QStandardItemModel; +class QListWidget; namespace geopro::app { @@ -14,6 +14,6 @@ constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole // 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。 // 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。 // 清空旧条目后重填。 -void populateAnomalyList(QStandardItemModel* model, const std::vector& anomalies); +void populateAnomalyList(QListWidget* list, const std::vector& anomalies); } // namespace geopro::app diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 5022835..2ca911f 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -1,8 +1,8 @@ #include "panels/DatasetListPanel.hpp" #include -#include -#include +#include +#include #include namespace geopro::app { @@ -16,34 +16,29 @@ QString humanSize(long long b) { } } // namespace -void populateDatasetList(QStandardItemModel* model, const std::vector& rows, - bool append) { - if (!model) return; - if (!append) model->clear(); +void populateDatasetList(QListWidget* list, const std::vector& rows, bool append) { + if (!list) return; + if (!append) list->clear(); for (const auto& d : rows) { QString text = QString::fromStdString(d.dsName); QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 if (!d.typeName.empty()) sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); - auto* item = new QStandardItem(text); - item->setEditable(false); - item->setData(QString::fromStdString(d.id), kDsIdRole); - item->setData(QString::fromStdString(d.ddCode), kDsDdTypeRole); - model->appendRow(item); + auto* item = new QListWidgetItem(text, list); + item->setData(kDsIdRole, QString::fromStdString(d.id)); + item->setData(kDsDdTypeRole, QString::fromStdString(d.ddCode)); } } -void populateFileList(QStandardItemModel* model, const std::vector& rows, - bool append) { - if (!model) return; - if (!append) model->clear(); +void populateFileList(QListWidget* list, const std::vector& rows, bool append) { + if (!list) return; + if (!append) list->clear(); if (!append && rows.empty()) { - auto* hint = new QStandardItem(QStringLiteral("(暂无文件)")); + auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); hint->setFlags(Qt::NoItemFlags); hint->setForeground(QColor("#9AA6B6")); hint->setTextAlignment(Qt::AlignCenter); - model->appendRow(hint); return; } for (const auto& d : rows) { @@ -52,11 +47,9 @@ void populateFileList(QStandardItemModel* model, const std::vectorsetEditable(false); - item->setData(QString::fromStdString(d.id), kDsIdRole); - item->setData(QString::fromStdString(d.fileUrl), kDsFileUrlRole); - model->appendRow(item); + auto* item = new QListWidgetItem(text, list); + item->setData(kDsIdRole, QString::fromStdString(d.id)); + item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl)); } } diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index f512c2c..0356760 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -3,7 +3,7 @@ #include "repo/RepoTypes.hpp" -class QStandardItemModel; +class QListWidget; namespace geopro::app { @@ -13,11 +13,9 @@ constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1 constexpr int kDsFileUrlRole = 0x0102; // Qt::UserRole + 2(文件下载 url,备用) constexpr int kDsLoadMoreRole = 0x0103; // 标记"加载更多"行 -// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。填入标准 model。 -void populateDatasetList(QStandardItemModel* model, const std::vector& rows, - bool append); +// 数据页签:每条 = dsName +(类型名);UserRole 存 dsId、+1 存 ddCode。 +void populateDatasetList(QListWidget* list, const std::vector& rows, bool append); // 文件页签:每条 = 文件名 +(可读大小);UserRole 存 dsId、+2 存文件 url。空时显示占位。 -void populateFileList(QStandardItemModel* model, const std::vector& rows, - bool append); +void populateFileList(QListWidget* list, const std::vector& rows, bool append); } // namespace geopro::app From a13b58e09fd9d3df4970f3faf54af9842feee2a4 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 14:03:16 +0800 Subject: [PATCH 33/56] =?UTF-8?q?feat(settings):=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86(=E5=A4=96=E8=A7=82/=E5=85=B3?= =?UTF-8?q?=E4=BA=8E)=20+=20=E4=B8=BB=E9=A2=98=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=20+=20=E7=95=8C=E9=9D=A2=E5=AD=97=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主题持久化(QSettings ui/themeMode: system/light/dark): 启动时 applyPersistedThemeMode 在弹登录窗前 应用 → 登录页与主页明暗统一(修登录页一直 dark); 设置里改主题热切, Ctrl+Shift+T 仍可用 - 界面字号(ui/fontScale: 90/100/115/130): applyPersistedFontScale 设 qApp 基准字体; scaledPx() 让 内联 QSS 的 chrome(顶栏/面板表头/工具条/登录/浮层)字号也随之缩放; 字号改动重启后整体统一生效 - SettingsDialog: 左分类(外观/关于)+右页; 外观=主题下拉+字号下拉(+立即重启); 关于=版本+第三方许可 - 顶栏齿轮 → settingsRequested → 打开设置 --- src/app/CMakeLists.txt | 3 +- src/app/PanelHeader.cpp | 6 +- src/app/SettingsDialog.cpp | 156 ++++++++++++++++++++++++++++++++++ src/app/SettingsDialog.hpp | 15 ++++ src/app/Theme.cpp | 55 ++++++++++++ src/app/Theme.hpp | 13 +++ src/app/TopBar.cpp | 14 +-- src/app/TopBar.hpp | 1 + src/app/login/LoginWindow.cpp | 8 +- src/app/main.cpp | 21 +++-- 10 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 src/app/SettingsDialog.cpp create mode 100644 src/app/SettingsDialog.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 11fc9a0..d8193ea 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -25,7 +25,8 @@ add_executable(geopro_desktop WIN32 panels/DatasetListPanel.cpp panels/ObjectTreePanel.cpp CentralScene.cpp - ProjectListDialog.cpp) + ProjectListDialog.cpp + SettingsDialog.cpp) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) # QtKeychain 经 FetchContent 接入,头文件不随 target 传播,显式加源/构建目录(含生成的 export 头)。 diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 832555c..aadb7a7 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -46,12 +46,12 @@ QString headerQss() "QToolButton#tabBtn:hover { color:#1F2A3D; }" "QToolButton#tabBtn:checked { color:#2D6CB5; font-weight:%3;" " border-bottom:2px solid #2D6CB5; }") - .arg(type::kTitle) - .arg(type::kCaption) + .arg(scaledPx(type::kTitle)) + .arg(scaledPx(type::kCaption)) .arg(type::kWeightSemibold) .arg(QString::fromUtf8(semantic::kWarningFill)) .arg(QString::fromUtf8(semantic::kWarning)) - .arg(type::kBody); + .arg(scaledPx(type::kBody)); } // 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。 diff --git a/src/app/SettingsDialog.cpp b/src/app/SettingsDialog.cpp new file mode 100644 index 0000000..5a695e6 --- /dev/null +++ b/src/app/SettingsDialog.cpp @@ -0,0 +1,156 @@ +#include "SettingsDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Theme.hpp" + +namespace geopro::app { + +namespace { + +// 「标签 + 控件」一行(标签定宽左对齐,控件右随)。 +QWidget* makeRow(const QString& label, QWidget* control) { + auto* row = new QWidget(); + auto* lay = new QHBoxLayout(row); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(12); + auto* lbl = new QLabel(label, row); + lbl->setMinimumWidth(96); + lay->addWidget(lbl); + lay->addWidget(control, 1); + return row; +} + +// 区段标题。 +ElaText* sectionTitle(const QString& text, QWidget* parent) { + auto* t = new ElaText(text, parent); + t->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kHeading)); + return t; +} + +QWidget* buildAppearancePage() { + auto* page = new QWidget(); + auto* v = new QVBoxLayout(page); + v->setContentsMargins(24, 20, 24, 20); + v->setSpacing(16); + v->addWidget(sectionTitle(QStringLiteral("外观"), page)); + + // 主题:跟随系统 / 浅色 / 深色(热切)。 + auto* themeCombo = new ElaComboBox(page); + themeCombo->addItem(QStringLiteral("跟随系统"), QStringLiteral("system")); + themeCombo->addItem(QStringLiteral("浅色"), QStringLiteral("light")); + themeCombo->addItem(QStringLiteral("深色"), QStringLiteral("dark")); + const QString curTheme = geopro::app::themeModePreference(); + themeCombo->setCurrentIndex(themeCombo->findData(curTheme) >= 0 ? themeCombo->findData(curTheme) : 0); + QObject::connect(themeCombo, &QComboBox::activated, page, [themeCombo](int) { + geopro::app::setThemeModePreference(themeCombo->currentData().toString()); + }); + v->addWidget(makeRow(QStringLiteral("主题"), themeCombo)); + + // 界面字号:小/标准/大/特大(重启生效)。 + auto* fontCombo = new ElaComboBox(page); + fontCombo->addItem(QStringLiteral("小"), 90); + fontCombo->addItem(QStringLiteral("标准"), 100); + fontCombo->addItem(QStringLiteral("大"), 115); + fontCombo->addItem(QStringLiteral("特大"), 130); + const int curScale = geopro::app::fontScalePreference(); + fontCombo->setCurrentIndex(fontCombo->findData(curScale) >= 0 ? fontCombo->findData(curScale) : 1); + v->addWidget(makeRow(QStringLiteral("界面字号"), fontCombo)); + + // 字号改动:持久化 + 提示重启(提供立即重启)。 + auto* restartRow = new QWidget(page); + auto* rlay = new QHBoxLayout(restartRow); + rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐 + rlay->setSpacing(10); + auto* hint = new ElaText(QStringLiteral("界面字号将在重启后生效"), restartRow); + hint->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kCaption)); + auto* restartBtn = new ElaPushButton(QStringLiteral("立即重启"), restartRow); + rlay->addWidget(hint); + rlay->addWidget(restartBtn); + rlay->addStretch(); + restartRow->setVisible(false); + v->addWidget(restartRow); + + QObject::connect(fontCombo, &QComboBox::activated, page, [fontCombo, restartRow](int) { + geopro::app::setFontScalePreference(fontCombo->currentData().toInt()); + restartRow->setVisible(true); + }); + QObject::connect(restartBtn, &QPushButton::clicked, restartBtn, [] { + QProcess::startDetached(QCoreApplication::applicationFilePath(), + QCoreApplication::arguments().mid(1)); + qApp->quit(); + }); + + v->addStretch(); + return page; +} + +QWidget* buildAboutPage() { + auto* page = new QWidget(); + auto* v = new QVBoxLayout(page); + v->setContentsMargins(24, 20, 24, 20); + v->setSpacing(12); + v->addWidget(sectionTitle(QStringLiteral("关于"), page)); + + auto* ver = new ElaText(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"), page); + ver->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kTitle)); + v->addWidget(ver); + + auto* license = new QTextBrowser(page); + license->setOpenExternalLinks(true); + license->setHtml(QStringLiteral( + "第三方组件与许可证" + "" + "" + "" + "" + "" + "" + "
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

")); + v->addWidget(license, 1); + return page; +} + +} // namespace + +SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { + setWindowTitle(QStringLiteral("设置")); + resize(720, 480); + + auto* root = new QHBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // 左:分类列表。 + auto* sidebar = new QListWidget(this); + sidebar->setObjectName(QStringLiteral("settingsSidebar")); + sidebar->setFixedWidth(150); + sidebar->addItem(QStringLiteral("外观")); + sidebar->addItem(QStringLiteral("关于")); + root->addWidget(sidebar); + + // 右:分页。 + auto* stack = new QStackedWidget(this); + stack->addWidget(buildAppearancePage()); + stack->addWidget(buildAboutPage()); + root->addWidget(stack, 1); + + QObject::connect(sidebar, &QListWidget::currentRowChanged, stack, + &QStackedWidget::setCurrentIndex); + sidebar->setCurrentRow(0); +} + +} // namespace geopro::app diff --git a/src/app/SettingsDialog.hpp b/src/app/SettingsDialog.hpp new file mode 100644 index 0000000..282c18e --- /dev/null +++ b/src/app/SettingsDialog.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +namespace geopro::app { + +// 设置对话框(左分类 + 右内容页,参考常见客户端布局): +// 外观 —— 主题(跟随系统/浅色/深色,热切) + 界面字号(小/标准/大/特大,重启生效) +// 关于 —— 版本号 + 第三方组件与许可证 +class SettingsDialog : public QDialog { + Q_OBJECT +public: + explicit SettingsDialog(QWidget* parent = nullptr); +}; + +} // namespace geopro::app diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index c287dce..4f8471f 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -473,6 +474,60 @@ void applyBrandAccent() eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::BasicSelectedHoverAlpha, QColor(0x3C, 0x5D, 0x87)); } +// ── 设置:主题 / 字号 偏好 ────────────────────────────────────────────── +namespace { +constexpr int kBaseFontPx = 13; // 基准字号(与 eApp 默认一致) +int g_fontScale = 100; // 当前字号缩放百分比 +} // namespace + +QString themeModePreference() +{ + return QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString(); +} + +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); + } +} + +void setThemeModePreference(const QString& mode) +{ + QSettings().setValue(QStringLiteral("ui/themeMode"), mode); + applyPersistedThemeMode(); // 主题可热切,立即应用 +} + +int fontScalePreference() +{ + return QSettings().value(QStringLiteral("ui/fontScale"), 100).toInt(); +} + +void setFontScalePreference(int percent) +{ + QSettings().setValue(QStringLiteral("ui/fontScale"), percent); // 重启后生效 +} + +void applyPersistedFontScale() +{ + g_fontScale = fontScalePreference(); + QFont f = qApp->font(); + f.setPixelSize(kBaseFontPx * g_fontScale / 100); + qApp->setFont(f); +} + +int scaledPx(int basePx) +{ + return basePx * g_fontScale / 100; +} + bool isDarkTheme() { return eTheme->getThemeMode() == ElaThemeType::Dark; diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index b80f69e..0563274 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -94,6 +94,19 @@ void applyTheme(QApplication& app); // (选中/激活/标题栏强调) 与本项目 QSS 共用同一套蓝,消除"多种蓝打架"。在 eApp->init() 后调一次。 void applyBrandAccent(); +// ── 设置:主题 / 界面字号 偏好(QSettings 持久化)──────────────────────── +// 启动时(eApp->init + applyBrandAccent 之后、弹登录窗之前)各调一次,使登录页与主页统一。 +void applyPersistedThemeMode(); // 应用持久化主题:跟随系统 / 浅色 / 深色 → ElaTheme +void applyPersistedFontScale(); // 应用持久化字号:设 qApp 基准字体 + 记录缩放(供 scaledPx) + +QString themeModePreference(); // "system" | "light" | "dark"(默认 system) +void setThemeModePreference(const QString& mode); // 持久化 + 立即应用(主题可热切) + +int fontScalePreference(); // 缩放百分比 90/100/115/130(默认 100) +void setFontScalePreference(int percent); // 仅持久化(字号改动重启后生效) + +int scaledPx(int basePx); // basePx × 当前字号% / 100(内联 QSS 字号用,使自定义 chrome 也随字号缩放) + // 当前 ElaTheme 是否暗色(供内联样式判断)。 bool isDarkTheme(); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 8272249..73295b3 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -1,5 +1,6 @@ #include "TopBar.hpp" +#include #include #include #include @@ -145,12 +146,12 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { " font-size:%1px; }" "#userName { color:#1F2A3D; font-size:%3px; font-weight:%4; }" "#userRole { color:#8A93A3; font-size:%5px; }") - .arg(type::kBody) + .arg(scaledPx(type::kBody)) .arg(type::kWeightBold) - .arg(type::kLabel) + .arg(scaledPx(type::kLabel)) .arg(type::kWeightSemibold) - .arg(type::kCaption) - .arg(type::kTitle)); + .arg(scaledPx(type::kCaption)) + .arg(scaledPx(type::kTitle))); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); @@ -188,7 +189,10 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeIconButton(this, ElaIconType::CircleQuestion, QStringLiteral("帮助"))); lay->addWidget(makeIconButton(this, ElaIconType::Bell, QStringLiteral("通知"))); - lay->addWidget(makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置"))); + auto* gearBtn = makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置")); + if (auto* gb = qobject_cast(gearBtn)) + QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); }); + lay->addWidget(gearBtn); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(12); diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index 662bed6..f90445e 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -26,6 +26,7 @@ signals: void projectSwitchRequested(const QString& projectId); void allProjectsRequested(); // 点击"全部项目…" void logoutRequested(); // 头像菜单「退出登录」 + void settingsRequested(); // 点击齿轮图标 → 打开设置 private: QToolButton* wsBtn_ = nullptr; diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index fbd43ae..8df61fd 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -104,10 +104,10 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") - .arg(type::kDisplay) + .arg(scaledPx(type::kDisplay)) .arg(type::kWeightBold) - .arg(type::kCaption) - .arg(type::kCaption) + .arg(scaledPx(type::kCaption)) + .arg(scaledPx(type::kCaption)) .arg(type::kWeightSemibold)); auto* root = new QVBoxLayout(this); @@ -198,7 +198,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 错误提示:固定占位高度,避免出现时整体布局跳动。 errorLabel_ = new QLabel(body); geopro::app::applyThemedStyleSheet( - errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption)); + errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(scaledPx(type::kCaption))); errorLabel_->setWordWrap(true); errorLabel_->setMinimumHeight(18); form->addWidget(errorLabel_); diff --git a/src/app/main.cpp b/src/app/main.cpp index 6e6a92d..a493a93 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -81,6 +81,7 @@ #include "Glyphs.hpp" #include "PanelHeader.hpp" #include "Theme.hpp" +#include "SettingsDialog.hpp" #include "TopBar.hpp" #include "CentralScene.hpp" #include "ProjectListDialog.hpp" @@ -335,7 +336,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "QToolButton:hover{ background:#EEF3FB; }" "QToolButton:checked{ color:#2D6CB5; font-weight:%2;" " border-bottom:2px solid #2D6CB5; }") - .arg(geopro::app::type::kBody) + .arg(geopro::app::scaledPx(geopro::app::type::kBody)) .arg(geopro::app::type::kWeightSemibold); // 工具条:「二维地图/三维视图」两个互斥可勾选按钮。默认二维地图。 @@ -380,7 +381,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re layerTitle, QStringLiteral("font-weight:%1;color:#2D6CB5;border:none;background:transparent;" "padding-bottom:3px;font-size:%2px;") .arg(geopro::app::type::kWeightSemibold) - .arg(geopro::app::type::kTitle)); + .arg(geopro::app::scaledPx(geopro::app::type::kTitle))); auto* chkCurtain = new ElaCheckBox(QStringLiteral("帘面(断面墙)")); chkCurtain->setChecked(true); auto* chkVoxel = new ElaCheckBox(QStringLiteral("体素(dd_voxel)")); @@ -431,7 +432,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re esTitle->setAlignment(Qt::AlignCenter); geopro::app::applyThemedStyleSheet( esTitle, QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") - .arg(geopro::app::type::kHeading) + .arg(geopro::app::scaledPx(geopro::app::type::kHeading)) .arg(geopro::app::type::kWeightSemibold)); auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n" @@ -439,7 +440,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re emptyState); esHint->setAlignment(Qt::AlignCenter); geopro::app::applyThemedStyleSheet( - esHint, QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody)); + esHint, + QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody))); esLay->addWidget(esIcon); esLay->addWidget(esTitle); @@ -930,6 +932,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QCoreApplication::arguments().mid(1)); qApp->quit(); }); + // 设置:点齿轮 → 打开设置对话框(外观/关于)。 + QObject::connect(topBar, &geopro::app::TopBar::settingsRequested, &window, [&window]() { + geopro::app::SettingsDialog dlg(&window); + dlg.exec(); + }); QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window, [&projectRepo, &nav, topBar, &window]() { auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window); @@ -1059,10 +1066,12 @@ int main(int argc, char* argv[]) // ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化, // 两种壳都需要它(登录发生在选壳之前)。Ela 控件跟随 ElaTheme;标准控件仍由下面 QSS 接管。 eApp->init(); - geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝 + geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝 + geopro::app::applyPersistedThemeMode(); // 应用持久化主题(跟随系统/浅/深)——登录页与主页统一 + geopro::app::applyPersistedFontScale(); // 应用持久化字号缩放(登录页也随之缩放) // 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。 - // 跟随 ElaTheme 初始模式(可能随系统为暗),使登录窗与标准控件明暗一致(review M2)。 + // 跟随最终 ElaTheme 模式(持久化偏好已生效),使登录窗与标准控件明暗一致。 geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量; From 9010b20b571d68cd591c442d7c847cd90363cd8f Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 14:44:59 +0800 Subject: [PATCH 34/56] =?UTF-8?q?refactor(ui):=20=E6=96=B9=E6=A1=88A=20?= =?UTF-8?q?=E2=80=94=20=E7=A7=BB=E9=99=A4=20ElaWidgetTools=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=BA=E6=A0=87=E5=87=86=20Qt=20+=20?= =?UTF-8?q?=E5=8D=95=E4=B8=80=E8=AE=BE=E8=AE=A1=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: 此前 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 --- CMakeLists.txt | 17 - NOTICE.md | 9 +- docs/Geopro3.0_视觉设计规范.md | 669 +++++++++++++++++++++++++++++ src/app/CMakeLists.txt | 1 - src/app/Glyphs.cpp | 12 +- src/app/PanelHeader.cpp | 27 +- src/app/ProjectListDialog.cpp | 18 +- src/app/SettingsDialog.cpp | 31 +- src/app/Theme.cpp | 233 ++++++---- src/app/Theme.hpp | 22 +- src/app/TopBar.cpp | 44 +- src/app/login/LoginWindow.cpp | 14 +- src/app/main.cpp | 82 ++-- src/app/panels/ObjectTreePanel.cpp | 10 +- 14 files changed, 924 insertions(+), 265 deletions(-) create mode 100644 docs/Geopro3.0_视觉设计规范.md 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); From a6a3979b93f25472bb230120da726e1423b834b7 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 14:52:27 +0800 Subject: [PATCH 35/56] =?UTF-8?q?fix(ui):=20=E4=B8=8B=E6=8B=89=E6=A1=86?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=20=E2=80=94=20=E5=B9=B2=E5=87=80=E5=A1=AB?= =?UTF-8?q?=E5=85=85=E9=80=89=E4=B8=AD=20+=20=E5=8E=BB=E8=BE=B9=E6=A1=86/?= =?UTF-8?q?=E5=9C=86=E8=A7=92=E4=BC=AA=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QComboBox 弹窗当前/选中项原来显示一个细边框方块(Fusion 默认 + 缺 ::item 规则)。 补 QComboBox QAbstractItemView::item 规则: 统一行高/内边距、去边框、选中=浅蓝填充+强调色文字; 去掉 view 的 border-radius(防圆角弹窗露直角)。改一处(单一 QSS)即生效。 --- src/app/Theme.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index c504ffe..6c8a941 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -303,10 +303,18 @@ QComboBox::drop-down { QComboBox QAbstractItemView { background: #FFFFFF; border: 1px solid #D5DBE5; - border-radius: 6px; - selection-background-color: #DCE9F8; - selection-color: #1B3D67; outline: none; + padding: 2px; +} +QComboBox QAbstractItemView::item { + border: none; + padding: 6px 10px; + min-height: 20px; + color: #1F2A3D; +} +QComboBox QAbstractItemView::item:selected { + background: #EAF1FB; + color: #2D6CB5; } /* ── 分组框(按需出现时也与主题一致)──────────────────────── */ From 6c34f71177e7cd5bd62cd1d932cb0acfc6309519 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 14:58:01 +0800 Subject: [PATCH 36/56] =?UTF-8?q?fix(ui):=20=E4=B8=8B=E6=8B=89=E6=A1=86?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=94=B9=E4=B8=BA=E7=B4=A7=E8=B4=B4=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=A1=86=E4=B8=8B=E6=96=B9=E7=9A=84=E5=8D=95=E5=B1=82?= =?UTF-8?q?=E5=88=97=E8=A1=A8(AppProxyStyle)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: Fusion 的 combo 弹窗是「菜单式覆盖当前项」(SH_ComboBox_Popup=true) → 位置怪、 容器框+列表两层、选中/悬停不清。AppProxyStyle 把该 hint 改为 0 → 标准「下方下拉列表」: 位置紧贴文本框、单层、当前项与悬停项走 ::item:selected 高亮。 --- src/app/Theme.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 6c8a941..e05e7eb 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,19 @@ namespace geopro::app { namespace { +// 应用样式:在 Fusion 基础上把下拉框弹窗改为「列表紧贴文本框下方」(而非 Fusion 默认的 +// 菜单式弹窗覆盖当前项——那会导致弹窗位置怪、容器+列表两层、选中不清)。 +class AppProxyStyle : public QProxyStyle { +public: + AppProxyStyle() : QProxyStyle(QStyleFactory::create(QStringLiteral("Fusion"))) {} + int styleHint(StyleHint hint, const QStyleOption* opt, const QWidget* w, + QStyleHintReturn* ret) const override + { + if (hint == QStyle::SH_ComboBox_Popup) return 0; + return QProxyStyle::styleHint(hint, opt, w, ret); + } +}; + // 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。 // 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写, // 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制, @@ -479,8 +493,8 @@ QPalette buildPalette(bool dark) void applyThemeMode(QApplication& app, bool dark) { - // Fusion:跨平台一致且对 QSS 友好(Windows 原生风对部分控件会忽略样式表)。 - app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); + // Fusion + 下拉框弹窗修正(AppProxyStyle):跨平台一致、对 QSS 友好。 + app.setStyle(new AppProxyStyle()); // 基础字体:微软雅黑 UI;基准字号取令牌 type::kBody(13px),与 QSS 同单位。 QFont base(QStringLiteral("Microsoft YaHei UI")); From 0edfa56ec654c0a7d120a4abb8110df035d548fc Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 15:14:44 +0800 Subject: [PATCH 37/56] =?UTF-8?q?docs:=20=E8=AE=BE=E8=AE=A1=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E8=90=BD=E5=9C=B0=E8=AE=A1=E5=88=92=20+=20=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF=E4=B8=8E=E5=81=8F=E7=A6=BB=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-06-10-design-baseline.md | 14 + .../2026-06-10-design-system-adoption.md | 533 ++++++++++++++++++ 2 files changed, 547 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-10-design-baseline.md create mode 100644 docs/superpowers/plans/2026-06-10-design-system-adoption.md diff --git a/docs/superpowers/plans/2026-06-10-design-baseline.md b/docs/superpowers/plans/2026-06-10-design-baseline.md new file mode 100644 index 0000000..a0a7141 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-design-baseline.md @@ -0,0 +1,14 @@ +# 设计规范落地 —— 基线与有意偏离记录 + +**基线(改动前):** `build.bat app` 通过;`build/release/src/app/geopro_desktop.exe` 现行可执行(ninja: no work to do = 源码与产物同步)。分支 `refactor/pure-qt-ui`。 + +## 有意偏离规范的三点(经用户确认的范围裁剪) + +1. **字号**:保留现有 px 字号缩放体系(`Theme.hpp` `type::` 命名空间 + `scaledPx`),不切规范 §2.2 的 pt。理由:用户已投入字号缩放设置,切 pt 会破坏现有缩放与持久化。 +2. **图标**:保留自有 `Glyphs`(程序绘制矢量、随主题着色),不引入 QtAwesome。理由:已满足规范 §9「矢量 + 可染色 + 随主题」的意图,引入新依赖收益低。 +3. **本轮不做**:表格 / 对话框 / Toast / Tooltip 富组件、VTK colormap(§8.2)。留待后续独立计划。 + +## 构建说明(供实现者) + +- 命令:项目根目录执行 `build.bat app`(MSVC + Ninja,preset `msvc-release`)。 +- 在 PowerShell 下 `& .\build.bat app` 会打印一行 `vswhere.exe is not recognized` 的 stderr 噪声,但 ninja 仍会运行——以最终的 ninja/cl 输出与 exit code 为准,不要被该行误导。 diff --git a/docs/superpowers/plans/2026-06-10-design-system-adoption.md b/docs/superpowers/plans/2026-06-10-design-system-adoption.md new file mode 100644 index 0000000..7d1b112 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-design-system-adoption.md @@ -0,0 +1,533 @@ +# Geopro 3.0 视觉设计规范落地 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **每个 Task 末尾的「规范一致性校验」步骤是硬性门禁** —— 必须派 subagent 逐值核对改动与 `docs/Geopro3.0_视觉设计规范.md`,PASS 才能进入下一个 Task。 + +**Goal:** 把 `docs/Geopro3.0_视觉设计规范.md` 落成代码层的单一事实来源:集中语义令牌(light/dark 双值)→ 模板化 QSS → 同步 QPalette → 数据画布常深 → 关键列表面板卡片化,消除当前「裸 hex 散落 + 暗色靠字符串替换」的脆弱结构。 + +**Architecture:** 在 `Theme.cpp` 建一张语义令牌表(`name → {lightHex, darkHex}`,取值严格来自规范 §1.5 + 附录 A + §1.3 画布色)。新增 `token()/tokenColor()/fillTokens()/applyTokenizedStyleSheet()` API:QSS 模板用 `{{token}}` 占位,运行时按当前明暗填充;`QPalette` 同一令牌表构建;主题切换时广播重填。逐步把内联 QSS 调用方迁移到令牌,最后删除遗留的 `kDarkMap` 字符串替换路径。 + +**Tech Stack:** C++17 / Qt 6 (QtWidgets) / Fusion QStyle + QSS + QPalette / VTK / ADS。构建:`build.bat app`(MSVC + Ninja,CMake preset `msvc-release`)。 + +**Scope(明确边界,YAGNI):** +- ✅ 本计划覆盖:令牌基础设施、数据画布常深、全局标准控件重着色到规范色值、内联 QSS 调用方迁移、异常列表 + 数据集列表两个面板卡片化。 +- ❌ 本计划**不**覆盖(留待后续独立计划):表格/对话框/Toast/Tooltip 富组件、VTK colormap(§8.2)、字号从 px 切 pt(§2.2,当前 px 字号缩放体系刻意保留)、图标改 QtAwesome(当前自有 `Glyphs` 已矢量+随主题,满足 §9 意图,刻意保留)。这三点在 Task 0 记为「有意偏离」。 + +**Verify 形态说明(本领域无 QSS 单测,刻意不套 TDD):** 每个 Task 的验证 = (a) `build.bat app` 通过;(b) 规范一致性校验 subagent 返回 PASS;(c) 标注 `[截图检查点]` 的 Task 由用户看真机截图拍板。 + +--- + +## File Structure + +| 文件 | 职责 | 本计划改动 | +|---|---|---| +| `src/app/Theme.hpp` | 主题公共 API + 排版/间距/圆角令牌 | 新增令牌 API 声明 | +| `src/app/Theme.cpp` | 令牌表、QSS 模板、QPalette、主题管理器 | 重写核心:令牌表 + 模板化 + palette 从令牌构建;最后删 `kDarkMap` | +| `src/app/CentralScene.cpp` | VTK 场景重建(含背景色) | 画布背景改常深(间接,经 `vtkBackground()`) | +| `src/app/TopBar.cpp` | 顶栏 chrome 内联 QSS | 迁移到 `{{token}}` | +| `src/app/PanelHeader.cpp` | 面板表头内联 QSS | 迁移到 `{{token}}` | +| `src/app/panels/ObjectTreePanel.cpp` | 对象树(内联复选框/选中色) | 迁移到令牌 | +| `src/app/panels/AnomalyListPanel.cpp` | 异常列表 | 卡片化(自绘 item delegate / 富 item) | +| `src/app/panels/DatasetListPanel.cpp` | 数据集列表 | 卡片化(双行 + 选中竖条) | + +--- + +## Task 0: 基线快照 + 偏离记录(无代码改动) + +**Files:** +- Create: `docs/superpowers/plans/2026-06-10-design-baseline.md` + +- [ ] **Step 1: 记录当前 git 状态与构建基线** + +Run: `build.bat app` +Expected: 构建成功(建立「改动前可编译」基线)。若失败,先停下来报告,不要继续。 + +- [ ] **Step 2: 写下有意偏离规范的三点** + +在 `docs/superpowers/plans/2026-06-10-design-baseline.md` 写入: + +```markdown +# 设计规范落地 —— 有意偏离记录(经用户确认的范围裁剪) +1. 字号:保留现有 px 字号缩放体系(Theme.hpp type:: 命名空间 + scaledPx),不切规范 §2.2 的 pt。理由:用户已投入字号缩放设置,切 pt 会破坏现有缩放。 +2. 图标:保留自有 Glyphs(程序绘制矢量、随主题着色),不引入 QtAwesome。理由:已满足规范 §9「矢量+可染色+随主题」的意图,引入新依赖收益低。 +3. 本轮不做:表格/对话框/Toast/Tooltip 富组件、VTK colormap(§8.2)。留待后续计划。 +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/superpowers/plans/ +git commit -m "docs: 设计规范落地计划 + 基线与偏离记录" +``` + +--- + +## Task 1: 语义令牌基础设施(additive,无视觉变化) + +**Files:** +- Modify: `src/app/Theme.hpp`(新增 API 声明) +- Modify: `src/app/Theme.cpp`(新增令牌表 + 实现,暂不替换现有 QSS) + +- [ ] **Step 1: 在 `Theme.hpp` 声明令牌 API** + +在 `namespace geopro::app {` 内、`applyThemedStyleSheet` 声明附近加入: + +```cpp +// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens;规范 §1.5 + 附录 A + §1.3)── +// 组件只引语义 token,禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。 +QString token(const char* name); // 当前明暗下的 hex(未知名返回品红 "#FF00FF" 以便一眼发现漏配) +QColor tokenColor(const char* name); // 同上,QColor 形式 + +// 把 QSS 模板里的 {{token}} 占位替换为当前明暗的 hex 后返回。 +QString fillTokens(const QString& tmpl); + +// 应用一段 {{token}} 模板 QSS 到 widget,并随主题切换自动重填。 +// 迁移内联 QSS 调用方的目标接口(取代 applyThemedStyleSheet 的浅色 hex 写法)。 +void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl); +``` + +- [ ] **Step 2: 在 `Theme.cpp` 匿名 namespace 顶部加入令牌表** + +放在 `kStyleSheet` 之前。**取值逐一对照规范,禁止改动**: + +```cpp +// ── 语义令牌表(全 UI 唯一颜色来源)。改色只改这一处。 ────────────────── +// 取值来源:规范 §1.5 语义映射 + 附录 A 速查 + §1.3 画布专用色。 +// 画布(canvas/*)与 bg/canvas 两模式同值——规范 §0.5「视图区永远深色」。 +struct Token { const char* name; const char* light; const char* dark; }; +const Token kTokens[] = { + // 背景 + {"bg/app", "#F7F8FA", "#0E1116"}, + {"bg/panel", "#FFFFFF", "#161A20"}, + {"bg/panel-subtle", "#FCFCFD", "#161B22"}, + {"bg/header", "#FFFFFF", "#12161C"}, + {"bg/hover", "#EFF1F4", "#1B2129"}, + {"bg/selected", "#EFF5FF", "#16243F"}, + {"bg/canvas", "#0B1320", "#0B1320"}, + // 边框 + {"border/default", "#E3E6EB", "#262C35"}, + {"border/strong", "#CDD2DA", "#333B45"}, + {"border/focus", "#3B73EC", "#5E8DF5"}, + // 文字 + {"text/primary", "#272C35", "#E6E9EF"}, + {"text/secondary", "#5A626F", "#A4ADBB"}, + {"text/tertiary", "#7C8493", "#7A8494"}, + {"text/disabled", "#A8AFBC", "#5A626F"}, + {"text/link", "#3B73EC", "#5E8DF5"}, + {"text/on-primary", "#FFFFFF", "#FFFFFF"}, + // 强调 + {"accent/primary", "#3B73EC", "#5E8DF5"}, + {"accent/primary-hover", "#2B5FD9", "#93B4FA"}, + {"accent/primary-pressed","#2450B8", "#3B73EC"}, + // 其他 + {"divider", "#E3E6EB", "#22272F"}, + {"scrollbar/thumb", "#CDD2DA", "#3A424D"}, + {"scrollbar/thumb-hover", "#A8AFBC", "#4A535F"}, + // 状态色(主色 + 浅底)规范 §1.4 + {"status/danger", "#E5484D", "#FF6166"}, + {"status/danger-bg", "#FDECEC", "#3A1D1F"}, + {"status/warning", "#E08A1E", "#F5A623"}, + {"status/warning-bg", "#FBF0DD", "#3A2C12"}, + {"status/success", "#2E9E5B", "#46C07A"}, + {"status/success-bg", "#E7F6ED", "#16301F"}, + {"status/info", "#3B73EC", "#5E8DF5"}, + {"status/info-bg", "#EFF5FF", "#16243F"}, + {"status/neutral", "#7C8493", "#8A93A3"}, + // 画布专用(两模式同值)规范 §1.3 + {"canvas/bg", "#0B1320", "#0B1320"}, + {"canvas/bg-soft", "#111B2D", "#111B2D"}, + {"canvas/grid", "#1E2A3D", "#1E2A3D"}, + {"canvas/text", "#E6ECF5", "#E6ECF5"}, + {"canvas/text-dim", "#8A97AC", "#8A97AC"}, +}; + +QString tokenHex(const char* name, bool dark) +{ + for (const auto& t : kTokens) + if (qstrcmp(t.name, name) == 0) return QString::fromLatin1(dark ? t.dark : t.light); + return QStringLiteral("#FF00FF"); // 漏配的令牌显眼品红,便于一眼发现 +} +``` + +- [ ] **Step 3: 在 `Theme.cpp` 实现公共 API(文件末尾 `namespace geopro::app` 内)** + +```cpp +QString token(const char* name) { return tokenHex(name, isDarkTheme()); } + +QColor tokenColor(const char* name) { return QColor(token(name)); } + +QString fillTokens(const QString& tmpl) +{ + const bool dark = isDarkTheme(); + QString s = tmpl; + for (const auto& t : kTokens) + s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)), + QString::fromLatin1(dark ? t.dark : t.light)); + return s; +} + +void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl) +{ + if (!w) return; + w->setStyleSheet(fillTokens(tmpl)); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w, + [w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); }); +} +``` + +> 注:`kTokens`/`tokenHex` 在匿名 namespace;`token/fillTokens` 等在 `geopro::app` 内调用 `tokenHex` 没问题(同 TU)。 + +- [ ] **Step 4: 构建** + +Run: `build.bat app` +Expected: 构建成功,**界面无任何变化**(新 API 尚无调用方)。 + +- [ ] **Step 5: 规范一致性校验(门禁)** + +派 subagent(opus)执行:读取 `docs/Geopro3.0_视觉设计规范.md` 的 §1.1–§1.5 + §1.3 + 附录 A,与 `Theme.cpp` 的 `kTokens` 表**逐 token 逐 hex 比对**。输出 PASS/FAIL 清单:每个 token 标注「规范值 vs 代码值」是否一致;列出规范有但表里缺的、表里有但规范无依据的。FAIL 则修正后重校,PASS 才继续。 + +- [ ] **Step 6: Commit** + +```bash +git add src/app/Theme.hpp src/app/Theme.cpp +git commit -m "feat(theme): 语义令牌基础设施(令牌表+token/fillTokens API,规范§1)" +``` + +--- + +## Task 2: 数据画布常深(规范 §0.5 / §5 / §11)[截图检查点] + +**Files:** +- Modify: `src/app/Theme.cpp:612-618`(`vtkBackground` 实现) + +- [ ] **Step 1: 改 `vtkBackground` 返回画布令牌,不随主题** + +把现有实现: + +```cpp +void vtkBackground(double& r, double& g, double& b) +{ + const QColor c = roleColor(isDarkTheme(), "#F4F6FA"); + r = c.redF(); + g = c.greenF(); + b = c.blueF(); +} +``` + +改为: + +```cpp +void vtkBackground(double& r, double& g, double& b) +{ + // 规范 §0.5/§11:数据画布永远深色,不随明暗切换。取 canvas/bg。 + const QColor c = tokenColor("canvas/bg"); // #0B1320 + r = c.redF(); + g = c.greenF(); + b = c.blueF(); +} +``` + +- [ ] **Step 2: 更新 `CentralScene.cpp:23` 的注释(避免注释腐烂)** + +把 `// 背景随主题(取 ElaTheme 窗口底色),暗色下不再是刺眼白底。` +改为 `// 背景永远深色(规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。` + +- [ ] **Step 3: 构建** + +Run: `build.bat app` +Expected: 构建成功。 + +- [ ] **Step 4: 规范一致性校验(门禁)** + +派 subagent:确认 (a) `vtkBackground` 返回 `canvas/bg` = `#0B1320`;(b) 两种主题模式下该函数返回值相同(不分支 `isDarkTheme()` 影响结果);(c) 符合规范 §11「VTK 渲染器背景在两模式下均深色,不参与切换」。PASS/FAIL。 + +- [ ] **Step 5: 用户截图检查点** + +请用户运行 `build.bat run`,截图浅色 + 深色两模式下中间视图,确认画布均为深蓝黑 `#0B1320`、与外围 UI 衔接自然。用户确认后继续。 + +- [ ] **Step 6: Commit** + +```bash +git add src/app/Theme.cpp src/app/CentralScene.cpp +git commit -m "feat(canvas): 数据画布常深 #0B1320(规范§0.5/§11)" +``` + +--- + +## Task 3: 全局标准控件重着色到规范色值(模板化 QSS + palette 从令牌)[截图检查点] + +**Files:** +- Modify: `src/app/Theme.cpp`(重写 `kStyleSheet` 为 `{{token}}` 模板、`buildPalette` 从令牌、`styleSheetForMode` 走 `fillTokens`) + +**说明:** 这是观感变化最大的一步(强调蓝 `#2D6CB5`→`#3B73EC`,中性灰换规范阶)。遗留的 `kDarkMap`/`themedQss`/`applyThemedStyleSheet` 暂保留,供尚未迁移的内联调用方(TopBar/面板)在本步后仍可用;Task 4–5 迁移完后于 Task 5 删除。 + +- [ ] **Step 1: 把 `kStyleSheet` 内所有裸 hex 换成 `{{token}}` 占位** + +逐段替换(映射规则固定,便于校验): + +| 原裸 hex | 语义 → 占位 | +|---|---| +| `#F4F6FA`(QMainWindow/QDialog 底、ADS 区底) | `{{bg/app}}` | +| `#FFFFFF`(面板/树/列表/工具条/状态栏/菜单底) | `{{bg/panel}}` | +| `#1F2A3D`(主文字) | `{{text/primary}}` | +| `#5A6B85`(次文字) | `{{text/secondary}}` | +| `#3A475C`(表头/分组标题文字) | `{{text/secondary}}` | +| `#9AA6B6` / `#8A93A3`(禁用/占位文字) | `{{text/disabled}}` | +| `#EDF1F7`(表头/抬升/ADS 标题底) | `{{bg/hover}}` | +| `#EFF1F4`/`#EEF3FB`/`#EAF1FB`/`#EEF2FB`(hover 底) | `{{bg/hover}}` | +| `#DCE9F8`/`#DCE6F4`(选中/按下底) | `{{bg/selected}}` | +| `#1B3D67`(选中文字) | `{{accent/primary-pressed}}`(深蓝,深底对比)| +| `#E3E6EB`/`#D5DBE5`/`#EAEEF4`/`#E1E6EE`/`#EEF1F5`/`#E6EBF3`/`#E6EAF1`(分隔/边框/轨道) | `{{divider}}` 或 `{{border/default}}`(边框用 border/default,分隔线/轨道用 divider)| +| `#C2CCDA`/`#C7D2E0`(输入/按钮强边框、滚动条 thumb) | 边框→`{{border/strong}}`;滚动条 thumb→`{{scrollbar/thumb}}` | +| `#A7B4C7`(滚动条 hover thumb) | `{{scrollbar/thumb-hover}}` | +| `#2D6CB5`(强调) | `{{accent/primary}}` | +| `#2862A6`(强调 hover) | `{{accent/primary-hover}}` | +| `#234F87`(强调 pressed) | `{{accent/primary-pressed}}` | +| `#F0F2F6`/`#F0F1F4`(禁用底) | `{{bg/app}}` | + +> QToolTip 段维持「不写 QSS」现状(用原生)。`QSplitter::handle:hover`/ADS splitter hover 用 `{{accent/primary}}`(规范 §4.2「splitter hover 显示 accent/primary」)。 + +- [ ] **Step 2: `styleSheetForMode` 改走 `fillTokens`** + +```cpp +QString styleSheetForMode(bool /*dark*/) +{ + return fillTokens(QString::fromUtf8(kStyleSheet)); +} +``` + +(`fillTokens` 内部已按 `isDarkTheme()` 取值;保留参数签名以免动调用方,但实现以当前模式为准。若调用点传入与当前模式不一致的 dark,需在 Step 3 校验中确认 `applyThemeMode` 调用时 `ThemeManager` 状态已就绪。) + +> ⚠️ 依赖检查:`applyThemeMode(app, dark)` 在 `main` 中调用时机须保证 `ThemeManager::instance()` 的 `dark_` 已与传入 `dark` 一致。若不一致,改为让 `applyThemeMode` 内 `setStyleSheet(fillTokens(...))` 前不依赖参数、统一以 `ThemeManager` 为准;并在本步明确 `applyThemeMode` 的 `dark` 参数仅用于 palette。**实现者须先 grep `applyThemeMode(` 的所有调用点确认。** + +- [ ] **Step 3: `buildPalette` 从令牌构建** + +把 `buildPalette` 内的 `roleColor(dark, "#xxxxxx")` 调用改为 `QColor(tokenHex(name, dark))`: + +```cpp +QPalette buildPalette(bool dark) +{ + QPalette p; + const QColor shell = QColor(tokenHex("bg/app", dark)); + const QColor panel = QColor(tokenHex("bg/panel", dark)); + const QColor text = QColor(tokenHex("text/primary", dark)); + const QColor muted = QColor(tokenHex("text/secondary", dark)); + const QColor accent = QColor(tokenHex("accent/primary", dark)); + const QColor border = QColor(tokenHex("border/default", dark)); + const QColor disabled = QColor(tokenHex("text/disabled", dark)); + const QColor hoverBg = QColor(tokenHex("bg/hover", dark)); + + p.setColor(QPalette::Window, shell); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, panel); + p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark))); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, hoverBg); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::ToolTipBase, text); + p.setColor(QPalette::ToolTipText, panel); + p.setColor(QPalette::Highlight, accent); + p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark))); + p.setColor(QPalette::PlaceholderText, muted); + p.setColor(QPalette::Link, accent); + p.setColor(QPalette::Light, panel); + p.setColor(QPalette::Midlight, border); + p.setColor(QPalette::Mid, border); + p.setColor(QPalette::Dark, border); + p.setColor(QPalette::Shadow, border); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + return p; +} +``` + +- [ ] **Step 4: 构建** + +Run: `build.bat app` +Expected: 构建成功。 + +- [ ] **Step 5: 规范一致性校验(门禁)** + +派 subagent:(a) 确认 `kStyleSheet` 内**不再有任何裸 `#` hex**(QToolTip 注释除外),全部为 `{{token}}`;(b) 抽查每条 `{{token}}` 的语义角色与规范 §3/§6/§7 控件描述一致(如选中行 = `bg/selected`、splitter hover = `accent/primary`);(c) `buildPalette` 各 QPalette 角色取的令牌合理。输出 PASS/FAIL。 + +- [ ] **Step 6: 用户截图检查点** + +请用户 `build.bat run`,截图浅/深两模式工作台全貌。重点确认强调蓝、中性灰、选中/hover、边框层级符合规范气质。**色板方向由用户拍板**(这是审美决策)。用户确认后继续。 + +- [ ] **Step 7: Commit** + +```bash +git add src/app/Theme.cpp +git commit -m "feat(theme): 全局 QSS 模板化 + palette 从令牌,标准控件对齐规范色值(§1/§3/§6/§7)" +``` + +--- + +## Task 4: 顶栏 + 面板表头内联 QSS 迁移到令牌 + +**Files:** +- Modify: `src/app/TopBar.cpp:135-154` +- Modify: `src/app/PanelHeader.cpp`(全文件内联 QSS) + +- [ ] **Step 1: TopBar 内联 QSS 改 `{{token}}` + `applyTokenizedStyleSheet`** + +把 `applyThemedStyleSheet(this, QStringLiteral("...裸 hex..."))` 改为 `applyTokenizedStyleSheet(this, QStringLiteral("...{{token}}..."))`,裸 hex 按 Task 3 同一映射替换: +- `#FFFFFF`→`{{bg/header}}`(顶栏用 header 底)、`#E1E6EE`→`{{divider}}`、`#1F2A3D`→`{{text/primary}}`、`#EEF3FB`→`{{bg/hover}}`、`#2D6CB5`→`{{accent/primary}}`、`#8A93A3`→`{{text/tertiary}}`。 +- `#avatar` 背景用 `{{accent/primary}}`,`color:white` 保持字面 `white`(头像白字恒白,规范 §5 主按钮 on-primary)。 +- 字号 `.arg(scaledPx(...))` 占位(`%1`…`%6`)保持不变——令牌只替换颜色,不碰字号。 + +> 注意:`{{token}}` 与 `.arg()` 的 `%N` 共存无冲突(`fillTokens` 只替换 `{{...}}`,`.arg` 只替换 `%N`)。先 `.arg()` 拼好字符串,再交给 `applyTokenizedStyleSheet` 填 `{{}}`。 + +- [ ] **Step 2: PanelHeader 同法迁移** + +读 `PanelHeader.cpp` 全文,将其内联 QSS 的裸 hex 按 Task 3 映射改为 `{{token}}`,调用改 `applyTokenizedStyleSheet`。表头底用 `{{bg/panel}}`、底部分隔 `{{divider}}`、标题文字 `{{text/primary}}`、图标按钮 hover `{{bg/hover}}`(规范 §4.3 面板标题栏:背景 bg/panel + 底部 1px divider)。 + +- [ ] **Step 3: 构建** + +Run: `build.bat app` +Expected: 构建成功,顶栏/表头外观与 Task 3 后的全局风格一致(同一套令牌)。 + +- [ ] **Step 4: 规范一致性校验(门禁)** + +派 subagent:确认 TopBar.cpp / PanelHeader.cpp 内联 QSS **无裸 hex**(`white` 关键字允许,记为「on-primary 恒白」);表头结构符合规范 §4.3 / §5。PASS/FAIL。 + +- [ ] **Step 5: Commit** + +```bash +git add src/app/TopBar.cpp src/app/PanelHeader.cpp +git commit -m "refactor(theme): TopBar/PanelHeader 内联 QSS 迁移到语义令牌(§4.3/§5)" +``` + +--- + +## Task 5: 对象树面板迁移 + 删除遗留 kDarkMap 路径 + +**Files:** +- Modify: `src/app/panels/ObjectTreePanel.cpp:45-71` +- Modify: `src/app/Theme.cpp`(删除 `kDarkMap`/`darkOf`/`roleColor`/`themedQss`/`themed`/`applyThemedStyleSheet`/`themed` 声明与实现——**仅当全仓无调用方时**) + +- [ ] **Step 1: ObjectTreePanel 复选框/选中色改令牌** + +把 `applyCheckboxStyle` lambda 内的硬编码 `QColor(0x..)` 与 `selBg/selFg` 字面 hex 改为 `tokenColor(...)`: +- `border` → `tokenColor("border/strong")` +- `boxBg` → `tokenColor("bg/panel")` +- `accent` → `tokenColor("accent/primary")` +- `selBg` → `token("bg/selected")` +- `selFg` → `token("accent/primary-pressed")`(深底对比的深蓝字) + +`hint_` 的 `color:#9AA6B6` → `applyTokenizedStyleSheet(hint_, "color:{{text/disabled}}; padding:16px;")`。 + +- [ ] **Step 2: grep 确认遗留路径已无调用方** + +Run: `grep -rn "applyThemedStyleSheet\|themedQss\|\bthemed(\|roleColor\|kDarkMap\|darkOf" src/` +Expected: 仅 `Theme.cpp` 定义处命中,无其他调用方。**若仍有调用方(如 DatasetListPanel/AnomalyListPanel 尚在 Task 6 前用到),跳过 Step 3,把删除挪到 Task 6 末尾。** + +- [ ] **Step 3: 删除遗留 kDarkMap 字符串替换路径(仅当 Step 2 确认无调用方)** + +从 `Theme.cpp` 删除:`struct DarkPair` + `kDarkMap[]` + `darkOf` + `roleColor` + `themedQss` + `themed` + `applyThemedStyleSheet`;从 `Theme.hpp` 删除 `themed` / `applyThemedStyleSheet` 声明。`styleSheetForMode` 已在 Task 3 改走 `fillTokens`,不依赖它们。 + +- [ ] **Step 4: 构建** + +Run: `build.bat app` +Expected: 构建成功。 + +- [ ] **Step 5: 规范一致性校验(门禁)** + +派 subagent:(a) ObjectTreePanel 无裸 hex;(b) 若执行了 Step 3,确认 `Theme.cpp` 已无字符串替换 dark 路径、暗色完全由 `kTokens` 双值驱动(规范 §13.1「集中 token」)。PASS/FAIL。 + +- [ ] **Step 6: Commit** + +```bash +git add src/app/panels/ObjectTreePanel.cpp src/app/Theme.hpp src/app/Theme.cpp +git commit -m "refactor(theme): 对象树迁移令牌 + 移除遗留 kDarkMap 字符串替换路径(§13.1)" +``` + +--- + +## Task 6: 异常列表 + 数据集列表卡片化(规范 §6.2 / §6.3)[截图检查点] + +**Files:** +- Modify: `src/app/panels/AnomalyListPanel.cpp`(异常卡片:左状态竖条 + 圆点 + 名称 + 等级胶囊 + 属性行 + 显隐眼睛) +- Modify: `src/app/panels/DatasetListPanel.cpp`(双行列表项 + 状态圆点 + 选中竖条) + +**实现取向:** 用 `QStyledItemDelegate` 自绘(保留 `QListWidget` 数据模型),颜色全取 `tokenColor(...)`。避免 `\n` 拼字符串那种朴素渲染。 + +- [ ] **Step 1: 异常列表 —— 定义等级→状态令牌映射** + +在 `AnomalyListPanel.cpp` 匿名 namespace 加: + +```cpp +// 规范 §1.4:异常分级 高=Danger 中=Warning 低=Info,停用=Neutral。 +// 返回 {主色 token, 浅底 token}。 +struct LevelTokens { const char* main; const char* bg; }; +LevelTokens levelTokens(int level) // 0=高 1=中 2=低 其他=停用 +{ + switch (level) { + case 0: return {"status/danger", "status/danger-bg"}; + case 1: return {"status/warning", "status/warning-bg"}; + case 2: return {"status/info", "status/info-bg"}; + default: return {"status/neutral", "bg/hover"}; + } +} +``` + +> 实现者须先确认 `geopro::core::Anomaly` 是否已有「等级」字段;若无,本步先用现有 `lineColor` 决定竖条色、等级胶囊暂以「—」占位,并在 baseline 文档记一条 TODO(不阻塞卡片结构)。**先读 `AnomalyListPanel.hpp` + Anomaly 定义确认。** + +- [ ] **Step 2: 异常列表 —— 写 `QStyledItemDelegate` 子类自绘卡片** + +卡片规范(§6.3):`radius/md`(8) 圆角、左 3px 状态色竖条、底为状态浅底、名称 `text/primary-strong`、右侧等级胶囊(`radius/pill`、状态浅底 + 状态主色字)、属性行 `text/caption` `text/secondary` 等宽数值、右侧显隐眼睛(开=`text/secondary`,关=`text/disabled`)。`sizeHint` 高度容纳双行(约 56px)。所有颜色 `tokenColor(...)`;主题切换时 `viewport()->update()`。 + +> 实现细节(cardpaint):`paint()` 内 `painter->setRenderHint(QPainter::Antialiasing)`;用 `QPainterPath::addRoundedRect` 画卡底;竖条 `fillRect`;胶囊 `drawRoundedRect`;眼睛图标复用 `makeGlyph`(若 `Glyph` 无 eye,先用文字「●/○」占位并记 TODO)。 + +- [ ] **Step 3: 数据集列表 —— 双行项 delegate** + +规范 §6.2:项高 52px、标题 `text/body` + 前置状态圆点、元信息行 `text/caption` `text/tertiary`(如「2026-03-15 09:21 · 64 道」)、选中项 `bg/selected` + 左 2px 竖条 + `radius/md`。同样 `QStyledItemDelegate` 自绘,颜色 `tokenColor`。 + +> 先读 `DatasetListPanel.cpp/.hpp` 确认现有数据字段(标题/日期/通道数来源)。 + +- [ ] **Step 4: 构建** + +Run: `build.bat app` +Expected: 构建成功。 + +- [ ] **Step 5: 若 Task 5 Step 3 当时被跳过,现在删除遗留 kDarkMap 路径** + +重跑 Task 5 的 grep 确认无调用方后,执行 Task 5 Step 3 的删除并构建。 + +- [ ] **Step 6: 规范一致性校验(门禁)** + +派 subagent:对照规范 §6.2 / §6.3 / §1.4,核对:竖条宽度(异常 3px / 数据集 2px)、圆角档(md/pill)、状态色映射(高=danger…)、字号角色、显隐态颜色、所有颜色经 `tokenColor` 无裸 hex。PASS/FAIL。 + +- [ ] **Step 7: 用户截图检查点** + +用户 `build.bat run`,截图异常列表 + 数据集列表(浅/深)。确认卡片层级、状态色、胶囊、选中竖条符合规范且美观。 + +- [ ] **Step 8: Commit** + +```bash +git add src/app/panels/AnomalyListPanel.cpp src/app/panels/DatasetListPanel.cpp src/app/Theme.hpp src/app/Theme.cpp +git commit -m "feat(panels): 异常/数据集列表卡片化,状态色对齐规范(§6.2/§6.3/§1.4)" +``` + +--- + +## 收尾:整体一致性复核 + +- [ ] **Step 1: 全仓裸 hex 扫描** + +Run: `grep -rn "#[0-9A-Fa-f]\{6\}" src/app/ --include=*.cpp --include=*.hpp` +Expected: 仅 `Theme.cpp` 的 `kTokens` 表命中(唯一颜色来源)。其余文件命中即为漏迁移,逐个修。 + +- [ ] **Step 2: 终版规范一致性总校验(门禁)** + +派 subagent(opus)做一次全量复核:以 `docs/Geopro3.0_视觉设计规范.md` 为准,遍历本计划范围内的所有改动文件,产出一份「规范条款 → 实现位置 → 一致/偏离」对照表。偏离项须落在 Task 0 记录的三条「有意偏离」内,否则修正。 + +- [ ] **Step 3: 用户最终验收截图**(浅/深 × 工作台全貌 + 各面板) + +- [ ] **Step 4: Commit 复核报告** + +```bash +git add docs/superpowers/plans/ +git commit -m "docs: 设计规范落地终版一致性复核报告" +``` From 18d084047f2bd6585aa0065349cfc40f777a2961 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 15:17:46 +0800 Subject: [PATCH 38/56] =?UTF-8?q?feat(theme):=20=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=88?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=E8=A1=A8+token/fillTokens=20API=EF=BC=8C?= =?UTF-8?q?=E8=A7=84=E8=8C=83=C2=A71=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/Theme.cpp | 79 +++++++++++++++++++++++++++++++++++++++++++++++ src/app/Theme.hpp | 12 +++++++ 2 files changed, 91 insertions(+) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index e05e7eb..3a8b0e1 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -28,6 +28,63 @@ public: } }; +// ── 语义令牌表(全 UI 唯一颜色来源)。改色只改这一处。 ────────────────── +// 取值来源:规范 §1.5 语义映射 + 附录 A 速查 + §1.3 画布专用色。 +// 画布(canvas/*)与 bg/canvas 两模式同值——规范 §0.5「视图区永远深色」。 +struct Token { const char* name; const char* light; const char* dark; }; +const Token kTokens[] = { + // 背景 + {"bg/app", "#F7F8FA", "#0E1116"}, + {"bg/panel", "#FFFFFF", "#161A20"}, + {"bg/panel-subtle", "#FCFCFD", "#161B22"}, + {"bg/header", "#FFFFFF", "#12161C"}, + {"bg/hover", "#EFF1F4", "#1B2129"}, + {"bg/selected", "#EFF5FF", "#16243F"}, + {"bg/canvas", "#0B1320", "#0B1320"}, + // 边框 + {"border/default", "#E3E6EB", "#262C35"}, + {"border/strong", "#CDD2DA", "#333B45"}, + {"border/focus", "#3B73EC", "#5E8DF5"}, + // 文字 + {"text/primary", "#272C35", "#E6E9EF"}, + {"text/secondary", "#5A626F", "#A4ADBB"}, + {"text/tertiary", "#7C8493", "#7A8494"}, + {"text/disabled", "#A8AFBC", "#5A626F"}, + {"text/link", "#3B73EC", "#5E8DF5"}, + {"text/on-primary", "#FFFFFF", "#FFFFFF"}, + // 强调 + {"accent/primary", "#3B73EC", "#5E8DF5"}, + {"accent/primary-hover", "#2B5FD9", "#93B4FA"}, + {"accent/primary-pressed","#2450B8", "#3B73EC"}, + // 其他 + {"divider", "#E3E6EB", "#22272F"}, + {"scrollbar/thumb", "#CDD2DA", "#3A424D"}, + {"scrollbar/thumb-hover", "#A8AFBC", "#4A535F"}, + // 状态色(主色 + 浅底)规范 §1.4 + {"status/danger", "#E5484D", "#FF6166"}, + {"status/danger-bg", "#FDECEC", "#3A1D1F"}, + {"status/warning", "#E08A1E", "#F5A623"}, + {"status/warning-bg", "#FBF0DD", "#3A2C12"}, + {"status/success", "#2E9E5B", "#46C07A"}, + {"status/success-bg", "#E7F6ED", "#16301F"}, + {"status/info", "#3B73EC", "#5E8DF5"}, + {"status/info-bg", "#EFF5FF", "#16243F"}, + {"status/neutral", "#7C8493", "#8A93A3"}, + // 画布专用(两模式同值)规范 §1.3 + {"canvas/bg", "#0B1320", "#0B1320"}, + {"canvas/bg-soft", "#111B2D", "#111B2D"}, + {"canvas/grid", "#1E2A3D", "#1E2A3D"}, + {"canvas/text", "#E6ECF5", "#E6ECF5"}, + {"canvas/text-dim", "#8A97AC", "#8A97AC"}, +}; + +QString tokenHex(const char* name, bool dark) +{ + for (const auto& t : kTokens) + if (qstrcmp(t.name, name) == 0) return QString::fromLatin1(dark ? t.dark : t.light); + return QStringLiteral("#FF00FF"); // 漏配的令牌显眼品红,便于一眼发现 +} + // 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。 // 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写, // 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制, @@ -625,4 +682,26 @@ void applyThemedStyleSheet(QWidget* w, const QString& designQss) [w, designQss]() { w->setStyleSheet(themedQss(designQss, isDarkTheme())); }); } +QString token(const char* name) { return tokenHex(name, isDarkTheme()); } + +QColor tokenColor(const char* name) { return QColor(token(name)); } + +QString fillTokens(const QString& tmpl) +{ + const bool dark = isDarkTheme(); + QString s = tmpl; + for (const auto& t : kTokens) + s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)), + QString::fromLatin1(dark ? t.dark : t.light)); + return s; +} + +void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl) +{ + if (!w) return; + w->setStyleSheet(fillTokens(tmpl)); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w, + [w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); }); +} + } // namespace geopro::app diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 50e98bb..9d79c0f 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -9,6 +9,7 @@ // 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA // 危险 #C0392B +#include #include #include @@ -136,4 +137,15 @@ void applyThemedStyleSheet(QWidget* w, const QString& designQss); // 如 ADS dockManager 在其自带样式后追加规则)。不自动随主题切换,调用方需自行在切换时重取。 QString themed(const QString& designQss); +// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens;规范 §1.5 + 附录 A + §1.3)── +// 组件只引语义 token,禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。 +QString token(const char* name); // 当前明暗下的 hex(未知名返回品红 "#FF00FF" 以便一眼发现漏配) +QColor tokenColor(const char* name); // 同上,QColor 形式 + +// 把 QSS 模板里的 {{token}} 占位替换为当前明暗的 hex 后返回。 +QString fillTokens(const QString& tmpl); + +// 应用一段 {{token}} 模板 QSS 到 widget,并随主题切换自动重填。 +void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl); + } // namespace geopro::app From 425e17e6afe9b8acb71b418ebe3570d62294de1e Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 15:22:32 +0800 Subject: [PATCH 39/56] =?UTF-8?q?feat(canvas):=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=B8=B8=E6=B7=B1=20#0B1320=EF=BC=88?= =?UTF-8?q?=E8=A7=84=E8=8C=83=C2=A70.5/=C2=A711=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/CentralScene.cpp | 2 +- src/app/Theme.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/CentralScene.cpp b/src/app/CentralScene.cpp index a80e951..faefd85 100644 --- a/src/app/CentralScene.cpp +++ b/src/app/CentralScene.cpp @@ -20,7 +20,7 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer, scene.clear(); const bool is2D = (mode == ViewMode::Map2D); (void)is2D; - // 背景随主题(取 ElaTheme 窗口底色),暗色下不再是刺眼白底。 + // 背景永远深色(规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。 double bgR, bgG, bgB; geopro::app::vtkBackground(bgR, bgG, bgB); renderer->SetBackground(bgR, bgG, bgB); diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 3a8b0e1..e052f7f 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -668,7 +668,8 @@ QString themed(const QString& designQss) void vtkBackground(double& r, double& g, double& b) { - const QColor c = roleColor(isDarkTheme(), "#F4F6FA"); + // 规范 §0.5/§11:数据画布永远深色,不随明暗切换。取 canvas/bg。 + const QColor c = tokenColor("canvas/bg"); // #0B1320 r = c.redF(); g = c.greenF(); b = c.blueF(); From b242240df6f0c7254ba9a40e85f9376dede4050a Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 15:29:07 +0800 Subject: [PATCH 40/56] =?UTF-8?q?feat(theme):=20=E5=85=A8=E5=B1=80=20QSS?= =?UTF-8?q?=20=E6=A8=A1=E6=9D=BF=E5=8C=96=20+=20palette=20=E4=BB=8E?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=EF=BC=8C=E6=A0=87=E5=87=86=E6=8E=A7=E4=BB=B6?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E8=A7=84=E8=8C=83=E8=89=B2=E5=80=BC=EF=BC=88?= =?UTF-8?q?=C2=A71/=C2=A73/=C2=A76/=C2=A77=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/Theme.cpp | 219 +++++++++++++++++++++++----------------------- 1 file changed, 110 insertions(+), 109 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index e052f7f..0a15e91 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -88,57 +88,57 @@ QString tokenHex(const char* name, bool dark) // 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。 // 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写, // 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制, -// 它会自动采用调色板的 Highlight(#2D6CB5) 作勾选色,省去打包图片资源。 +// 它会自动采用调色板的 Highlight(accent/primary) 作勾选色,省去打包图片资源。 const char* kStyleSheet = R"QSS( /* ── 基础 ───────────────────────────────────────────────── */ QWidget { - color: #1F2A3D; + color: {{text/primary}}; } QMainWindow, QDialog { - background: #F4F6FA; + background: {{bg/app}}; } /* QToolTip 不写 QSS:用系统原生工具提示(自定义 QSS 会让弹窗圆角露直角、且不像原生)。 */ /* ── 视图内工具条(2D/3D、数据详情):白底分段控件,柔和不刺眼 ── */ QToolBar { - background: #FFFFFF; + background: {{bg/panel}}; border: none; - border-bottom: 1px solid #EAEEF4; + border-bottom: 1px solid {{divider}}; padding: 6px 8px; spacing: 4px; } QToolBar QToolButton { background: transparent; - color: #5A6B85; + color: {{text/secondary}}; border: none; border-radius: 8px; padding: 6px 14px; font-weight: 500; } QToolBar QToolButton:hover { - background: #EEF3FB; - color: #1F2A3D; + background: {{bg/hover}}; + color: {{text/primary}}; } QToolBar QToolButton:pressed { - background: #DCE9F8; + background: {{bg/selected}}; } QToolBar QToolButton:checked { - background: #EAF1FB; - color: #2D6CB5; + background: {{bg/hover}}; + color: {{accent/primary}}; font-weight: 600; } QToolBar QToolButton:checked:hover { - background: #DCE9F8; + background: {{bg/selected}}; } QToolBar::separator { - background: #EAEEF4; + background: {{divider}}; width: 1px; margin: 6px 8px; } /* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */ QTreeWidget, QListWidget, QTreeView, QListView { - background: #FFFFFF; + background: {{bg/panel}}; border: none; padding: 6px; outline: none; @@ -148,22 +148,22 @@ QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item { } QTreeWidget::item:hover, QListWidget::item:hover, QTreeView::item:hover, QListView::item:hover { - background: #EEF3FB; + background: {{bg/hover}}; } QTreeWidget::item:selected, QListWidget::item:selected, QTreeView::item:selected, QListView::item:selected { - background: #DCE9F8; - color: #1B3D67; + background: {{bg/selected}}; + color: {{text/primary}}; } /* 注意:不要给 QTreeView::branch 设 background——一旦改写 branch,Qt 会停止绘制 默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */ /* 表头(对象显示栏) */ QHeaderView::section { - background: #EDF1F7; - color: #3A475C; + background: {{bg/hover}}; + color: {{text/secondary}}; border: none; - border-bottom: 1px solid #D5DBE5; + border-bottom: 1px solid {{border/default}}; padding: 6px 8px; font-weight: 600; } @@ -171,82 +171,82 @@ QHeaderView::section { /* ── 标签页(数据 / 文件):现代下划线 tab,无边框盒子 ──────── */ QTabWidget::pane { border: none; - border-top: 1px solid #EAEEF4; + border-top: 1px solid {{divider}}; top: 0; - background: #FFFFFF; + background: {{bg/panel}}; } QTabBar { background: transparent; } QTabBar::tab { background: transparent; - color: #5A6B85; + color: {{text/secondary}}; border: none; border-bottom: 2px solid transparent; padding: 8px 16px; margin-right: 4px; } QTabBar::tab:selected { - color: #2D6CB5; - border-bottom: 2px solid #2D6CB5; + color: {{accent/primary}}; + border-bottom: 2px solid {{accent/primary}}; font-weight: 600; } QTabBar::tab:hover:!selected { - color: #1F2A3D; + color: {{text/primary}}; } /* ── 复选框(仅调间距/字色,indicator 交给 Fusion 原生)──── */ QCheckBox { spacing: 7px; - color: #1F2A3D; + color: {{text/primary}}; } QCheckBox:disabled { - color: #9AA6B6; + color: {{text/disabled}}; } /* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */ QPushButton { - background: #FFFFFF; - color: #1F2A3D; - border: 1px solid #C2CCDA; + background: {{bg/panel}}; + color: {{text/primary}}; + border: 1px solid {{border/strong}}; border-radius: 6px; padding: 6px 14px; } QPushButton:hover { - background: #EEF3FB; - border-color: #2D6CB5; + background: {{bg/hover}}; + border-color: {{accent/primary}}; } QPushButton:pressed { - background: #DCE9F8; + background: {{bg/selected}}; } QPushButton:default { - background: #2D6CB5; - color: #FFFFFF; - border-color: #2D6CB5; + background: {{accent/primary}}; + color: {{text/on-primary}}; + border-color: {{accent/primary}}; } QPushButton:default:hover { - background: #2862A6; + background: {{accent/primary-hover}}; } QPushButton:disabled { - background: #F0F2F6; - color: #9AA6B6; - border-color: #DCE0E7; + background: {{bg/app}}; + color: {{text/disabled}}; + border-color: {{border/default}}; } QLineEdit { - background: #FFFFFF; - color: #1F2A3D; - border: 1px solid #C7D2E0; + background: {{bg/panel}}; + color: {{text/primary}}; + border: 1px solid {{border/strong}}; border-radius: 6px; padding: 6px 8px; - selection-background-color: #2D6CB5; - selection-color: #FFFFFF; + selection-background-color: {{accent/primary}}; + selection-color: {{text/on-primary}}; } QLineEdit:focus { - border: 1px solid #2D6CB5; + border: 1px solid {{accent/primary}}; } QLineEdit:disabled { - background: #F0F2F6; - color: #8A93A3; + background: {{bg/app}}; + color: {{text/disabled}}; } /* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */ @@ -256,12 +256,12 @@ QScrollBar:vertical { margin: 2px; } QScrollBar::handle:vertical { - background: #C2CCDA; + background: {{scrollbar/thumb}}; border-radius: 6px; min-height: 28px; } QScrollBar::handle:vertical:hover { - background: #A7B4C7; + background: {{scrollbar/thumb-hover}}; } QScrollBar:horizontal { background: transparent; @@ -269,12 +269,12 @@ QScrollBar:horizontal { margin: 2px; } QScrollBar::handle:horizontal { - background: #C2CCDA; + background: {{scrollbar/thumb}}; border-radius: 6px; min-width: 28px; } QScrollBar::handle:horizontal:hover { - background: #A7B4C7; + background: {{scrollbar/thumb-hover}}; } QScrollBar::add-line, QScrollBar::sub-line { width: 0; @@ -286,38 +286,38 @@ QScrollBar::add-page, QScrollBar::sub-page { /* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */ QSplitter::handle { - background: #EAEEF4; + background: {{divider}}; } QSplitter::handle:hover { - background: #C7D2E0; + background: {{accent/primary}}; } ads--CDockSplitter::handle { - background: #EAEEF4; + background: {{divider}}; } ads--CDockSplitter::handle:hover { - background: #C7D2E0; + background: {{accent/primary}}; } /* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */ QStatusBar { - background: #FFFFFF; - color: #5A6B85; - border-top: 1px solid #EAEEF4; + background: {{bg/panel}}; + color: {{text/secondary}}; + border-top: 1px solid {{divider}}; } QStatusBar::item { border: none; } QStatusBar QLabel { - color: #5A6B85; + color: {{text/secondary}}; padding: 0 4px; } /* ── 菜单栏 / 菜单(标准 QMenuBar/QMenu):刻意不设 border-radius——弹窗圆角靠系统(Win11 原生圆角),QSS 设圆角会露出后面的直角。仅设底/字/选中,干净不刺眼。 */ QMenuBar { - background: #FFFFFF; - color: #1F2A3D; - border-bottom: 1px solid #EEF1F5; + background: {{bg/panel}}; + color: {{text/primary}}; + border-bottom: 1px solid {{divider}}; padding: 2px 6px; } QMenuBar::item { @@ -326,16 +326,16 @@ QMenuBar::item { border-radius: 6px; } QMenuBar::item:selected { - background: #EAF1FB; - color: #2D6CB5; + background: {{bg/hover}}; + color: {{accent/primary}}; } QMenuBar::item:pressed { - background: #DCE6F4; + background: {{bg/selected}}; } QMenu { - background: #FFFFFF; - color: #1F2A3D; - border: 1px solid #D5DBE5; + background: {{bg/panel}}; + color: {{text/primary}}; + border: 1px solid {{border/default}}; padding: 4px; } QMenu::item { @@ -343,37 +343,37 @@ QMenu::item { border-radius: 6px; } QMenu::item:selected { - background: #EAF1FB; - color: #2D6CB5; + background: {{bg/hover}}; + color: {{accent/primary}}; } QMenu::separator { height: 1px; - background: #E1E6EE; + background: {{divider}}; margin: 4px 8px; } /* ── 下拉框(按需出现时也与主题一致)──────────────────────── */ QComboBox { - background: #FFFFFF; - color: #1F2A3D; - border: 1px solid #C2CCDA; + background: {{bg/panel}}; + color: {{text/primary}}; + border: 1px solid {{border/strong}}; border-radius: 6px; padding: 6px 10px; min-height: 18px; } QComboBox:hover { - border-color: #2D6CB5; + border-color: {{accent/primary}}; } QComboBox:focus { - border-color: #2D6CB5; + border-color: {{accent/primary}}; } QComboBox::drop-down { border: none; width: 22px; } QComboBox QAbstractItemView { - background: #FFFFFF; - border: 1px solid #D5DBE5; + background: {{bg/panel}}; + border: 1px solid {{border/default}}; outline: none; padding: 2px; } @@ -381,16 +381,16 @@ QComboBox QAbstractItemView::item { border: none; padding: 6px 10px; min-height: 20px; - color: #1F2A3D; + color: {{text/primary}}; } QComboBox QAbstractItemView::item:selected { - background: #EAF1FB; - color: #2D6CB5; + background: {{bg/hover}}; + color: {{accent/primary}}; } /* ── 分组框(按需出现时也与主题一致)──────────────────────── */ QGroupBox { - border: 1px solid #D5DBE5; + border: 1px solid {{border/default}}; border-radius: 8px; margin-top: 10px; padding-top: 6px; @@ -400,20 +400,20 @@ QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 4px; - color: #3A475C; + color: {{text/secondary}}; } /* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */ QProgressBar { - background: #E6EBF3; + background: {{divider}}; border: none; border-radius: 6px; height: 8px; text-align: center; - color: #5A6B85; + color: {{text/secondary}}; } QProgressBar::chunk { - background: #2D6CB5; + background: {{accent/primary}}; border-radius: 6px; } @@ -421,30 +421,30 @@ QProgressBar::chunk { 面板已锁定(无关闭/浮动/拖动),每个区只一个标签即表头:统一浅色头 + 蓝色下划线强调 + 深色加粗标题。各区一致,读起来像固定子窗口表头。 */ ads--CDockAreaWidget { - background: #F4F6FA; + background: {{bg/app}}; } ads--CDockAreaTitleBar { - background: #EDF1F7; - border-bottom: 1px solid #D5DBE5; + background: {{bg/hover}}; + border-bottom: 1px solid {{border/default}}; padding: 0; } ads--CDockWidgetTab { - background: #EDF1F7; + background: {{bg/hover}}; border: none; border-bottom: 2px solid transparent; padding: 7px 12px; min-height: 22px; } ads--CDockWidgetTab[activeTab="true"] { - background: #EDF1F7; - border-bottom: 2px solid #2D6CB5; + background: {{bg/hover}}; + border-bottom: 2px solid {{accent/primary}}; } ads--CDockWidgetTab QLabel { - color: #5A6B85; + color: {{text/secondary}}; font-weight: 600; } ads--CDockWidgetTab[activeTab="true"] QLabel { - color: #1F2A3D; + color: {{text/primary}}; font-weight: 600; } )QSS"; @@ -504,34 +504,35 @@ QString themedQss(const QString& designQss, bool dark) } // 当前模式的全局 QSS。 -QString styleSheetForMode(bool dark) +QString styleSheetForMode(bool /*dark*/) { - return themedQss(QString::fromUtf8(kStyleSheet), dark); + return fillTokens(QString::fromUtf8(kStyleSheet)); } // 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。 QPalette buildPalette(bool dark) { QPalette p; - 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"); + const QColor shell = QColor(tokenHex("bg/app", dark)); + const QColor panel = QColor(tokenHex("bg/panel", dark)); + const QColor text = QColor(tokenHex("text/primary", dark)); + const QColor muted = QColor(tokenHex("text/secondary", dark)); + const QColor accent = QColor(tokenHex("accent/primary", dark)); + const QColor border = QColor(tokenHex("border/default", dark)); + const QColor disabled = QColor(tokenHex("text/disabled", dark)); + const QColor hoverBg = QColor(tokenHex("bg/hover", dark)); p.setColor(QPalette::Window, shell); p.setColor(QPalette::WindowText, text); p.setColor(QPalette::Base, panel); - p.setColor(QPalette::AlternateBase, roleColor(dark, "#EDF1F7")); + p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark))); p.setColor(QPalette::Text, text); - p.setColor(QPalette::Button, roleColor(dark, "#EDF1F7")); + p.setColor(QPalette::Button, hoverBg); p.setColor(QPalette::ButtonText, text); p.setColor(QPalette::ToolTipBase, text); - p.setColor(QPalette::ToolTipText, shell); + p.setColor(QPalette::ToolTipText, panel); p.setColor(QPalette::Highlight, accent); - p.setColor(QPalette::HighlightedText, panel); + p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark))); p.setColor(QPalette::PlaceholderText, muted); p.setColor(QPalette::Link, accent); // Fusion 的明暗 3D 角色统一压成边框色,立体效果塌成平面(去斜角/凹槽)。 From 58cabc63508772cc0493cb854d4ab89d08fabaad Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 15:52:45 +0800 Subject: [PATCH 41/56] =?UTF-8?q?fix(view):=202D/3D=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E8=A1=A8=E5=A4=B4(=E5=88=86=E6=AE=B5?= =?UTF-8?q?=E9=A1=B5=E7=AD=BE)=20+=20=E7=94=BB=E5=B8=83=E7=A9=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=8F=90=E7=A4=BA=E7=94=A8=20canvas=20=E4=BB=A4?= =?UTF-8?q?=E7=89=8C=E8=9E=8D=E5=85=A5=E6=B7=B1=E5=BA=95=EF=BC=88=E8=A7=84?= =?UTF-8?q?=E8=8C=83=C2=A75/=C2=A76.5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/PanelHeader.cpp | 37 +++++++++++++++++++++++++++++++++ src/app/PanelHeader.hpp | 13 ++++++++++++ src/app/main.cpp | 46 ++++++++++++++++------------------------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 844f2f8..ec20fc5 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -165,4 +165,41 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
& segments, + const QVector& actions) +{ + auto* header = new QWidget(); + header->setObjectName(QStringLiteral("panelHeader")); + header->setFixedHeight(kHeaderHeight); + geopro::app::applyThemedStyleSheet(header, headerQss()); + + auto* hlay = new QHBoxLayout(header); + hlay->setContentsMargins(10, 0, 8, 0); + hlay->setSpacing(2); + + auto* group = new QButtonGroup(header); + group->setExclusive(true); + + SegmentedHeader result; + result.header = header; + + for (int i = 0; i < segments.size(); ++i) { + auto* btn = new QToolButton(header); // 与异常/属性页签统一: tabBtn 样式 + 强调色下划线 + btn->setObjectName(QStringLiteral("tabBtn")); + btn->setText(segments[i]); + btn->setCheckable(true); + btn->setCursor(Qt::PointingHandCursor); + group->addButton(btn, i); + hlay->addWidget(btn); + hlay->addSpacing(10); + result.buttons.append(btn); + } + + hlay->addStretch(); + for (const auto& a : actions) hlay->addWidget(makeActionButton(header, a)); + + if (!result.buttons.isEmpty()) result.buttons[0]->setChecked(true); + return result; +} + } // namespace geopro::app diff --git a/src/app/PanelHeader.hpp b/src/app/PanelHeader.hpp index 2309127..16a5ccb 100644 --- a/src/app/PanelHeader.hpp +++ b/src/app/PanelHeader.hpp @@ -14,6 +14,7 @@ class QWidget; class QLabel; +class QToolButton; namespace geopro::app { @@ -43,4 +44,16 @@ struct TabbedPanel { TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector& actions = {}); +// 分段切换表头构建结果:表头容器 + 各分段按钮(互斥,首个默认激活,供调用方接 clicked)。 +struct SegmentedHeader { + QWidget* header; + QVector buttons; +}; + +// 构建「分段切换表头」:一行 Tab 风格互斥按钮(与异常/属性页签同款:选中=强调色文字 + 2px +// 强调色下划线)+ 右侧操作按钮。表头底/高度/边框与 buildTabbedPanel 完全一致;内容由调用方 +// 自行 addWidget 到表头下方(不建堆叠,因 2D/3D 共用同一画布部件)。 +SegmentedHeader buildSegmentedHeader(const QVector& segments, + const QVector& actions = {}); + } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index fcd7b63..4270dea 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -334,27 +334,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re .arg(geopro::app::scaledPx(geopro::app::type::kBody)) .arg(geopro::app::type::kWeightSemibold); - // 工具条:「二维地图/三维视图」两个互斥可勾选按钮。默认二维地图。 - auto* viewToolBar = new QWidget(); - auto* viewBarLay = new QHBoxLayout(viewToolBar); - viewBarLay->setContentsMargins(8, 6, 8, 6); - viewBarLay->setSpacing(6); - auto* viewGroup = new QButtonGroup(viewToolBar); - viewGroup->setExclusive(true); - auto* act2D = new QToolButton(viewToolBar); - act2D->setText(QStringLiteral("二维地图")); - act2D->setCheckable(true); - auto* act3D = new QToolButton(viewToolBar); - act3D->setText(QStringLiteral("三维视图")); - act3D->setCheckable(true); - viewGroup->addButton(act2D); - viewGroup->addButton(act3D); - viewBarLay->addWidget(act2D); - viewBarLay->addWidget(act3D); - viewBarLay->addStretch(); - act2D->setChecked(true); // 默认二维地图 - geopro::app::applyThemedStyleSheet(viewToolBar, kBarBtnQss); - centerLayout->addWidget(viewToolBar); + // 「二维地图/三维视图」分段切换表头:与「异常/属性」面板表头同款(42px 表头底 + 强调色下划线页签)。 + auto seg = geopro::app::buildSegmentedHeader( + {QStringLiteral("二维地图"), QStringLiteral("三维视图")}, + {{geopro::app::Glyph::Collapse, QStringLiteral("折叠")}, + {geopro::app::Glyph::Download, QStringLiteral("导出")}}); + auto* viewHeader = seg.header; + auto* act2D = seg.buttons[0]; + auto* act3D = seg.buttons[1]; + centerLayout->addWidget(viewHeader); centerLayout->addWidget(vtkWidget, 1); // ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。 @@ -420,13 +408,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* esIcon = new QLabel(emptyState); esIcon->setPixmap( - geopro::app::makeGlyph(geopro::app::Glyph::Dataset, QColor("#C2CCDA"), 56).pixmap(56, 56)); + geopro::app::makeGlyph(geopro::app::Glyph::Dataset, + geopro::app::tokenColor("canvas/text-dim"), 56) + .pixmap(56, 56)); esIcon->setAlignment(Qt::AlignCenter); auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState); esTitle->setAlignment(Qt::AlignCenter); - geopro::app::applyThemedStyleSheet( - esTitle, QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") + geopro::app::applyTokenizedStyleSheet( + esTitle, QStringLiteral("color:{{canvas/text}}; font-size:%1px; font-weight:%2;") .arg(geopro::app::scaledPx(geopro::app::type::kHeading)) .arg(geopro::app::type::kWeightSemibold)); @@ -434,9 +424,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "切到「三维视图」可叠加帘面、体素与地形图层"), emptyState); esHint->setAlignment(Qt::AlignCenter); - geopro::app::applyThemedStyleSheet( + geopro::app::applyTokenizedStyleSheet( esHint, - QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody))); + QStringLiteral("color:{{canvas/text-dim}}; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody))); esLay->addWidget(esIcon); esLay->addWidget(esTitle); @@ -827,9 +817,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); // 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。 - auto showLayerPanel = [layerPanel, viewToolBar](bool show3D) { + auto showLayerPanel = [layerPanel, viewHeader](bool show3D) { if (show3D) { - layerPanel->move(14, viewToolBar->height() + 12); + layerPanel->move(14, viewHeader->height() + 12); layerPanel->adjustSize(); layerPanel->setVisible(true); layerPanel->raise(); From 8eb0c7413d94db3beb0801dfad97285fe22ecbf1 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:03:21 +0800 Subject: [PATCH 42/56] =?UTF-8?q?fix(canvas):=20=E7=A9=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=8E=BB=E4=B8=8D=E9=80=8F=E6=98=8E=E5=8D=A1=E7=89=87?= =?UTF-8?q?(=E7=A7=BB=E9=99=A4OpacityEffect)=20+=20=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E8=BD=AC=E6=B7=B1=E8=89=B2=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E6=B5=AE=E5=B1=82=EF=BC=88=E8=A7=84=E8=8C=83=C2=A76.5/=C2=A77.?= =?UTF-8?q?11=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 4270dea..c83497f 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -349,19 +349,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。 auto* layerPanel = new QFrame(centerWidget); layerPanel->setFrameShape(QFrame::StyledPanel); - geopro::app::applyThemedStyleSheet( + geopro::app::applyTokenizedStyleSheet( layerPanel, - QStringLiteral("QFrame{background:#FFFFFF;border:1px solid #D5DBE5;border-radius:8px;}" - "QCheckBox{padding:2px 1px;color:#1F2A3D;}" - "QCheckBox:disabled{color:#9AA6B6;}")); + QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};border-radius:8px;}" + "QCheckBox{padding:2px 1px;color:{{canvas/text}};}" + "QCheckBox:disabled{color:{{canvas/text-dim}};}")); auto* layerLayout = new QVBoxLayout(layerPanel); // 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。 layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl, geopro::app::space::kLg, geopro::app::space::kMl); layerLayout->setSpacing(geopro::app::space::kSm); auto* layerTitle = new QLabel(QStringLiteral("视图详情")); - geopro::app::applyThemedStyleSheet( - layerTitle, QStringLiteral("font-weight:%1;color:#2D6CB5;border:none;background:transparent;" + geopro::app::applyTokenizedStyleSheet( + layerTitle, QStringLiteral("font-weight:%1;color:{{canvas/text}};border:none;background:transparent;" "padding-bottom:3px;font-size:%2px;") .arg(geopro::app::type::kWeightSemibold) .arg(geopro::app::scaledPx(geopro::app::type::kTitle))); @@ -397,6 +397,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* emptyState = new QFrame(centerWidget); emptyState->setObjectName(QStringLiteral("centralEmpty")); emptyState->setAttribute(Qt::WA_TransparentForMouseEvents); + emptyState->setAttribute(Qt::WA_TranslucentBackground); // 透明浮于深色画布(不让控件填充不透明底) emptyState->setStyleSheet(QStringLiteral( "#centralEmpty { background: transparent; }" "#centralEmpty QLabel { background: transparent; }")); @@ -435,16 +436,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget); emptyCentering->reposition(); - // 引导层淡入(350ms,仅透明度,OutCubic):首屏空态出现的克制过渡,不阻塞任务。 - auto* esFx = new QGraphicsOpacityEffect(emptyState); - emptyState->setGraphicsEffect(esFx); - auto* esAnim = new QPropertyAnimation(esFx, "opacity", emptyState); - esAnim->setDuration(350); - esAnim->setStartValue(0.0); - esAnim->setEndValue(1.0); - esAnim->setEasingCurve(QEasingCurve::OutCubic); - esAnim->start(QAbstractAnimation::DeleteWhenStopped); - auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图")); vtkDock->setWidget(centerWidget); auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock); From e60bdbc150358b7350685e75b5e9fd8a8c760de7 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:08:31 +0800 Subject: [PATCH 43/56] =?UTF-8?q?fix(canvas):=20=E7=A9=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=8D=A1=E7=89=87=E7=94=A8=20canvas/bg=20=E7=AD=89?= =?UTF-8?q?=E8=89=B2=E6=97=A0=E7=BC=9D=E5=BA=95(=E5=8E=9F=E7=94=9FGL?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E9=80=8F=E6=98=8E=E5=A4=B1=E6=95=88=E7=9A=84?= =?UTF-8?q?=E5=8F=AF=E9=9D=A0=E8=A7=A3=E6=B3=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index c83497f..043e289 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -397,10 +397,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* emptyState = new QFrame(centerWidget); emptyState->setObjectName(QStringLiteral("centralEmpty")); emptyState->setAttribute(Qt::WA_TransparentForMouseEvents); - emptyState->setAttribute(Qt::WA_TranslucentBackground); // 透明浮于深色画布(不让控件填充不透明底) - emptyState->setStyleSheet(QStringLiteral( - "#centralEmpty { background: transparent; }" - "#centralEmpty QLabel { background: transparent; }")); + // 背景取 canvas/bg(#0B1320)——与画布同色:在原生 GL 上覆盖透明无法生效(会回退成不透明浅底), + // 故用与画布等色的不透明底,卡片即「无缝隐形」,浅色提示字稳稳浮于深底(与左上视图详情浮层同法)。 + geopro::app::applyTokenizedStyleSheet( + emptyState, QStringLiteral("#centralEmpty { background: {{canvas/bg}}; }" + "#centralEmpty QLabel { background: transparent; }")); auto* esLay = new QVBoxLayout(emptyState); esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl, geopro::app::space::kXl, geopro::app::space::kXl); From b2ec3459c7360ed554bef15c159ad8f825209f78 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:14:51 +0800 Subject: [PATCH 44/56] =?UTF-8?q?fix(canvas):=20=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=B5=AE=E5=B1=82=E5=8E=BB=E5=9C=86=E8=A7=92?= =?UTF-8?q?(=E7=9B=B4=E8=A7=92)=E6=B6=88=E9=99=A4=E6=B5=85=E8=89=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=9B=9B=E7=99=BD=E8=A7=92=E4=BC=AA=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 043e289..478a4d1 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -351,7 +351,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re layerPanel->setFrameShape(QFrame::StyledPanel); geopro::app::applyTokenizedStyleSheet( layerPanel, - QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};border-radius:8px;}" + // 不设 border-radius:浮层是 centerWidget 的子控件,悬于原生 GL 画布上,圆角四角处会 + // 露出父级浅底(浅色模式下即四个白直角)。改为直角矩形,不透明底铺满整块,无四角伪影。 + QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};}" "QCheckBox{padding:2px 1px;color:{{canvas/text}};}" "QCheckBox:disabled{color:{{canvas/text-dim}};}")); auto* layerLayout = new QVBoxLayout(layerPanel); From b728631477886f05bc56ca9ccfba999b44b3638f Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:21:49 +0800 Subject: [PATCH 45/56] =?UTF-8?q?refactor(theme):=20TopBar/PanelHeader=20?= =?UTF-8?q?=E5=86=85=E8=81=94=E6=A0=B7=E5=BC=8F=E8=BF=81=E7=A7=BB=E5=88=B0?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E4=BB=A4=E7=89=8C=EF=BC=88=E8=A7=84=E8=8C=83?= =?UTF-8?q?=C2=A74.3/=C2=A75=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/PanelHeader.cpp | 36 +++++++++++++++++------------------- src/app/TopBar.cpp | 20 ++++++++++---------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index ec20fc5..e45939f 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -31,25 +31,23 @@ constexpr int kTabIcon = 19; // Tab 图标 QString headerQss() { return QStringLiteral( - "#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }" - "#panelTitle { color:#1F2A3D; font-size:%1px; font-weight:%3; }" - "#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;" + "#panelHeader { background:{{bg/panel}}; border-bottom:1px solid {{divider}}; }" + "#panelTitle { color:{{text/primary}}; font-size:%1px; font-weight:%3; }" + "#panelBadge { background:{{bg/hover}}; color:{{text/secondary}}; border-radius:9px;" " padding:1px 7px; font-size:%2px; font-weight:%3; }" - "#panelBadgeWarn { background:%4; color:%5; border-radius:9px;" + "#panelBadgeWarn { background:{{status/warning-bg}}; color:{{status/warning}}; border-radius:9px;" " padding:1px 7px; font-size:%2px; font-weight:%3; }" "QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }" - "QToolButton#panelAction: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; }" - "QToolButton#tabBtn:checked { color:#2D6CB5; font-weight:%3;" - " border-bottom:2px solid #2D6CB5; }") - .arg(scaledPx(type::kTitle)) - .arg(scaledPx(type::kCaption)) - .arg(type::kWeightSemibold) - .arg(QString::fromUtf8(semantic::kWarningFill)) - .arg(QString::fromUtf8(semantic::kWarning)) - .arg(scaledPx(type::kBody)); + "QToolButton#panelAction:hover { background:{{bg/hover}}; }" + "QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:{{text/secondary}};" + " padding:8px 6px; font-size:%4px; }" + "QToolButton#tabBtn:hover { color:{{text/primary}}; }" + "QToolButton#tabBtn:checked { color:{{accent/primary}}; font-weight:%3;" + " border-bottom:2px solid {{accent/primary}}; }") + .arg(scaledPx(type::kTitle)) // %1 标题字号 + .arg(scaledPx(type::kCaption)) // %2 徽标字号 + .arg(type::kWeightSemibold) // %3 字重(多处) + .arg(scaledPx(type::kBody)); // %4 页签字号 } // 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。 @@ -83,7 +81,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - geopro::app::applyThemedStyleSheet(header, headerQss()); + geopro::app::applyTokenizedStyleSheet(header, headerQss()); auto* lay = new QHBoxLayout(header); lay->setContentsMargins(12, 0, 8, 0); @@ -116,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - geopro::app::applyThemedStyleSheet(header, headerQss()); + geopro::app::applyTokenizedStyleSheet(header, headerQss()); auto* hlay = new QHBoxLayout(header); hlay->setContentsMargins(10, 0, 8, 0); hlay->setSpacing(2); @@ -171,7 +169,7 @@ SegmentedHeader buildSegmentedHeader(const QVector& segments, auto* header = new QWidget(); header->setObjectName(QStringLiteral("panelHeader")); header->setFixedHeight(kHeaderHeight); - geopro::app::applyThemedStyleSheet(header, headerQss()); + geopro::app::applyTokenizedStyleSheet(header, headerQss()); auto* hlay = new QHBoxLayout(header); hlay->setContentsMargins(10, 0, 8, 0); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 5599dd4..218de0e 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -131,21 +131,21 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、 // 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。 // 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent,不再写它们的 QSS。 - // 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 white 关键字(恒白)。 - geopro::app::applyThemedStyleSheet( + // 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 {{text/on-primary}} 令牌。 + geopro::app::applyTokenizedStyleSheet( this, QStringLiteral( - "#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }" - "#topDivider { color:#E1E6EE; }" + "#appToolBar { background:{{bg/header}}; border-bottom:1px solid {{divider}}; }" + "#topDivider { color:{{divider}}; }" "QToolButton::menu-indicator { image:none; }" - "#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;" + "#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 12px;" " font-size:%6px; font-weight:%4; }" - "#wsSwitcher:hover { background:#EEF3FB; }" + "#wsSwitcher:hover { background:{{bg/hover}}; }" "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;" + "QToolButton#iconBtn:hover { background:{{bg/hover}}; }" + "#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;" " font-size:%1px; }" - "#userName { color:#1F2A3D; font-size:%3px; font-weight:%4; }" - "#userRole { color:#8A93A3; font-size:%5px; }") + "#userName { color:{{text/primary}}; font-size:%3px; font-weight:%4; }" + "#userRole { color:{{text/tertiary}}; font-size:%5px; }") .arg(scaledPx(type::kBody)) .arg(type::kWeightBold) .arg(scaledPx(type::kLabel)) From 824e8bdf62d991584e020ae3831d5ebf67ecf775 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:27:57 +0800 Subject: [PATCH 46/56] =?UTF-8?q?refactor(theme):=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E5=86=85=E8=81=94=E8=89=B2(=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E6=A0=91/=E6=95=B0=E6=8D=AE=E8=AF=A6=E6=83=85=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=9D=A1/=E5=81=9C=E9=9D=A0=E5=88=86=E9=9A=94)?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0=E4=BB=A4=E7=89=8C=EF=BC=88=E8=A7=84?= =?UTF-8?q?=E8=8C=83=C2=A74.2/=C2=A76.1/=C2=A76.12=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 16 ++++++++-------- src/app/panels/ObjectTreePanel.cpp | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 478a4d1..bf6df1c 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -295,9 +295,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto applyDockSplitter = [dockManager, dockBaseQss]() { dockManager->setStyleSheet( dockBaseQss + - geopro::app::themed(QStringLiteral( - "ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }" - "ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"))); + geopro::app::fillTokens(QStringLiteral( + "ads--CDockContainerWidget ads--CDockSplitter::handle { background: {{divider}}; }" + "ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: {{accent/primary}}; }"))); }; applyDockSplitter(); QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, @@ -326,11 +326,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。 const QString kBarBtnQss = QStringLiteral( - "QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:#1F2A3D;" + "QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:{{text/primary}};" " font-size:%1px; }" - "QToolButton:hover{ background:#EEF3FB; }" - "QToolButton:checked{ color:#2D6CB5; font-weight:%2;" - " border-bottom:2px solid #2D6CB5; }") + "QToolButton:hover{ background:{{bg/hover}}; }" + "QToolButton:checked{ color:{{accent/primary}}; font-weight:%2;" + " border-bottom:2px solid {{accent/primary}}; }") .arg(geopro::app::scaledPx(geopro::app::type::kBody)) .arg(geopro::app::type::kWeightSemibold); @@ -503,7 +503,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailBarLay->addWidget(actShowElectrodes); detailBarLay->addWidget(actShowContour); detailBarLay->addStretch(); - geopro::app::applyThemedStyleSheet(detailToolBar, kBarBtnQss); + geopro::app::applyTokenizedStyleSheet(detailToolBar, kBarBtnQss); detailLayout->addWidget(detailToolBar); detailLayout->addWidget(detailWidget, 1); diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 0f96985..bca7f1a 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -44,15 +44,15 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { // 规避 Fusion 原生复选框在浅底下边框过淡、看不清的问题。 auto applyCheckboxStyle = [this]() { 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 = dark ? QColor(0x5E, 0x9B, 0xD6) : QColor(0x2D, 0x6C, 0xB5); // 强调明/暗 + const QColor border = geopro::app::tokenColor("border/strong"); // 未选复选框描边(规范§6.12) + const QColor boxBg = geopro::app::tokenColor("bg/panel"); + const QColor accent = geopro::app::tokenColor("accent/primary"); // 选中填充 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); - // 选中底色与 Ela 列表(BasicSelectedAlpha)保持一致:明=浅蓝、暗=中深蓝;:!active 防失焦变淡。 - const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2"); - const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F"); + // 选中底/字取语义令牌,与全局树/列表选中一致(规范§6.1/§10)。 + const QString selBg = geopro::app::token("bg/selected"); + const QString selFg = geopro::app::token("text/primary"); tree_->setStyleSheet(QStringLiteral("QTreeView::indicator{ width:16px; height:16px; }" "QTreeView::indicator:unchecked{ image:url(%1); }" "QTreeView::indicator:checked{ image:url(%2); }" @@ -68,7 +68,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); hint_->setAlignment(Qt::AlignCenter); - geopro::app::applyThemedStyleSheet(hint_, QStringLiteral("color:#9AA6B6; padding:16px;")); + geopro::app::applyTokenizedStyleSheet(hint_, QStringLiteral("color:{{text/disabled}}; padding:16px;")); hint_->setVisible(false); lay->addWidget(hint_); From b26dcc1ca7ae4ba17f67da6943607f91373f627f Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:40:14 +0800 Subject: [PATCH 47/56] =?UTF-8?q?feat(panels):=20=E6=95=B0=E6=8D=AE/?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=88=97=E8=A1=A8=E5=8D=A1=E7=89=87=E5=8C=96?= =?UTF-8?q?(=E6=A0=87=E9=A2=98+=E5=85=83=E4=BF=A1=E6=81=AF=E5=8F=8C?= =?UTF-8?q?=E8=A1=8C+=E9=80=89=E4=B8=AD=E7=AB=96=E6=9D=A1)=EF=BC=88?= =?UTF-8?q?=E8=A7=84=E8=8C=83=C2=A76.2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 2 + src/app/panels/DatasetListPanel.cpp | 97 +++++++++++++++++++++++++++++ src/app/panels/DatasetListPanel.hpp | 3 + 3 files changed, 102 insertions(+) diff --git a/src/app/main.cpp b/src/app/main.cpp index bf6df1c..9d69a3a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -544,9 +544,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* datasetTabs = new QTabWidget(); auto* datasetList = new QListWidget(); applyListSelection(datasetList); + geopro::app::applyDatasetCardDelegate(datasetList); datasetTabs->addTab(datasetList, QStringLiteral("数据")); auto* fileList = new QListWidget(); applyListSelection(fileList); + geopro::app::applyDatasetCardDelegate(fileList); datasetTabs->addTab(fileList, QStringLiteral("文件")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); auto* datasetBox = wrapWithHeader( diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 2ca911f..1630a16 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -3,7 +3,13 @@ #include #include #include +#include +#include +#include #include +#include + +#include "Theme.hpp" namespace geopro::app { @@ -14,6 +20,88 @@ QString humanSize(long long b) { if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1); return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1); } + +// 数据/文件列表卡片委托:标题+元信息双行、悬停/选中圆角高亮 + 选中左 2px 强调竖条(规范§6.2)。 +// 特殊行(加载更多 / 占位提示)退回为居中纯文本,不画卡片。 +class DatasetCardDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + + QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override { + const bool special = + idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable); + return QSize(0, special ? 34 : 52); + } + + void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override { + p->save(); + p->setRenderHint(QPainter::Antialiasing, true); + const QString disp = idx.data(Qt::DisplayRole).toString(); + + // 「加载更多」:居中强调色文本(hover 时加底)。 + if (idx.data(kDsLoadMoreRole).toBool()) { + if (opt.state & QStyle::State_MouseOver) { + QPainterPath bgp; + bgp.addRoundedRect(opt.rect.adjusted(4, 2, -4, -2), 6, 6); + p->fillPath(bgp, geopro::app::tokenColor("bg/hover")); + } + p->setPen(geopro::app::tokenColor("accent/primary")); + p->drawText(opt.rect, Qt::AlignCenter, disp); + p->restore(); + return; + } + // 占位提示行(不可选):居中淡色文本。 + if (!(idx.flags() & Qt::ItemIsSelectable)) { + p->setPen(geopro::app::tokenColor("text/disabled")); + p->drawText(opt.rect, Qt::AlignCenter, disp); + p->restore(); + return; + } + + // 卡片 + const QRect r = opt.rect.adjusted(4, 2, -4, -2); + const bool selected = opt.state & QStyle::State_Selected; + const bool hover = opt.state & QStyle::State_MouseOver; + if (selected || hover) { + QPainterPath path; + path.addRoundedRect(r, 6, 6); + p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover")); + } + if (selected) { // 左 2px 强调竖条(规范§6.2) + p->fillRect(QRect(r.left(), r.top() + 4, 2, r.height() - 8), + geopro::app::tokenColor("accent/primary")); + } + + QString title = disp, meta; + const int nl = disp.indexOf(QLatin1Char('\n')); + if (nl >= 0) { + title = disp.left(nl); + meta = disp.mid(nl + 1); + } + + const QRect textR = r.adjusted(14, 6, -12, -6); + // 标题 + QFont tf = opt.font; + tf.setPixelSize(geopro::app::scaledPx(13)); + p->setFont(tf); + p->setPen(geopro::app::tokenColor("text/primary")); + const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2); + p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width())); + // 元信息 + if (!meta.isEmpty()) { + QFont mf = opt.font; + mf.setPixelSize(geopro::app::scaledPx(11)); + p->setFont(mf); + p->setPen(geopro::app::tokenColor("text/tertiary")); + const QRect metaR(textR.left(), textR.center().y() + 1, textR.width(), + textR.height() / 2); + p->drawText(metaR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(meta, Qt::ElideRight, metaR.width())); + } + p->restore(); + } +}; } // namespace void populateDatasetList(QListWidget* list, const std::vector& rows, bool append) { @@ -53,4 +141,13 @@ void populateFileList(QListWidget* list, const std::vector& } } +void applyDatasetCardDelegate(QListWidget* list) { + if (!list) return; + list->setItemDelegate(new DatasetCardDelegate(list)); + list->setMouseTracking(true); // 让委托收到 hover 状态 + list->setSpacing(0); // 卡间距由委托内边距控制 + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list, + [list]() { list->viewport()->update(); }); +} + } // namespace geopro::app diff --git a/src/app/panels/DatasetListPanel.hpp b/src/app/panels/DatasetListPanel.hpp index 0356760..8ac2887 100644 --- a/src/app/panels/DatasetListPanel.hpp +++ b/src/app/panels/DatasetListPanel.hpp @@ -18,4 +18,7 @@ void populateDatasetList(QListWidget* list, const std::vector& rows, bool append); +// 给数据/文件列表套用卡片委托(标题+元信息双行、悬停/选中圆角高亮+左强调竖条,规范§6.2)。 +void applyDatasetCardDelegate(QListWidget* list); + } // namespace geopro::app From 8f31f043df0657243b30421a195d9124dcdeff1d Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:44:35 +0800 Subject: [PATCH 48/56] =?UTF-8?q?feat(panels):=20=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8D=A1=E7=89=87=E5=8C=96(=E8=89=B2?= =?UTF-8?q?=E6=9D=A1+=E7=B1=BB=E5=9E=8B=E6=A0=87=E7=AD=BE+=E6=98=BE?= =?UTF-8?q?=E9=9A=90=E7=9C=BC=E7=9D=9B,=E7=9C=9F=E5=AE=9E=E6=95=B0?= =?UTF-8?q?=E6=8D=AE)=EF=BC=88=E8=A7=84=E8=8C=83=C2=A76.3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 1 + src/app/panels/AnomalyListPanel.cpp | 159 +++++++++++++++++++++++++--- src/app/panels/AnomalyListPanel.hpp | 8 ++ 3 files changed, 151 insertions(+), 17 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 9d69a3a..19dfe7a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -563,6 +563,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 auto* anomalyList = new QListWidget(); applyListSelection(anomalyList); + geopro::app::applyAnomalyCardDelegate(anomalyList); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); objAttrLabel->setWordWrap(true); objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); diff --git a/src/app/panels/AnomalyListPanel.cpp b/src/app/panels/AnomalyListPanel.cpp index 6f5fe2d..f2eb677 100644 --- a/src/app/panels/AnomalyListPanel.cpp +++ b/src/app/panels/AnomalyListPanel.cpp @@ -3,22 +3,26 @@ #include #include +#include #include -#include +#include #include #include -#include +#include +#include +#include +#include +#include #include +#include +#include "Theme.hpp" #include "model/ColorScale.hpp" namespace geopro::app { namespace { -// 颜色块图标边长(像素)。 -constexpr int kSwatch = 12; - // 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。 // 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。 QString summarize(const geopro::core::Anomaly& a) @@ -43,15 +47,128 @@ QString summarize(const geopro::core::Anomaly& a) .arg(span, 0, 'f', 0); } -// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap。 -QPixmap swatch(const std::string& colorStr) +// lineColor 字符串 → QColor(兼容 "#RRGGBB" 与 "rgba(...)")。 +QColor barColor(const QString& s) { - const auto c = geopro::core::parseColor(colorStr, geopro::core::AlphaScale::Bit255); - QPixmap pm(kSwatch, kSwatch); - pm.fill(QColor(c.r, c.g, c.b)); - return pm; + const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255); + return QColor(c.r, c.g, c.b); } +// 右侧眼睛命中区(卡片右端,竖直居中)。 +QRect anomalyEyeRect(const QRect& itemRect) +{ + const QRect r = itemRect.adjusted(4, 2, -4, -2); + const int sz = 22; + return QRect(r.right() - sz - 8, r.center().y() - sz / 2, sz, sz); +} + +class AnomalyCardDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + + QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override + { + return QSize(0, 58); + } + + bool editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem& opt, + const QModelIndex& idx) override + { + if (e->type() == QEvent::MouseButtonRelease) { + auto* me = static_cast(e); + if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) { + const auto cur = static_cast(idx.data(Qt::CheckStateRole).toInt()); + model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked, + Qt::CheckStateRole); + return true; // 吃掉点击:只切显隐,不改选中 + } + } + return QStyledItemDelegate::editorEvent(e, model, opt, idx); + } + + void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override + { + p->save(); + p->setRenderHint(QPainter::Antialiasing, true); + + const QRect r = opt.rect.adjusted(4, 3, -4, -3); + const bool selected = opt.state & QStyle::State_Selected; + const bool hover = opt.state & QStyle::State_MouseOver; + + // 卡底(hover/选中高亮) + if (selected || hover) { + QPainterPath path; path.addRoundedRect(r, 6, 6); + p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover")); + } + // 左 3px 状态色竖条(取异常自身 lineColor) + p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8), + barColor(idx.data(kAnomalyColorRole).toString())); + + const QString name = idx.data(Qt::DisplayRole).toString(); + const QString type = idx.data(kAnomalyTypeRole).toString(); + const QString summary = idx.data(kAnomalySummaryRole).toString(); + + const int left = r.left() + 14; + const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位 + const int rowW = right - left; + + // 第一行:名称(加粗) + QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold); + p->setFont(nf); + p->setPen(geopro::app::tokenColor("text/primary")); + const QRect nameR(left, r.top() + 8, rowW, 20); + p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(name, Qt::ElideRight, rowW)); + + // 第二行:类型胶囊 + 摘要 + int x = left; + const int cy = r.top() + 38; + if (!type.isEmpty()) { + QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11)); + p->setFont(pf); + const QFontMetrics fm(pf); + const int tw = fm.horizontalAdvance(type); + const int ph = fm.height() + 2; + const QRect pill(x, cy - ph / 2, tw + 12, ph); + QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0); + p->fillPath(pp, geopro::app::tokenColor("bg/hover")); + p->setPen(geopro::app::tokenColor("text/secondary")); + p->drawText(pill, Qt::AlignCenter, type); + x = pill.right() + 8; + } + if (!summary.isEmpty()) { + QFont sf = opt.font; sf.setPixelSize(geopro::app::scaledPx(11)); + p->setFont(sf); + p->setPen(geopro::app::tokenColor("text/secondary")); + const QRect sumR(x, cy - 10, right - x, 20); + p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter, + p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width())); + } + + // 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠 + const bool visible = + static_cast(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked; + const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled"); + const QRectF eb = anomalyEyeRect(opt.rect); + const QPointF c = eb.center(); + const double w = eb.width() * 0.42, h = eb.height() * 0.24; + p->setPen(QPen(eyeCol, 1.4)); + p->setBrush(Qt::NoBrush); + QPainterPath eye; + eye.moveTo(c.x() - w, c.y()); + eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y()); + eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y()); + p->drawPath(eye); + p->setBrush(eyeCol); + p->drawEllipse(c, h * 0.95, h * 0.95); + p->setBrush(Qt::NoBrush); + if (!visible) + p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6)); + + p->restore(); + } +}; + } // namespace void populateAnomalyList(QListWidget* list, const std::vector& anomalies) @@ -61,16 +178,24 @@ void populateAnomalyList(QListWidget* list, const std::vectorsetData(kAnomalyIndexRole, static_cast(i)); + item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor)); + item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName)); + item->setData(kAnomalySummaryRole, summarize(a)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Checked); // 默认显示 } } +void applyAnomalyCardDelegate(QListWidget* list) +{ + if (!list) return; + list->setItemDelegate(new AnomalyCardDelegate(list)); + list->setMouseTracking(true); + list->setSpacing(0); + QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list, + [list]() { list->viewport()->update(); }); +} + } // namespace geopro::app diff --git a/src/app/panels/AnomalyListPanel.hpp b/src/app/panels/AnomalyListPanel.hpp index e5566ed..ce6e6b2 100644 --- a/src/app/panels/AnomalyListPanel.hpp +++ b/src/app/panels/AnomalyListPanel.hpp @@ -10,6 +10,14 @@ namespace geopro::app { // 异常索引存于条目的 Qt::UserRole(= 在原异常 vector 中的下标,用于显隐映射)。 constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole +// 卡片委托读取的结构化角色(避免把数据塞进显示文本)。 +constexpr int kAnomalyColorRole = 0x0101; // lineColor 字符串 +constexpr int kAnomalyTypeRole = 0x0102; // typeName +constexpr int kAnomalySummaryRole = 0x0103; // 位置·深·尺寸 摘要 + +// 给异常列表套用卡片委托(左色条+名称+类型标签+摘要+右侧显隐眼睛,规范§6.3)。 +void applyAnomalyCardDelegate(QListWidget* list); + // 用异常填充 QListWidget(对齐原型右上「异常列表」):每条目 = 颜色块图标 + 名称 + // 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。 // 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。 From b78969471e771b37d16fc7b8908da78895da684c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 16:57:10 +0800 Subject: [PATCH 49/56] =?UTF-8?q?refactor(theme):=20=E7=99=BB=E5=BD=95/?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E8=BF=81=E7=A7=BB=E4=BB=A4=E7=89=8C=20+=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=81=97=E7=95=99=20kDarkMap=20=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E6=9B=BF=E6=8D=A2=E8=B7=AF=E5=BE=84(?= =?UTF-8?q?=E6=9A=97=E8=89=B2=E5=85=A8=E7=94=B1=E4=BB=A4=E7=89=8C=E5=8F=8C?= =?UTF-8?q?=E5=80=BC=E9=A9=B1=E5=8A=A8)=EF=BC=88=E8=A7=84=E8=8C=83=C2=A713?= =?UTF-8?q?.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/SettingsDialog.cpp | 4 +-- src/app/Theme.cpp | 67 ----------------------------------- src/app/Theme.hpp | 13 ++----- src/app/login/LoginWindow.cpp | 22 ++++++------ 4 files changed, 15 insertions(+), 91 deletions(-) diff --git a/src/app/SettingsDialog.cpp b/src/app/SettingsDialog.cpp index 8ca3537..d10f106 100644 --- a/src/app/SettingsDialog.cpp +++ b/src/app/SettingsDialog.cpp @@ -74,8 +74,8 @@ QWidget* buildAppearancePage() { rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐 rlay->setSpacing(10); auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow); - geopro::app::applyThemedStyleSheet( - hint, QStringLiteral("color:#5A6B85; font-size:%1px;") + geopro::app::applyTokenizedStyleSheet( + hint, QStringLiteral("color:{{text/secondary}}; font-size:%1px;") .arg(geopro::app::scaledPx(geopro::app::type::kCaption))); auto* restartBtn = new QPushButton(QStringLiteral("立即重启"), restartRow); rlay->addWidget(hint); diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 0a15e91..61b8ec3 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -449,60 +449,6 @@ ads--CDockWidgetTab[activeTab="true"] QLabel { } )QSS"; -// ── 主题桥配色:工作台标准控件的 QSS/调色板颜色直接取自 ElaTheme, -// 保证里外(ElaWindow 外壳 ↔ 内部工作台)在明/暗两模下完全一致——这是关键, -// 否则外壳一种灰、工作台另一种灰,明暗都显得割裂。 -// 做法:把浅色设计稿里每个色令牌按语义角色替换为 ElaTheme 当前模式的真实颜色。 -// 浅→暗 颜色映射(全 UI 唯一颜色来源)。明色 = 令牌本身(QSS 直接写的就是明色); -// 暗色 = 此表对应值。覆盖全部 QSS 用色,缺一即在暗色下露浅色。改色只改这一处。 -struct DarkPair { - const char* light; - const char* dark; -}; -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"}, -}; - -QString darkOf(const QString& lightHex) -{ - for (const auto& p : kDarkMap) - if (lightHex.compare(QLatin1String(p.light), Qt::CaseInsensitive) == 0) - return QString::fromLatin1(p.dark); - return lightHex; -} - -// 设计令牌(浅色 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& p : kDarkMap) - s.replace(QString::fromLatin1(p.light), QString::fromLatin1(p.dark), Qt::CaseInsensitive); - return s; -} - // 当前模式的全局 QSS。 QString styleSheetForMode(bool /*dark*/) { @@ -662,11 +608,6 @@ bool isDarkTheme() return ThemeManager::instance().isDark(); } -QString themed(const QString& designQss) -{ - return themedQss(designQss, isDarkTheme()); -} - void vtkBackground(double& r, double& g, double& b) { // 规范 §0.5/§11:数据画布永远深色,不随明暗切换。取 canvas/bg。 @@ -676,14 +617,6 @@ void vtkBackground(double& r, double& g, double& b) b = c.blueF(); } -void applyThemedStyleSheet(QWidget* w, const QString& designQss) -{ - if (!w) return; - w->setStyleSheet(themedQss(designQss, isDarkTheme())); - QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w, - [w, designQss]() { w->setStyleSheet(themedQss(designQss, isDarkTheme())); }); -} - QString token(const char* name) { return tokenHex(name, isDarkTheme()); } QColor tokenColor(const char* name) { return QColor(token(name)); } diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index 9d79c0f..5148b72 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -19,7 +19,7 @@ class QWidget; namespace geopro::app { // 主题管理器(纯 Qt,替代 ElaTheme):持有当前明暗 + 是否跟随系统;切换发 changed() 信号, -// 全 UI(全局 QSS 由 main 重应用、内联 chrome 由 applyThemedStyleSheet)据此热切重着色。 +// 全 UI(全局 QSS 由 main 重应用、内联 chrome 由 applyTokenizedStyleSheet)据此热切重着色。 class ThemeManager : public QObject { Q_OBJECT public: @@ -103,7 +103,7 @@ inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kW } // namespace semantic // 应用专业主题(Fusion + 调色板 + 全局样式表)。dark=true 走暗色(P2 主题桥用)。 -// 暗色复用同一 QSS 结构,仅按 kDarkMap 换色;幂等,可随主题切换重复调用。 +// 暗色复用同一 QSS 结构,颜色全由 kTokens 双值(fillTokens/tokenHex)驱动;幂等,可随主题切换重复调用。 void applyThemeMode(QApplication& app, bool dark); // 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。 @@ -128,15 +128,6 @@ bool isDarkTheme(); // VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b(0–1)。 void vtkBackground(double& r, double& g, double& b); -// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色应用到 widget,并随明/暗切换自动重着色。 -// 用于 TopBar/PanelHeader/浮层 等带内联 setStyleSheet 的自定义 chrome——让它们也跟随主题 -// (设计稿里用浅色令牌 #1F2A3D/#FFFFFF/#2D6CB5… 书写即可,与全局 QSS 同一套角色映射)。 -void applyThemedStyleSheet(QWidget* w, const QString& designQss); - -// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色后返回(供需要拼接/手动 setStyleSheet 的场景, -// 如 ADS dockManager 在其自带样式后追加规则)。不自动随主题切换,调用方需自行在切换时重取。 -QString themed(const QString& designQss); - // ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens;规范 §1.5 + 附录 A + §1.3)── // 组件只引语义 token,禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。 QString token(const char* name); // 当前明暗下的 hex(未知名返回品红 "#FF00FF" 以便一眼发现漏配) diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index abb81de..f042b27 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -89,17 +89,17 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。 // 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。 // 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。 - geopro::app::applyThemedStyleSheet( + geopro::app::applyTokenizedStyleSheet( this, QStringLiteral( - "QDialog { background: #F4F6FA; }" + "QDialog { background: {{bg/app}}; }" "#headerBand {" " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," - " stop:0 #2D6CB5, stop:1 #234F87); }" - "#brandTitle { color: white; font-size: %1px; font-weight: %2; }" + " stop:0 {{accent/primary}}, stop:1 {{accent/primary-pressed}}); }" + "#brandTitle { color: {{text/on-primary}}; font-size: %1px; font-weight: %2; }" "#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }" - "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" + "#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }" // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 - "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") + "#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }") .arg(scaledPx(type::kDisplay)) .arg(type::kWeightBold) .arg(scaledPx(type::kCaption)) @@ -178,11 +178,11 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body); refreshBtn_->setFlat(true); refreshBtn_->setCursor(Qt::PointingHandCursor); - geopro::app::applyThemedStyleSheet( + geopro::app::applyTokenizedStyleSheet( refreshBtn_, QStringLiteral( - "QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }" - "QPushButton:hover { color: #234F87; text-decoration: underline; }")); + "QPushButton { color: {{accent/primary}}; border: none; background: transparent; padding: 2px 0; }" + "QPushButton:hover { color: {{accent/primary-pressed}}; text-decoration: underline; }")); refreshRow->addWidget(refreshBtn_); form->addLayout(refreshRow); @@ -193,8 +193,8 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 错误提示:固定占位高度,避免出现时整体布局跳动。 errorLabel_ = new QLabel(body); - geopro::app::applyThemedStyleSheet( - errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(scaledPx(type::kCaption))); + geopro::app::applyTokenizedStyleSheet( + errorLabel_, QStringLiteral("color: {{status/danger}}; font-size: %1px;").arg(scaledPx(type::kCaption))); errorLabel_->setWordWrap(true); errorLabel_->setMinimumHeight(18); form->addWidget(errorLabel_); From 2a666663e7ad693804501c9d6ebc7c29df7b0860 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:02:03 +0800 Subject: [PATCH 50/56] =?UTF-8?q?refactor(theme):=20=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=AF=B9=E8=AF=9D=E6=A1=86=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=20+=20=E6=B8=85=E7=90=86=E5=A7=94=E6=89=98?= =?UTF-8?q?=E6=8E=A5=E7=AE=A1=E5=90=8E=E5=A4=B1=E6=95=88=E7=9A=84=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=80=89=E4=B8=AD/=E5=89=8D=E6=99=AF=E6=AD=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/ProjectListDialog.cpp | 12 ++++++------ src/app/main.cpp | 21 --------------------- src/app/panels/DatasetListPanel.cpp | 1 - 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/app/ProjectListDialog.cpp b/src/app/ProjectListDialog.cpp index 34bfb9c..950df35 100644 --- a/src/app/ProjectListDialog.cpp +++ b/src/app/ProjectListDialog.cpp @@ -28,11 +28,11 @@ QString statusText(int s) { } // 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。 -const char* statusColorHex(int s) { +QColor statusColor(int s) { switch (s) { - case 1: return "#8A93A3"; // 未开始:弱化 - case 2: return semantic::kInfo; // 进行中:活动中 - default: return "#5A6B85"; // 未知:中性 + case 1: return tokenColor("text/tertiary"); // 未开始:弱化 + case 2: return tokenColor("accent/primary"); // 进行中:活动中 + default: return tokenColor("text/secondary"); // 未知:中性 } } } // namespace @@ -155,12 +155,12 @@ void ProjectListDialog::query() { set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1)); auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name)); nameItem->setData(Qt::UserRole, QString::fromStdString(p.id)); - nameItem->setForeground(QColor("#2D6CB5")); + nameItem->setForeground(tokenColor("accent/primary")); table_->setItem(i, 1, nameItem); set(2, QString::fromStdString(p.code)); // 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。 auto* statusItem = new QTableWidgetItem(statusText(p.status)); - statusItem->setForeground(QColor(statusColorHex(p.status))); + statusItem->setForeground(statusColor(p.status)); if (p.status == 2) { QFont f = statusItem->font(); f.setBold(true); diff --git a/src/app/main.cpp b/src/app/main.cpp index 19dfe7a..f67c86e 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -523,31 +523,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re {{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}})); auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); - // 列表选中色:写死的强调蓝(明 #C2D9F2 / 暗 #33527A)+ 适配文字,:!active 防失焦变淡; - // 与对象树选中色一致。本地 QSS 覆盖全局弱选中色,随主题重设。 - auto applyListSelection = [](QListWidget* lw) { - auto styleIt = [lw]() { - const bool dark = geopro::app::isDarkTheme(); - const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2"); - const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F"); - lw->setStyleSheet(QStringLiteral("QListWidget::item:selected{ background:%1; color:%2; }" - "QListWidget::item:selected:!active{ background:%1;" - " color:%2; }") - .arg(selBg, selFg)); - }; - styleIt(); - QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, - lw, [styleIt]() { styleIt(); }); - }; - // 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。 auto* datasetTabs = new QTabWidget(); auto* datasetList = new QListWidget(); - applyListSelection(datasetList); geopro::app::applyDatasetCardDelegate(datasetList); datasetTabs->addTab(datasetList, QStringLiteral("数据")); auto* fileList = new QListWidget(); - applyListSelection(fileList); geopro::app::applyDatasetCardDelegate(fileList); datasetTabs->addTab(fileList, QStringLiteral("文件")); auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); @@ -562,7 +543,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。 auto* anomalyList = new QListWidget(); - applyListSelection(anomalyList); geopro::app::applyAnomalyCardDelegate(anomalyList); auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)")); objAttrLabel->setWordWrap(true); @@ -899,7 +879,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw); m->setData(geopro::app::kDsLoadMoreRole, true); m->setTextAlignment(Qt::AlignCenter); - m->setForeground(QColor("#2D6CB5")); } return loaded; }; diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index 1630a16..4856a85 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -125,7 +125,6 @@ void populateFileList(QListWidget* list, const std::vector& if (!append && rows.empty()) { auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); hint->setFlags(Qt::NoItemFlags); - hint->setForeground(QColor("#9AA6B6")); hint->setTextAlignment(Qt::AlignCenter); return; } From 5f02d494dc0c24278b9251a301c5f9c4bae929af Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:09:09 +0800 Subject: [PATCH 51/56] =?UTF-8?q?fix(theme):=20=E5=A4=8D=E9=80=89=E6=8C=87?= =?UTF-8?q?=E7=A4=BA=E5=99=A8=E5=85=A8=E5=B1=80=E7=BB=9F=E4=B8=80(?= =?UTF-8?q?=E6=B8=85=E6=99=B0=E5=8F=AF=E8=A7=81,=E8=A7=84=E9=81=BFFusion?= =?UTF-8?q?=E6=B5=85=E5=BA=95=E8=BF=87=E6=B7=A1)=20+=20=E5=A4=B1=E7=84=A6?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E9=80=89=E4=B8=AD,=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E6=A0=91=E6=9C=AC=E5=9C=B0=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/Theme.cpp | 33 +++++++++++++++++++++++++++++- src/app/panels/ObjectTreePanel.cpp | 24 ---------------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index 61b8ec3..5354e2c 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -1,5 +1,7 @@ #include "Theme.hpp" +#include "Glyphs.hpp" + #include #include #include @@ -155,6 +157,11 @@ QTreeView::item:selected, QListView::item:selected { background: {{bg/selected}}; color: {{text/primary}}; } +QTreeWidget::item:selected:!active, QListWidget::item:selected:!active, +QTreeView::item:selected:!active, QListView::item:selected:!active { + background: {{bg/selected}}; + color: {{text/primary}}; +} /* 注意:不要给 QTreeView::branch 设 background——一旦改写 branch,Qt 会停止绘制 默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */ @@ -449,10 +456,34 @@ ads--CDockWidgetTab[activeTab="true"] QLabel { } )QSS"; +// 全局复选指示器:用 writeCheckboxIcon 生成清晰复选框 PNG(未选=明显边框空心框, +// 选中=强调色填充+白勾),统一作用于 QCheckBox 与 树/列表的勾选指示器。规避 Fusion +// 原生复选框在浅底下边框过淡看不清的问题——全 UI 一套,避免逐控件打补丁。 +QString indicatorQss(bool dark) +{ + const QColor border = QColor(tokenHex("border/strong", dark)); + const QColor boxBg = QColor(tokenHex("bg/panel", dark)); + const QColor accent = QColor(tokenHex("accent/primary", dark)); + const QString tag = dark ? QStringLiteral("gd") : QStringLiteral("gl"); // 全局缓存标签 + const QString off = geopro::app::writeCheckboxIcon(false, border, boxBg, Qt::white, tag); + const QString on = geopro::app::writeCheckboxIcon(true, accent, accent, Qt::white, tag); + return QStringLiteral( + "QCheckBox::indicator, QTreeView::indicator, QListView::indicator," + "QTreeWidget::indicator, QListWidget::indicator { width:16px; height:16px; }" + "QCheckBox::indicator:unchecked, QTreeView::indicator:unchecked," + "QListView::indicator:unchecked, QTreeWidget::indicator:unchecked," + "QListWidget::indicator:unchecked { image:url(%1); }" + "QCheckBox::indicator:checked, QTreeView::indicator:checked," + "QListView::indicator:checked, QTreeWidget::indicator:checked," + "QListWidget::indicator:checked { image:url(%2); }") + .arg(off, on); +} + // 当前模式的全局 QSS。 QString styleSheetForMode(bool /*dark*/) { - return fillTokens(QString::fromUtf8(kStyleSheet)); + const bool dark = isDarkTheme(); + return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark); } // 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。 diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index bca7f1a..074d785 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -40,30 +40,6 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { tree_->setHeaderHidden(true); tree_->setIndentation(14); // 收紧缩进 - // 清晰复选框:自绘 PNG(未选=明显边框空心框,选中=强调色底+白勾),明暗各一套、切换重绘。 - // 规避 Fusion 原生复选框在浅底下边框过淡、看不清的问题。 - auto applyCheckboxStyle = [this]() { - const bool dark = geopro::app::isDarkTheme(); - const QColor border = geopro::app::tokenColor("border/strong"); // 未选复选框描边(规范§6.12) - const QColor boxBg = geopro::app::tokenColor("bg/panel"); - const QColor accent = geopro::app::tokenColor("accent/primary"); // 选中填充 - 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); - // 选中底/字取语义令牌,与全局树/列表选中一致(规范§6.1/§10)。 - const QString selBg = geopro::app::token("bg/selected"); - const QString selFg = geopro::app::token("text/primary"); - tree_->setStyleSheet(QStringLiteral("QTreeView::indicator{ width:16px; height:16px; }" - "QTreeView::indicator:unchecked{ image:url(%1); }" - "QTreeView::indicator:checked{ image:url(%2); }" - "QTreeView::item:selected{ background:%3; color:%4; }" - "QTreeView::item:selected:!active{ background:%3; color:%4; }") - .arg(off, on, selBg, selFg)); - }; - applyCheckboxStyle(); - QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, tree_, - [applyCheckboxStyle]() { applyCheckboxStyle(); }); - lay->addWidget(tree_, 1); hint_ = new QLabel(QStringLiteral("正在加载对象…"), this); From 9e80b2fea1cb4c3da781eacaba97400edf3ba088 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:21:36 +0800 Subject: [PATCH 52/56] =?UTF-8?q?feat(ui):=20=E9=9D=A2=E6=9D=BF=E6=94=B9?= =?UTF-8?q?=E5=90=8D(=E5=AF=B9=E8=B1=A1/=E6=95=B0=E6=8D=AE=E9=9B=86/?= =?UTF-8?q?=E5=BC=82=E5=B8=B8/=E6=95=B0=E6=8D=AE=E9=9B=86=E5=B1=9E?= =?UTF-8?q?=E6=80=A7)=20+=20=E5=8E=9F=E6=95=B0=E6=8D=AE/=E7=BD=91=E6=A0=BC?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=94=B9=E4=B8=BA=E4=B8=8B=E5=88=92=E7=BA=BF?= =?UTF-8?q?=E9=A1=B5=E7=AD=BE(=E4=B8=8E=E5=85=B6=E4=BB=96=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E4=B8=80=E8=87=B4)=20+=20=E8=A1=A8=E5=A4=B4=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E9=97=B4=E8=B7=9D=E5=AF=B9=E9=BD=90=E8=A7=84=E8=8C=83?= =?UTF-8?q?6px(=C2=A76.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/PanelHeader.cpp | 2 +- src/app/main.cpp | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index e45939f..f2f7b98 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -85,7 +85,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector
setContentsMargins(12, 0, 8, 0); - lay->setSpacing(8); + lay->setSpacing(geopro::app::space::kSm); auto* iconLbl = new QLabel(header); setThemedGlyph(iconLbl, icon, kTitleIcon); // 随主题着色(暗色下也清晰) diff --git a/src/app/main.cpp b/src/app/main.cpp index f67c86e..5bc18bd 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -330,6 +330,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re " font-size:%1px; }" "QToolButton:hover{ background:{{bg/hover}}; }" "QToolButton:checked{ color:{{accent/primary}}; font-weight:%2;" + " border-bottom:2px solid {{accent/primary}}; }" + "QToolButton#dataTab{ border:none; border-radius:0; background:transparent;" + " border-bottom:2px solid transparent; color:{{text/secondary}}; padding:8px 8px; }" + "QToolButton#dataTab:hover{ color:{{text/primary}}; background:transparent; }" + "QToolButton#dataTab:checked{ color:{{accent/primary}}; font-weight:%2;" " border-bottom:2px solid {{accent/primary}}; }") .arg(geopro::app::scaledPx(geopro::app::type::kBody)) .arg(geopro::app::type::kWeightSemibold); @@ -482,6 +487,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re detailGroup->setExclusive(true); auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true); auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true); + actScatter->setObjectName(QStringLiteral("dataTab")); + actSection->setObjectName(QStringLiteral("dataTab")); detailGroup->addButton(actScatter); detailGroup->addButton(actSection); detailBarLay->addWidget(actScatter); @@ -517,8 +524,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 左上 dock:对象树(真实结构:项目根 → GS → TM)。被动视图,数据由控制器推送。 auto* objectTree = new geopro::app::ObjectTreePanel(); - auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏")); - leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"), + auto* leftDock = new ads::CDockWidget(QStringLiteral("对象")); + leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"), objectTree, {{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}})); auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock); @@ -531,9 +538,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* fileList = new QListWidget(); geopro::app::applyDatasetCardDelegate(fileList); datasetTabs->addTab(fileList, QStringLiteral("文件")); - auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏")); + auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据集")); auto* datasetBox = wrapWithHeader( - geopro::app::Glyph::Dataset, QStringLiteral("数据真实显示栏"), datasetTabs, + geopro::app::Glyph::Dataset, QStringLiteral("数据集"), datasetTabs, {{geopro::app::Glyph::Filter, QStringLiteral("筛选")}, {geopro::app::Glyph::Upload, QStringLiteral("上传")}}); datasetDock->setWidget(datasetBox); @@ -550,7 +557,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re objAttrLabel->setMargin(8); auto anomalyPanel = geopro::app::buildTabbedPanel( - {{geopro::app::Glyph::Anomaly, QStringLiteral("异常列表"), anomalyList, true}, + {{geopro::app::Glyph::Anomaly, QStringLiteral("异常"), anomalyList, true}, {geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}}, {{geopro::app::Glyph::Filter, QStringLiteral("筛选")}, {geopro::app::Glyph::Plus, QStringLiteral("添加异常")}}); @@ -565,7 +572,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re anomalyBadge->style()->polish(anomalyBadge); } - auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性")); + auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性")); rightDock->setWidget(anomalyPanel.container); auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock); @@ -574,9 +581,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re propLabel->setWordWrap(true); propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); propLabel->setMargin(8); - auto* propDock = new ads::CDockWidget(QStringLiteral("属性")); + auto* propDock = new ads::CDockWidget(QStringLiteral("数据集属性")); propDock->setWidget( - wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel)); + wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propLabel)); dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea); // 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。 @@ -928,7 +935,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re objectTree->setStructure(projectName, nodes); datasetList->clear(); fileList->clear(); - if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); + if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集")); datasetTabs->setTabText(0, QStringLiteral("数据")); datasetTabs->setTabText(1, QStringLiteral("文件")); }); @@ -939,7 +946,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re removeLoadMore(datasetList); geopro::app::populateDatasetList(datasetList, rows, append); const int loaded = addLoadMore(datasetList, total); - if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏")); + if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集")); datasetTabs->setTabText( 0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total) : QStringLiteral("数据")); From c953b35334e6672a81832d9ed3fd467027f4faff Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:27:37 +0800 Subject: [PATCH 53/56] =?UTF-8?q?feat(topbar):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=99=A8=E4=B8=8B=E6=8B=89=E7=AE=AD=E5=A4=B4=E6=8D=A2=E9=AB=98?= =?UTF-8?q?=E6=B8=85chevron(=E6=9B=BF=E4=BB=A3=E7=B2=97=E7=B3=99=E2=96=BE)?= =?UTF-8?q?=20+=20=E7=94=A8=E6=88=B7=E5=8C=BA=E5=A4=B4=E5=83=8F/=E5=A7=93?= =?UTF-8?q?=E5=90=8D/=E8=81=8C=E5=8A=A1=E5=90=8C=E8=A1=8C=E4=B8=94?= =?UTF-8?q?=E6=95=B4=E5=9D=97=E5=8F=AF=E7=82=B9=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/TopBar.cpp | 75 +++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 218de0e..539fda5 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -8,10 +8,10 @@ #include #include #include +#include #include #include #include -#include #include #include "Glyphs.hpp" @@ -132,16 +132,23 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 角色名=caption(12)。原 11px 角色名上调到 12,去掉只差 1px 的糊层级。 // 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent,不再写它们的 QSS。 // 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 {{text/on-primary}} 令牌。 + // 切换器下拉箭头:用生成的高清 chevron PNG 作 menu-indicator(替代旧的粗糙文字箭头),中性灰双主题可读。 + const QString chevron = geopro::app::writeChevronIcon(true, QColor("#7C8493")); geopro::app::applyTokenizedStyleSheet( this, QStringLiteral( "#appToolBar { background:{{bg/header}}; border-bottom:1px solid {{divider}}; }" "#topDivider { color:{{divider}}; }" "QToolButton::menu-indicator { image:none; }" - "#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 12px;" + "#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 26px 8px 12px;" " font-size:%6px; font-weight:%4; }" "#wsSwitcher:hover { background:{{bg/hover}}; }" + "#wsSwitcher::menu-indicator { image:url(%7); width:13px; height:13px;" + " subcontrol-position: right center; subcontrol-origin: padding; right:8px; }" "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" "QToolButton#iconBtn:hover { background:{{bg/hover}}; }" + "#userBtn { border:none; border-radius:8px; padding:2px 4px; text-align:left; }" + "#userBtn:hover { background:{{bg/hover}}; }" + "#userBtn::menu-indicator { image:none; }" "#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;" " font-size:%1px; }" "#userName { color:{{text/primary}}; font-size:%3px; font-weight:%4; }" @@ -151,13 +158,14 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { .arg(scaledPx(type::kLabel)) .arg(type::kWeightSemibold) .arg(scaledPx(type::kCaption)) - .arg(scaledPx(type::kTitle))); + .arg(scaledPx(type::kTitle)) + .arg(chevron)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); lay->setSpacing(0); - // 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用文字"▾"保证清晰;数据驱动)。 + // 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用高清 chevron menu-indicator;数据驱动)。 wsBtn_ = new QToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活) @@ -197,31 +205,38 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(12); - // 用户区:头像可点击 → 弹出菜单(退出登录)。 - auto* avatar = new QToolButton(this); + // 用户区:头像 + 姓名 + 职务 同一行,整块可点击(QPushButton + 子布局,子控件鼠标穿透→点击命中按钮)。 + auto* userBtn = new QPushButton(this); + userBtn->setObjectName(QStringLiteral("userBtn")); + userBtn->setCursor(Qt::PointingHandCursor); + userBtn->setFlat(true); + auto* userLay = new QHBoxLayout(userBtn); + userLay->setContentsMargins(6, 2, 10, 2); + userLay->setSpacing(8); + + auto* avatar = new QLabel(QStringLiteral("ZL"), userBtn); avatar->setObjectName(QStringLiteral("avatar")); - avatar->setText(QStringLiteral("ZL")); avatar->setFixedSize(34, 34); - avatar->setCursor(Qt::PointingHandCursor); - avatar->setPopupMode(QToolButton::InstantPopup); - auto* userMenu = new QMenu(avatar); + avatar->setAlignment(Qt::AlignCenter); + avatar->setAttribute(Qt::WA_TransparentForMouseEvents); + userLay->addWidget(avatar); + + auto* userName = new QLabel(QStringLiteral("张磊"), userBtn); + userName->setObjectName(QStringLiteral("userName")); + userName->setAttribute(Qt::WA_TransparentForMouseEvents); + userLay->addWidget(userName); + + auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBtn); + userRole->setObjectName(QStringLiteral("userRole")); + userRole->setAttribute(Qt::WA_TransparentForMouseEvents); + userLay->addWidget(userRole); + + auto* userMenu = new QMenu(userBtn); QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, [this] { emit logoutRequested(); }); - avatar->setMenu(userMenu); - lay->addWidget(avatar); - lay->addSpacing(8); + userBtn->setMenu(userMenu); - auto* userBox = new QWidget(this); - auto* userLay = new QVBoxLayout(userBox); - userLay->setContentsMargins(0, 0, 0, 0); - userLay->setSpacing(0); - auto* userName = new QLabel(QStringLiteral("张磊"), userBox); - userName->setObjectName(QStringLiteral("userName")); - auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBox); - userRole->setObjectName(QStringLiteral("userRole")); - userLay->addWidget(userName); - userLay->addWidget(userRole); - lay->addWidget(userBox); + lay->addWidget(userBtn); } void TopBar::setWorkspaces(const std::vector& list, const QString& currentId) { @@ -241,7 +256,7 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - wsBtn_->setText(name + QStringLiteral(" ▾")); // 立即反馈 + wsBtn_->setText(name); // 立即反馈 emit workspaceSwitchRequested(id); }); } @@ -250,8 +265,7 @@ void TopBar::setWorkspaces(const std::vector& list, const QStri none->setEnabled(false); } wsBtn_->setMenu(menu); - wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) + - QStringLiteral(" ▾")); + wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName); } void TopBar::setProjects(const std::vector& list, const QString& currentId, @@ -272,7 +286,7 @@ void TopBar::setProjects(const std::vector& list, const QS group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { - projBtn_->setText(name + QStringLiteral(" ▾")); + projBtn_->setText(name); emit projectSwitchRequested(id); }); } @@ -286,12 +300,11 @@ void TopBar::setProjects(const std::vector& list, const QS QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); }); } projBtn_->setMenu(menu); - projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) + - QStringLiteral(" ▾")); + projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName); } void TopBar::setProjectButtonText(const QString& name) { - projBtn_->setText(name + QStringLiteral(" ▾")); + projBtn_->setText(name); } } // namespace geopro::app From 3ccb8df4ed47c3c49427e011ff3a06c41d1b1802 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:41:46 +0800 Subject: [PATCH 54/56] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E5=AF=BC=E8=87=B4=E7=9A=84=E5=81=9C=E9=9D=A0=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=A2=E5=A4=B1(bump=20dockState=E9=94=AE=E4=B8=A2?= =?UTF-8?q?=E5=BC=83=E5=A4=B1=E9=85=8D=E6=97=A7=E5=B8=83=E5=B1=80)=20+=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8C=BA=E6=94=B9=E5=9B=9EQToolButton(?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E5=9B=BE=E6=A0=87+=E5=A7=93=E5=90=8D?= =?UTF-8?q?=E8=81=8C=E5=8A=A1=E4=B8=80=E8=A1=8C,=E6=95=B4=E5=9D=97?= =?UTF-8?q?=E5=8F=AF=E7=82=B9)=E4=BF=AE=E5=A4=8D=E6=8C=A4=E6=88=90?= =?UTF-8?q?=E4=B8=80=E5=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/TopBar.cpp | 65 +++++++++++++++++++++++++++------------------- src/app/main.cpp | 6 +++-- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 539fda5..471675d 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -3,12 +3,15 @@ #include #include #include +#include #include #include +#include #include #include #include -#include +#include +#include #include #include #include @@ -50,6 +53,29 @@ QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip) return btn; } +// 圆形头像图标:强调色填充 + 白色缩写。2x 绘制保证高 DPI 清晰。 +QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QColor& fg) +{ + constexpr int kScale = 2; + const int s = px * kScale; + QPixmap pm(s, s); + pm.fill(Qt::transparent); + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing, true); + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawEllipse(0, 0, s, s); + QFont f = p.font(); + f.setPixelSize(static_cast(s * 0.4)); + f.setBold(true); + p.setFont(f); + p.setPen(fg); + p.drawText(QRect(0, 0, s, s), Qt::AlignCenter, initials); + p.end(); + pm.setDevicePixelRatio(kScale); + return pm; +} + // ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)── QMenu* buildViewMenu(QWidget* p) { @@ -146,7 +172,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { " subcontrol-position: right center; subcontrol-origin: padding; right:8px; }" "QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }" "QToolButton#iconBtn:hover { background:{{bg/hover}}; }" - "#userBtn { border:none; border-radius:8px; padding:2px 4px; text-align:left; }" + "#userBtn { border:none; border-radius:8px; padding:4px 10px 4px 6px;" + " color:{{text/primary}}; font-size:%3px; }" "#userBtn:hover { background:{{bg/hover}}; }" "#userBtn::menu-indicator { image:none; }" "#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;" @@ -205,37 +232,21 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(12); - // 用户区:头像 + 姓名 + 职务 同一行,整块可点击(QPushButton + 子布局,子控件鼠标穿透→点击命中按钮)。 - auto* userBtn = new QPushButton(this); + // 用户区:头像(圆形图标) + 姓名·职务 同一行,整块可点击 → 退出登录菜单。 + // 用 QToolButton(图标+文字),而非 QPushButton+子布局——后者按空文本算尺寸会把内容挤成一团。 + auto* userBtn = new QToolButton(this); userBtn->setObjectName(QStringLiteral("userBtn")); + userBtn->setIcon(QIcon(renderAvatar(QStringLiteral("ZL"), 30, + geopro::app::tokenColor("accent/primary"), Qt::white))); + userBtn->setIconSize(QSize(30, 30)); + userBtn->setText(QStringLiteral("张磊 · 高级工程师")); + userBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + userBtn->setPopupMode(QToolButton::InstantPopup); userBtn->setCursor(Qt::PointingHandCursor); - userBtn->setFlat(true); - auto* userLay = new QHBoxLayout(userBtn); - userLay->setContentsMargins(6, 2, 10, 2); - userLay->setSpacing(8); - - auto* avatar = new QLabel(QStringLiteral("ZL"), userBtn); - avatar->setObjectName(QStringLiteral("avatar")); - avatar->setFixedSize(34, 34); - avatar->setAlignment(Qt::AlignCenter); - avatar->setAttribute(Qt::WA_TransparentForMouseEvents); - userLay->addWidget(avatar); - - auto* userName = new QLabel(QStringLiteral("张磊"), userBtn); - userName->setObjectName(QStringLiteral("userName")); - userName->setAttribute(Qt::WA_TransparentForMouseEvents); - userLay->addWidget(userName); - - auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBtn); - userRole->setObjectName(QStringLiteral("userRole")); - userRole->setAttribute(Qt::WA_TransparentForMouseEvents); - userLay->addWidget(userRole); - auto* userMenu = new QMenu(userBtn); QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, [this] { emit logoutRequested(); }); userBtn->setMenu(userMenu); - lay->addWidget(userBtn); } diff --git a/src/app/main.cpp b/src/app/main.cpp index 5bc18bd..d88b1fe 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -999,7 +999,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QSettings settings; const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray(); if (!geo.isEmpty()) window.restoreGeometry(geo); - const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray(); + // 注意:ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局, + // 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。 + const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v2")).toByteArray(); if (!dockState.isEmpty()) { dockManager->restoreState(dockState); // restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。 @@ -1010,7 +1012,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() { QSettings settings; settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry()); - settings.setValue(QStringLiteral("ui/dockState"), dockManager->saveState()); + settings.setValue(QStringLiteral("ui/dockState_v2"), dockManager->saveState()); }); } From 9680fefbe33418f44178a2ebbb023036ef7d8619 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 17:59:10 +0800 Subject: [PATCH 55/56] =?UTF-8?q?feat(topbar):=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8C=BA=E6=8C=89=E6=A0=B7=E5=9B=BE=E9=87=8D=E5=81=9A(?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E7=AB=96=E7=9B=B4=E5=B1=85=E4=B8=AD+?= =?UTF-8?q?=E5=A7=93=E5=90=8D/=E8=81=8C=E5=8A=A1=E4=B8=A4=E8=A1=8C?= =?UTF-8?q?=E5=B7=A6=E5=AF=B9=E9=BD=90+=E4=B8=8B=E6=8B=89=E7=AE=AD?= =?UTF-8?q?=E5=A4=B4,=E6=95=B4=E5=9D=97=E5=8F=AF=E7=82=B9)=20+=20=E5=8A=A0?= =?UTF-8?q?=E5=AE=BD=E4=B8=8B=E6=8B=89=E8=8F=9C=E5=8D=95(=E8=B4=A6?= =?UTF-8?q?=E6=88=B7/=E4=B8=AA=E4=BA=BA=E8=B5=84=E6=96=99/=E5=81=8F?= =?UTF-8?q?=E5=A5=BD=E8=AE=BE=E7=BD=AE/API=E5=AF=86=E9=92=A5/=E9=80=80?= =?UTF-8?q?=E5=87=BA=E7=99=BB=E5=BD=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/TopBar.cpp | 75 ++++++++++++++++++++++++++++++++++++---------- src/app/TopBar.hpp | 7 +++++ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 471675d..3bc82ad 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -232,22 +234,65 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeDivider(this)); lay->addSpacing(12); - // 用户区:头像(圆形图标) + 姓名·职务 同一行,整块可点击 → 退出登录菜单。 - // 用 QToolButton(图标+文字),而非 QPushButton+子布局——后者按空文本算尺寸会把内容挤成一团。 - auto* userBtn = new QToolButton(this); - userBtn->setObjectName(QStringLiteral("userBtn")); - userBtn->setIcon(QIcon(renderAvatar(QStringLiteral("ZL"), 30, - geopro::app::tokenColor("accent/primary"), Qt::white))); - userBtn->setIconSize(QSize(30, 30)); - userBtn->setText(QStringLiteral("张磊 · 高级工程师")); - userBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - userBtn->setPopupMode(QToolButton::InstantPopup); - userBtn->setCursor(Qt::PointingHandCursor); - auto* userMenu = new QMenu(userBtn); - QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, + // 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。 + // 用普通 QWidget + eventFilter:QWidget 按子布局正确撑开(QPushButton 装布局会按空文字算尺寸挤成一团)。 + userRow_ = new QWidget(this); + userRow_->setObjectName(QStringLiteral("userBtn")); + userRow_->setAttribute(Qt::WA_StyledBackground, true); // 令 QSS 背景(hover)在 QWidget 上生效 + userRow_->setCursor(Qt::PointingHandCursor); + userRow_->installEventFilter(this); + auto* uLay = new QHBoxLayout(userRow_); + uLay->setContentsMargins(8, 3, 8, 3); + uLay->setSpacing(10); + + auto* avatar = new QLabel(userRow_); + avatar->setPixmap( + renderAvatar(QStringLiteral("ZL"), 34, geopro::app::tokenColor("accent/primary"), Qt::white)); + avatar->setFixedSize(34, 34); + avatar->setAttribute(Qt::WA_TransparentForMouseEvents); + uLay->addWidget(avatar, 0, Qt::AlignVCenter); + + auto* nameBox = new QWidget(userRow_); + nameBox->setAttribute(Qt::WA_TransparentForMouseEvents); + auto* nameLay = new QVBoxLayout(nameBox); + nameLay->setContentsMargins(0, 0, 0, 0); + nameLay->setSpacing(0); + auto* userName = new QLabel(QStringLiteral("张磊"), nameBox); + userName->setObjectName(QStringLiteral("userName")); + auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox); + userRole->setObjectName(QStringLiteral("userRole")); + nameLay->addWidget(userName); + nameLay->addWidget(userRole); + uLay->addWidget(nameBox, 0, Qt::AlignVCenter); + + auto* chevronLbl = new QLabel(userRow_); + chevronLbl->setPixmap(QPixmap(geopro::app::writeChevronIcon(true, QColor("#7C8493"))) + .scaled(12, 12, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + chevronLbl->setAttribute(Qt::WA_TransparentForMouseEvents); + uLay->addWidget(chevronLbl, 0, Qt::AlignVCenter); + + // 下拉菜单(加宽):账户 / 个人资料 / 偏好设置 / API 密钥 / 退出登录。 + userMenu_ = new QMenu(this); + userMenu_->setMinimumWidth(200); + userMenu_->addAction(QStringLiteral("账户")); + userMenu_->addAction(QStringLiteral("个人资料")); + QObject::connect(userMenu_->addAction(QStringLiteral("偏好设置")), &QAction::triggered, this, + [this] { emit settingsRequested(); }); + userMenu_->addAction(QStringLiteral("API 密钥")); + userMenu_->addSeparator(); + QObject::connect(userMenu_->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, [this] { emit logoutRequested(); }); - userBtn->setMenu(userMenu); - lay->addWidget(userBtn); + + lay->addWidget(userRow_); +} + +bool TopBar::eventFilter(QObject* obj, QEvent* event) { + if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) { + if (userMenu_) + userMenu_->exec(userRow_->mapToGlobal(QPoint(0, userRow_->height() + 2))); + return true; + } + return QWidget::eventFilter(obj, event); } void TopBar::setWorkspaces(const std::vector& list, const QString& currentId) { diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index f90445e..87315d0 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -4,6 +4,8 @@ #include "repo/RepoTypes.hpp" class QToolButton; +class QEvent; +class QMenu; namespace geopro::app { @@ -21,6 +23,9 @@ public: bool hasMore); void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字 +protected: + bool eventFilter(QObject* obj, QEvent* event) override; // 用户区整块可点 → 弹菜单 + signals: void workspaceSwitchRequested(const QString& tenantId); void projectSwitchRequested(const QString& projectId); @@ -31,6 +36,8 @@ signals: private: QToolButton* wsBtn_ = nullptr; QToolButton* projBtn_ = nullptr; + QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头) + QMenu* userMenu_ = nullptr; // 用户下拉菜单 }; } // namespace geopro::app From d1be0567de5baa01484dc9fbe5c524d4481d36cc Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 18:39:49 +0800 Subject: [PATCH 56/56] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E4=B8=80=E5=9B=BE?= =?UTF-8?q?=E6=A0=87-=E6=96=87=E5=AD=97=E9=97=B4=E8=B7=9D=E5=88=B06px?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=BB=99=E5=88=87=E6=8D=A2=E5=99=A8/?= =?UTF-8?q?=E9=A1=B5=E7=AD=BE=E5=9B=BE=E6=A0=87=E5=8A=A02px=E5=8F=B3?= =?UTF-8?q?=E5=86=85=E8=BE=B9=E8=B7=9D(Fusion=E5=86=85=E7=BD=AE4px+2),?= =?UTF-8?q?=E4=B8=8E=E9=9D=A2=E6=9D=BF=E6=A0=87=E9=A2=98(6px)=E4=B8=80?= =?UTF-8?q?=E8=87=B4(=C2=A76.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/Glyphs.cpp | 17 +++++++++++------ src/app/Glyphs.hpp | 14 ++++++++++---- src/app/PanelHeader.cpp | 3 +-- src/app/TopBar.cpp | 6 ++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/app/Glyphs.cpp b/src/app/Glyphs.cpp index d4df0e7..53b619b 100644 --- a/src/app/Glyphs.cpp +++ b/src/app/Glyphs.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -98,7 +99,7 @@ QString svgPathFor(Glyph t) } // namespace -QIcon makeGlyph(Glyph type, const QColor& color, int px) +QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight) { const QString svg = QStringLiteral("setIcon(makeGlyph(type, themedIconColor(), px)); }; + auto apply = [button, type, px, padRight]() { + button->setIcon(makeGlyph(type, themedIconColor(), px, padRight)); + if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推 + }; apply(); QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); }); } diff --git a/src/app/Glyphs.hpp b/src/app/Glyphs.hpp index 303208d..6ded8fd 100644 --- a/src/app/Glyphs.hpp +++ b/src/app/Glyphs.hpp @@ -34,13 +34,19 @@ enum class Glyph { Gear, // 设置(齿轮) }; -// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 2x 绘制保证清晰)。 -QIcon makeGlyph(Glyph type, const QColor& color, int px = 16); +// 「图标+文字」按钮的图标→文字间距补丁:Fusion 内置约 4px,本值补到规范 §6.7 的 6px。 +// 做法:把图标渲染进“px 宽图标 + padRight 透明右边距”的画布,文字被这段透明区右推。 +inline constexpr int kGlyphTextGapPad = 2; -// 随 ElaTheme 明暗自动着色的 glyph(取主题文本色:暗色用浅色、亮色用深色),主题切换时自动重绘。 +// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 3x 绘制保证清晰)。 +// padRight>0 时图标画布右侧留透明内边距(用于「图标+文字」按钮统一间距),图标本体仍为 px×px 居左。 +QIcon makeGlyph(Glyph type, const QColor& color, int px = 16, int padRight = 0); + +// 随主题明暗自动着色的 glyph(取主题文本色:暗色用浅色、亮色用深色),主题切换时自动重绘。 // 用于面板表头/页签等 chrome 图标,避免固定色在暗色下看不清。 +// 按钮版的 padRight:见 kGlyphTextGapPad;>0 时本函数同时把 iconSize 设为 (px+padRight)×px。 void setThemedGlyph(QLabel* label, Glyph type, int px); -void setThemedGlyph(QAbstractButton* button, Glyph type, int px); +void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight = 0); // 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。 // 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。 diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index f2f7b98..61e1021 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -131,8 +131,7 @@ TabbedPanel buildTabbedPanel(const QVector& tabs, const QVector
setObjectName(QStringLiteral("tabBtn")); btn->setText(t.title); - setThemedGlyph(btn, t.icon, kTabIcon); // 随主题着色 - btn->setIconSize(QSize(kTabIcon, kTabIcon)); + setThemedGlyph(btn, t.icon, kTabIcon, kGlyphTextGapPad); // 随主题着色 + 图标→文字6px(§6.7) btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); btn->setCheckable(true); btn->setCursor(Qt::PointingHandCursor); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 3bc82ad..159bf1b 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -197,8 +197,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用高清 chevron menu-indicator;数据驱动)。 wsBtn_ = new QToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); - setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活) - wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); + setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon, kGlyphTextGapPad); // 图标→文字6px(§6.7) wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); wsBtn_->setPopupMode(QToolButton::InstantPopup); wsBtn_->setCursor(Qt::PointingHandCursor); @@ -213,8 +212,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { // 项目切换器(QToolButton + 主题化 QSS;数据驱动)。 projBtn_ = new QToolButton(this); projBtn_->setObjectName(QStringLiteral("wsSwitcher")); - setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon); // 中性主题色 - projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon)); + setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon, kGlyphTextGapPad); // 中性主题色 + 图标→文字6px projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); projBtn_->setPopupMode(QToolButton::InstantPopup); projBtn_->setCursor(Qt::PointingHandCursor);