Compare commits

..

57 Commits

Author SHA1 Message Date
gaozheng 439737a457 Merge pull request 'refactor/pure-qt-ui' (#3) from refactor/pure-qt-ui into main
Reviewed-on: https://gitea.geomative.cn/gaozheng/geopro/pulls/3
2026-06-10 18:41:52 +08:00
gaozheng d1be0567de fix(ui): 统一图标-文字间距到6px——给切换器/页签图标加2px右内边距(Fusion内置4px+2),与面板标题(6px)一致(§6.7) 2026-06-10 18:39:49 +08:00
gaozheng 9680fefbe3 feat(topbar): 用户区按样图重做(头像竖直居中+姓名/职务两行左对齐+下拉箭头,整块可点) + 加宽下拉菜单(账户/个人资料/偏好设置/API密钥/退出登录) 2026-06-10 17:59:10 +08:00
gaozheng 3ccb8df4ed fix(ui): 修复改名导致的停靠布局丢失(bump dockState键丢弃失配旧布局) + 用户区改回QToolButton(头像图标+姓名职务一行,整块可点)修复挤成一团 2026-06-10 17:41:46 +08:00
gaozheng c953b35334 feat(topbar): 切换器下拉箭头换高清chevron(替代粗糙▾) + 用户区头像/姓名/职务同行且整块可点击 2026-06-10 17:27:37 +08:00
gaozheng 9e80b2fea1 feat(ui): 面板改名(对象/数据集/异常/数据集属性) + 原数据/网格数据改为下划线页签(与其他切换一致) + 表头图标间距对齐规范6px(§6.7) 2026-06-10 17:21:36 +08:00
gaozheng 5f02d494dc fix(theme): 复选指示器全局统一(清晰可见,规避Fusion浅底过淡) + 失焦保持选中,移除对象树本地重复样式 2026-06-10 17:09:09 +08:00
gaozheng 2a666663e7 refactor(theme): 全部项目对话框迁移令牌 + 清理委托接管后失效的列表选中/前景死代码 2026-06-10 17:02:03 +08:00
gaozheng b78969471e refactor(theme): 登录/设置迁移令牌 + 删除遗留 kDarkMap 字符串替换路径(暗色全由令牌双值驱动)(规范§13.1) 2026-06-10 16:57:10 +08:00
gaozheng 8f31f043df feat(panels): 异常列表卡片化(色条+类型标签+显隐眼睛,真实数据)(规范§6.3) 2026-06-10 16:44:35 +08:00
gaozheng b26dcc1ca7 feat(panels): 数据/文件列表卡片化(标题+元信息双行+选中竖条)(规范§6.2) 2026-06-10 16:40:14 +08:00
gaozheng 824e8bdf62 refactor(theme): 工作台内联色(对象树/数据详情工具条/停靠分隔)迁移到令牌(规范§4.2/§6.1/§6.12) 2026-06-10 16:27:57 +08:00
gaozheng b728631477 refactor(theme): TopBar/PanelHeader 内联样式迁移到语义令牌(规范§4.3/§5) 2026-06-10 16:21:49 +08:00
gaozheng b2ec3459c7 fix(canvas): 视图详情浮层去圆角(直角)消除浅色模式四白角伪影 2026-06-10 16:14:51 +08:00
gaozheng e60bdbc150 fix(canvas): 空状态卡片用 canvas/bg 等色无缝底(原生GL覆盖透明失效的可靠解法) 2026-06-10 16:08:31 +08:00
gaozheng 8eb0c7413d fix(canvas): 空状态去不透明卡片(移除OpacityEffect) + 视图详情转深色画布浮层(规范§6.5/§7.11) 2026-06-10 16:03:21 +08:00
gaozheng 58cabc6350 fix(view): 2D/3D 统一面板表头(分段页签) + 画布空状态提示用 canvas 令牌融入深底(规范§5/§6.5) 2026-06-10 15:52:45 +08:00
gaozheng b242240df6 feat(theme): 全局 QSS 模板化 + palette 从令牌,标准控件对齐规范色值(§1/§3/§6/§7) 2026-06-10 15:29:07 +08:00
gaozheng 425e17e6af feat(canvas): 数据画布常深 #0B1320(规范§0.5/§11) 2026-06-10 15:22:32 +08:00
gaozheng 18d084047f feat(theme): 语义令牌基础设施(令牌表+token/fillTokens API,规范§1) 2026-06-10 15:17:46 +08:00
gaozheng 0edfa56ec6 docs: 设计规范落地计划 + 基线与偏离记录 2026-06-10 15:14:44 +08:00
gaozheng 6c34f71177 fix(ui): 下拉框弹窗改为紧贴文本框下方的单层列表(AppProxyStyle)
根因: Fusion 的 combo 弹窗是「菜单式覆盖当前项」(SH_ComboBox_Popup=true) → 位置怪、
容器框+列表两层、选中/悬停不清。AppProxyStyle 把该 hint 改为 0 → 标准「下方下拉列表」:
位置紧贴文本框、单层、当前项与悬停项走 ::item:selected 高亮。
2026-06-10 14:58:01 +08:00
gaozheng a6a3979b93 fix(ui): 下拉框弹窗 — 干净填充选中 + 去边框/圆角伪影
QComboBox 弹窗当前/选中项原来显示一个细边框方块(Fusion 默认 + 缺 ::item 规则)。
补 QComboBox QAbstractItemView::item 规则: 统一行高/内边距、去边框、选中=浅蓝填充+强调色文字;
去掉 view 的 border-radius(防圆角弹窗露直角)。改一处(单一 QSS)即生效。
2026-06-10 14:52:27 +08:00
gaozheng 9010b20b57 refactor(ui): 方案A — 移除 ElaWidgetTools,统一为标准 Qt + 单一设计系统
根因: 此前 Fusion+自定义QSS+ElaWidgetTools(自绘控件) 三套样式系统并存、互相打架,
是各种丑/不一致的来源。本次彻底收敛为一套:

- 移除 ElaWidgetTools 依赖(CMake FetchContent + 链接 + spike);所有 Ela* 控件 → 标准 Qt:
  ElaWindow→QMainWindow(原生标题栏)、ElaMenu/MenuBar→QMenu/QMenuBar、ElaLineEdit/ComboBox/
  CheckBox/PushButton/ToolButton/Text/TableWidget→对应 Qt、ElaIconButton→QToolButton+glyph
- 主题系统: 自建 ThemeManager(替代 ElaTheme,QStyleHints 检测系统明暗、持久化、changed 信号热切)
  + 单一「浅→暗」颜色映射(kDarkMap,全 UI 唯一颜色来源) + 单份 QSS(明色基线,暗色按表替换)
- 主题: 跟随系统/浅/深(持久化, 启动应用→登录与主页统一); 字号缩放经 scaledPx 覆盖内联 chrome
- NOTICE/关于 同步去掉 ElaWidgetTools
- ctest 53/53
2026-06-10 14:44:59 +08:00
gaozheng a13b58e09f feat(settings): 设置对话框(外观/关于) + 主题持久化 + 界面字号
- 主题持久化(QSettings ui/themeMode: system/light/dark): 启动时 applyPersistedThemeMode 在弹登录窗前
  应用 → 登录页与主页明暗统一(修登录页一直 dark); 设置里改主题热切, Ctrl+Shift+T 仍可用
- 界面字号(ui/fontScale: 90/100/115/130): applyPersistedFontScale 设 qApp 基准字体; scaledPx() 让
  内联 QSS 的 chrome(顶栏/面板表头/工具条/登录/浮层)字号也随之缩放; 字号改动重启后整体统一生效
- SettingsDialog: 左分类(外观/关于)+右页; 外观=主题下拉+字号下拉(+立即重启); 关于=版本+第三方许可
- 顶栏齿轮 → settingsRequested → 打开设置
2026-06-10 14:03:16 +08:00
gaozheng 52bdf054a6 fix(list): 数据/文件/异常列表退回标准 QListWidget + 写死强选中色
根因: ElaListView 的选中底走 BasicSelectedAlpha, setThemeColor 改它对 ElaListView 不生效(自绘控件坑),
选中色无法变强。与对象树同理, 退回 Qt 原生 QListWidget:
- 3 列表 ElaListView+QStandardItemModel → QListWidget+QListWidgetItem(populate/加载更多/点击/勾选 全回退)
- applyListSelection: 本地 QSS 写死强调蓝选中(明 #C2D9F2 / 暗 #33527A + 适配文字, :!active 防失焦淡),
  与对象树选中色完全一致, 100% 可控、明暗都清晰
- 行为(加载更多/数据集点击/异常勾选显隐)保持
2026-06-10 12:39:23 +08:00
gaozheng 66cf432a98 fix(theme): 选中底色统一加强(对象树+所有列表)
根因: ElaListView 的 BasicSelectedAlpha 默认是半透明灰、对比度弱; 树 QSS 选中也偏淡。
设计统一的强调蓝选中底(明 #C2D9F2 / 暗 #33527A, 强对比):
- applyBrandAccent: 设 BasicSelectedAlpha/BasicSelectedHoverAlpha 为该实色 → 所有 Ela 列表
  选中底变清晰(并保留其 3px 强调色左竖条)
- 对象树: 本地 QSS item:selected 设同款蓝 + 适配文字色, :!active 防失焦变淡
2026-06-10 12:26:00 +08:00
gaozheng 934e25be54 fix(tree): 自绘清晰复选框(明暗都可见) + 选中整行连续(去碎片框)
- 多余选中框: 全局 QTreeView::item 去掉 border-radius+margin → 选中是整行连续一条, 不再浮动碎块
- light 复选框看不清: Fusion 原生复选框浅底边框过淡。writeCheckboxIcon 自绘 PNG
  (未选=明显边框空心框, 选中=强调色底+白勾), 明暗各一套, 经 QTreeView::indicator QSS 引用,
  主题切换重绘 → 明暗都清晰
2026-06-10 12:21:01 +08:00
gaozheng e3a1b18efa chore(ela): 去掉 GEOPRO_UI_SHELL 兜底开关 + 新增开源 NOTICE
- main: 删除 env 选壳分支与经典 QMainWindow 回退, ElaWindow 成为唯一外壳(迁移已稳定)
- NOTICE.md: 列出第三方组件与许可证(Qt LGPL / VTK BSD / ADS LGPL / ElaWidgetTools MIT /
  QtKeychain BSD) 及合规要点、Ela 版本钉定说明
2026-06-10 12:02:43 +08:00
gaozheng cfd39e5be8 fix(ela): 对象树退回标准 QTreeWidget(复选框清晰) + 行内边距收紧
ElaTreeView 是 ElaWidgetTools 自绘控件(非 Qt 原生), light 下复选框对比度不足、选中渲染有局限。
- 对象树 ElaTreeView+QStandardItemModel → 标准 QTreeWidget+QTreeWidgetItem:
  复选框/展开箭头由 Fusion 原生绘制, 明暗都清晰; 行为(TM 勾选/点击)不变
- 全局 QTreeView::item padding 8px→4px: 行间距过大修正
2026-06-10 12:00:20 +08:00
gaozheng 464911dc57 fix(ela): 页签与工具条统一为同一套切换样式(消除三套不一致)
PanelHeader 页签(数据/文件, 异常列表/对象属性)从 ElaToolButton 退回 QToolButton, headerQss
恢复 tabBtn 下划线规则: 选中 = 强调色文字 + 2px 强调色下划线 — 与视图/详情工具条完全一致。
至此全 UI 的切换控件(页签 + 分段工具条)是同一种视觉语言。
2026-06-10 11:51:02 +08:00
gaozheng 107fed8182 fix(ela): 工具条选中态 + 切换器箭头改用 QToolButton+主题QSS(清晰可控)
ElaToolButton 硬限制: 选中态只画极淡 BasicHover(看不清)、展开箭头不可靠、且自绘无法被 QSS 覆盖。
故交互态强的这两类退回 QToolButton + applyThemedStyleSheet(用统一强调色):
- 视图/详情工具条(2D/3D, 原数据/网格数据, 显示异常/电极/等值线): 选中 = 强调色文字 + 2px 强调色下划线,
  明暗都清晰可辨
- 切换器: QToolButton + 文字'▾'(清晰, 不再是 ElaToolButton 那个发糙/消失的箭头) + 悬停底
其余(菜单/列表/树/表单/图标按钮)仍 Ela。强调色已全局统一为品牌蓝
2026-06-10 11:39:20 +08:00
gaozheng ec428ccaca fix(ela): 面板占位按钮(筛选/上传/添加/导出/折叠)改 Ela 字体图标
之前用 makeGlyph 位图 + 30×30 → 偏大且发糊。改为 ElaIconButton 字体图标(actionElaIcon 映射
Glyph→ElaIconType: Filter/Upload/Plus/Download/ChevronUp), 16px 图标 + 28×28 按钮,
与顶栏 帮助/通知/设置 一致: 清晰、随主题、尺寸协调
2026-06-10 11:23:54 +08:00
gaozheng 0867636ea4 feat(ela): 统一品牌强调色 + 切换器箭头修复 + 图标中性化 + 树缩进收紧
配色根因: Ela 默认主色(#0067C0亮/#4CC2FF暗)与项目品牌蓝 #2D6CB5 是两种蓝, 互相打架。
- applyBrandAccent(): 用 ElaTheme::setThemeColor 把 Primary 设成品牌蓝(亮 #2D6CB5/暗 #5E9BD6,
  含 Hover/Press 三态) → 所有 Ela 原生控件选中/激活/标题栏强调 + 本项目 QSS 共用一套蓝
- 切换器图标: #2D6CB5 → setThemedGlyph 中性主题色(蓝只留给选中/激活), 与面板图标一致
- 切换器箭头: 成员 QToolButton* → ElaToolButton*, setMenu 走 Ela 重载 → Ela 自绘清晰展开箭头
  (之前去掉手加的▾后没箭头, 是因为基类 setMenu 不触发自绘)
- 对象树缩进 20→14, 更紧凑
2026-06-10 11:05:45 +08:00
gaozheng c5393e8ac3 fix(ela): 标题栏收紧 + 切换器去掉重复下拉箭头
- ElaWindow AppBarHeight 45→38(默认偏大, 更接近原生标题栏高度)(点1 过大)
- 切换器去掉文字里手加的 '▾'(ElaToolButton 给带菜单按钮自绘展开箭头, 原来与文字▾重复)(点2)
- 注: 显示为 100% 缩放, 无 DPI 模糊; 若 Ela 自带标题栏图标仍觉软, 属其 Fluent 渲染风格
2026-06-10 10:51:36 +08:00
gaozheng 5a945e51a7 fix(ela): 面板表头/页签图标随主题着色(点3)
- 新增 setThemedGlyph(QLabel*/QAbstractButton*, Glyph, px): 取 ElaTheme 主文本色(暗=浅/亮=深)绘制,
  主题切换自动重绘
- PanelHeader 表头标题图标(原 #44546B 固定→暗色看不清)、页签图标(原 #5A6B85) 改走 setThemedGlyph
  → 对象显示栏/数据集显示栏/异常列表等所有面板图标在明暗下都清晰
2026-06-10 10:47:16 +08:00
gaozheng 2be49b205c fix(ela): 图标按钮变形 + tooltip 丑
- ElaIconButton(icon,pixelSize,parent) 不设固定尺寸→图标被压扁; 改用带固定宽高的构造
  (TopBar help/bell/gear: icon,18,34,34); PanelHeader 操作按钮(QPixmap 构造)显式 setFixedSize(30,30)
- 删全局 QToolTip QSS(深蓝底+蓝边框+圆角→弹窗露直角且不像原生)→ 用系统原生工具提示
2026-06-10 10:35:50 +08:00
gaozheng 3aa4e1bfe2 feat(ela): 视图/详情工具条 → ElaToolButton 行(替 QToolBar+QAction)
- 视图工具条(二维地图/三维视图): QToolBar+QActionGroup → QWidget+QHBoxLayout+ElaToolButton+QButtonGroup(互斥)
- 详情工具条(原数据/网格数据 互斥 + 显示异常/电极/等值线 开关): 同上 + QFrame 分隔
- 连接: QAction::triggered→QAbstractButton::clicked, QAction::toggled→QAbstractButton::toggled;
  视图模式/详情模式/叠加显隐 行为保持
- 注: 工具条交互(2D/3D 切换/详情模式/三个显隐开关)为活逻辑, 需运行验证
2026-06-10 10:27:33 +08:00
gaozheng b6e0142d06 feat(ela): TopBar 图标/切换器 + PanelHeader 页签/操作按钮 全 Ela 化(头像除外)
- TopBar: help/bell/gear → ElaIconButton(Fluent 图标字体, 自动主题);
  工作空间/项目切换器 → ElaToolButton; 去掉 #wsSwitcher/#iconBtn QSS(Ela 自绘);
  头像保留自定义圆形(白字用 white 关键字恒白)
- PanelHeader: 数据/文件等页签 → ElaToolButton(选中态走 Ela checked 高亮);
  表头操作按钮 → ElaIconButton; headerQss 去掉 tabBtn/panelAction 规则
2026-06-10 10:23:09 +08:00
gaozheng 8e7563c0f5 feat(ela): 数据集/文件/异常列表 → ElaListView + QStandardItemModel
- 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; 加载更多/勾选显隐/点击 行为保持
- 注: 列表交互(异常显隐/加载更多/数据集点击)为活逻辑, 需运行验证
2026-06-10 09:48:24 +08:00
gaozheng 389a2da744 feat(ela): 表格→ElaTableWidget(直替) + 对象树→ElaTreeView+QStandardItemModel
- ProjectListDialog: QTableWidget → ElaTableWidget(item 版直接继承 QTableWidget, 1 行)
- ObjectTreePanel: QTreeWidget → ElaTreeView + Qt 自带 QStandardItemModel(非手写 model);
  QTreeWidgetItem→QStandardItem, 勾选/点击逻辑改按 QModelIndex/itemChanged; ElaTreeView 自绘
  展开折叠指示(去掉自定义 chevron QSS); 行为(TM 勾选/点击发 tmClicked/tmCheckToggled)保持
2026-06-10 09:40:28 +08:00
gaozheng 68d832c57b fix(ela): 登录窗主题化 + VTK 背景稳健随主题 + 扫清所有硬编码浅色 QSS 遗漏
- 登录窗: setStyleSheet → applyThemedStyleSheet(整窗随主题, 品牌带文字用 white 关键字恒白);
  refreshBtn/errorLabel 也随主题。修暗系统下浅窗+暗 Ela 控件割裂、占位文字看不清
- VTK: 改为主题切换时重跑 rebuildCentral/rebuildDetail(走完整渲染必重绘, 兼顾 syncSystemTheme
  异步切暗时序); rebuildDetail 也按 vtkBackground 设背景
- 主动扫描修掉遗漏: dockManager 分隔条、数据集/文件列表、对象树枝/hint、空状态标题/提示 全走主题
- themed() 公共助手(给需拼接 ADS 样式的 dockManager)
2026-06-10 09:32:22 +08:00
gaozheng f5eff9e185 feat(ela): C VTK 视口背景随主题 + A 浮层勾选框 Ela 化
- vtkBackground(): 取 ElaTheme 窗口底色; CentralScene/详情渲染器初始用它; 切主题时中央+详情重设+刷新
- 浮层图层勾选(帘面/体素/切片/地形) → ElaCheckBox
- 说明: 自定义 chrome(工作空间/项目切换器/图标按钮/PanelHeader tab/工具条)保留并随主题(强换 Ela 会丢设计); 状态栏由 QSS 随主题
2026-06-10 09:17:45 +08:00
gaozheng 57c452e2d3 feat(ela): 菜单全换 ElaMenu/ElaMenuBar(根治圆角露直角) + 登出功能
- TopBar: 4 主菜单/子菜单/切换器菜单 → ElaMenu, 菜单栏 → ElaMenuBar(自绘透明圆角弹窗+随主题);
  去掉 #appMenuBar 内联 QSS; Theme.cpp 删除 QMenuBar/QMenu QSS(否则 border-radius 仍露直角)
- 登出: 头像改可点击 QToolButton + ElaMenu「退出登录」→ logoutRequested 信号;
  main 接信号 → forgetSession() 清凭证 + QProcess 重启回登录页(撤销之前的 GEOPRO_FORCE_LOGIN 补丁)
2026-06-10 09:12:05 +08:00
gaozheng 4a785ede88 fix(ela): 内联 chrome 也跟随主题 — 修暗色下切换器文字看不清等
截图问题根因: TopBar/PanelHeader/3D浮层 用每控件内联 setStyleSheet(硬编码浅色 hex),
未走全局 ElaTheme 替换 → 暗色下保持浅色令牌(深字落深底→看不清; 白底面板表头等)。
新增 applyThemedStyleSheet(widget,设计稿QSS): 按 ElaTheme 角色着色 + 随明暗自动重着色。
TopBar/PanelHeader/layerPanel/layerTitle 内联样式全改走它; 补 #EEF1F5/#E6EAF1/#EAEEF5 角色。
2026-06-10 08:59:59 +08:00
gaozheng 8a82029553 fix(ela): 工作台配色改为取自 ElaTheme(里外一致) — 修明暗都不对
根因: 我那套自挑的浅/暗配色与 ElaWindow 外壳的 Fluent 配色不一致, 里外两种色, 明暗都割裂。
改法: kRoleMap 把设计稿色令牌按语义角色映射到 ElaTheme 颜色角色, styleSheetForMode/
buildPalette 用 eTheme->getThemeColor(当前模式,角色) 取真实颜色, 与外壳同源。
(已知: #FFFFFF 兼作面板底与按钮文字, 全局替换后暗色下默认按钮文字对比度略弱, 影响很小)
2026-06-10 08:53:00 +08:00
gaozheng 9091d8c929 fix(ela): 启动主题跟随 ElaTheme 初始模式(review M2)
避免系统暗色启动时登录窗(标准控件)与 Ela 控件明暗错配。
2026-06-10 07:57:41 +08:00
gaozheng ef278ac335 feat(ela): P3-b 项目列表弹窗 Ela 化 — 输入/下拉/按钮→Ela*
nameEdit_→ElaLineEdit, typeCombo_→ElaComboBox, 搜索/重置/上一页/下一页→ElaPushButton;
表格(QTableWidget)保留并靠 P2 暗色 QSS 联动
2026-06-10 07:51:12 +08:00
gaozheng c8812aa8a6 feat(ela): P3-a 登录窗 Ela 化 + eApp->init 无条件化
- main.cpp: eApp->init() 提到登录前无条件调用(Ela 控件在登录窗/各面板都要用,登录在选壳前)
- LoginWindow: 输入框→ElaLineEdit, 记住登录→ElaCheckBox, 登录按钮→ElaPushButton;
  去掉它们的自定义 QSS 让 Ela 自绘 Fluent+自动明暗; 品牌头/字段标签/验证码/刷新链接保留
- Ela 控件均继承对应 Qt 基类, 故成员仍用 Qt 指针(多态), 不改 .hpp; 现有方法调用照常
2026-06-10 07:49:15 +08:00
gaozheng 26404cee2f feat(ela): P2 暗色主题桥 — ElaTheme 明/暗 → 全局 QSS+调色板联动
- 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 内, 需另接)
2026-06-10 07:43:46 +08:00
gaozheng af0012fd70 fix(ela): P1 底部状态栏贴底边 — setCentralCustomWidget 改 addPageNode
ElaCentralStackedWidget::setCustomWidget 用 insertWidget(0,...) 把控件插到页栈容器之上,
被禁用的空导航页栈仍占底部空间,状态栏不贴底边。改用 addPageNode 把工作台作为唯一页面
放进中心页栈,填满到底边。
2026-06-10 07:38:41 +08:00
gaozheng 8d938dd848 fix(ela): P1 code review 修复
- 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
2026-06-09 21:39:47 +08:00
gaozheng fc282824b9 feat(ela): P1 换壳 — ElaWindow 包裹工作台(GEOPRO_UI_SHELL=ela 开关)
- 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: 设置
2026-06-09 21:34:27 +08:00
gaozheng c4d76f57b6 新增claude.md(karpathy) 2026-06-09 21:24:28 +08:00
gaozheng 6df2c4832c chore(ela): ElaWidgetTools 评估 spike + 全面迁移计划 + 构建 TEMP 兜底
- 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 + 控件映射表 + 风险登记)
2026-06-09 21:23:14 +08:00
gaozheng 1a9fb72cf0 feat(ui): impeccable 设计令牌体系 + 空状态/语义色/动效 + dock 标题修复
- typeset: Theme.hpp 新增排版令牌(type::),统一各处散落字号/字重
- layout: 间距/圆角令牌(space::/radius::),圆角 6 档→2 档,手调奇数余白对称化
- delight: 中央空状态引导浮层、上下文化加载文案、登录错误淡入
- colorize: 语义色令牌(semantic::),项目状态着色、状态栏错误染色、异常徽标警示色(休眠)
- overdrive(休眠): 详情视图相机补间+actor淡入(animateReveal),待 dd 详情渲染接通后激活
- fix(dock): restoreState 后重新隐藏 ADS 子窗口标题栏,修复已保存布局下标题栏复现
2026-06-09 20:26:00 +08:00
gaozheng caf6f9ebd0 docs(build): 新增 build.bat 一键构建脚本 + README 补充构建与运行说明 2026-06-09 19:07:14 +08:00
31 changed files with 3406 additions and 413 deletions

65
CLAUDE.md Normal file
View File

@ -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.

23
NOTICE.md Normal file
View File

@ -0,0 +1,23 @@
# 第三方组件与许可证 (Third-Party Notices)
本项目geopro / Geopro 3.0 桌面端)使用了以下第三方开源组件。各组件版权归其各自作者所有,
按其各自许可证条款分发。
| 组件 | 用途 | 许可证 | 来源 |
|---|---|---|---|
| **Qt 6** (6.11.x) | GUI 框架Widgets/Gui/Core/Network/Svg/OpenGL 等) | LGPL-3.0(亦提供商业许可) | https://www.qt.io |
| **VTK** (9.x) | 三维/二维可视化渲染 | BSD-3-Clause | https://vtk.org |
| **Qt-Advanced-Docking-System** (4.3.1) | 停靠面板布局 | LGPL-2.1 | https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System |
| **QtKeychain** (0.14.0) | 凭证安全存取("记住登录" | BSD-3-Clause | https://github.com/frankosterfeld/qtkeychain |
> UI 为标准 Qt WidgetsFusion 风格 + 项目自带 QSS 主题),未使用第三方 UI 控件库。
## 许可证要点
- **Qt 6 / Qt-Advanced-Docking-SystemLGPL**:本项目以动态链接方式使用 Qt 与 ADS符合 LGPL
对动态链接的要求;如需替换这些库,最终用户可自行替换对应动态库。若改为静态链接或分发修改版,
需遵循 LGPL 的相应义务(提供目标代码/重新链接能力)。
- **VTK / QtKeychainBSD-3-Clause**:需在分发物中保留版权声明与许可证文本,不得用作者名背书。
> 各组件完整许可证文本随其源码分发FetchContent 拉取于 build 目录的 `_deps/<组件>-src/`
> 或随 Qt/VTK 安装目录)。如需在发行包中附带完整 LICENSE 文本,请从对应来源复制。

View File

@ -8,7 +8,7 @@
## 技术栈
Qt 6.8 LTSQtWidgets+ VTK 9.3+ · CMake + vcpkg全量含 Qt· MSVC 2022 / C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
Qt 6.11QtWidgets+ VTK 9.6 · CMake + Ninja · 官方 MSVC 预编译 Qt + vcpkg仅非 Qt 依赖)· MSVCVS 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 通过后展开完整实现计划。

79
build.bat Normal file
View File

@ -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%

View File

@ -0,0 +1,669 @@
# Geopro 3.0 桌面客户端 — 视觉设计规范Design System
**版本 v1.0** · 适用范围Geopro 3.0 桌面客户端全部界面
**技术载体**Qt 6QtWidgets+ Fusion QStyle + QSS + QPalette + QtAwesome + VTK
**模式**Light依据原型还原/ Dark同一设计语言派生
> 本规范是客户端视觉的**单一事实来源**。Claude Code 在实现任何界面时,颜色、间距、字号、圆角、控件尺寸、状态表达一律引用本文档的 token禁止在各 widget 中即兴硬编色值。所有 token 应集中定义在一个主题模块(见 §13全局通过主题对象访问。
---
## 0. 设计原则
1. **数据为主UI 退后**:中间的 2D/3D 视图与剖面图是视觉焦点,外围面板(树、列表、属性)使用克制的中性色,不与数据争夺注意力。
2. **浅色为默认,深色为派生**原型为浅色界面Light 为基准模式Dark 按相同色相、相同层级关系派生,保证两套是「同一语言的明暗版本」。
3. **信息密度优先**:勘探软件信息密集,控件紧凑、间距节制、对齐严谨,而非消费级 App 的宽松留白。
4. **强调色克制**:主强调色(科技蓝)只用于可交互的主操作、选中态、链接、聚焦;状态色只用于状态表达,不作装饰。
5. **视图区永远深色**:无论 Light/Dark 模式,中间的 2D 地图 / 3D 视图 / 剖面图画布**始终是深色衬底**(原型即如此),让色阶数据更突出。这意味着「模式切换」主要影响外围 UI视图画布的深色基调保持稳定。
6. **双模式同构**:同一组件在两种模式下结构、间距、字号完全一致,仅颜色 token 取值不同。
---
## 1. 色彩系统Color Tokens
色彩采用**语义化分层**原始色板Primitives→ 语义 tokenSemantic。组件只引用语义 token不直接引用原始色板便于换肤。
### 1.1 原始色板 · 主强调色Brand / Primary
科技蓝,取自原型导航高亮、主按钮、链接、选中态。
| Token | 色值 | 用途 |
|---|---|---|
| `--primary-50` | `#EFF5FF` | 最浅选中行背景、hover 底 |
| `--primary-100` | `#DBE8FE` | 浅强调背景 |
| `--primary-200` | `#BFD4FD` | |
| `--primary-300` | `#93B4FA` | |
| `--primary-400` | `#5E8DF5` | |
| `--primary-500` | `#3B73EC` | **主强调色Light 主按钮、链接)** |
| `--primary-600` | `#2B5FD9` | 主按钮 hover |
| `--primary-700` | `#2450B8` | 主按钮 pressed |
| `--primary-800` | `#21478F` | |
| `--primary-900` | `#1B3A6E` | |
> Dark 模式主强调略微提亮以保证深底对比度Dark 主强调用 `--primary-400` (`#5E8DF5`)hover 用 `--primary-300`
### 1.2 原始色板 · 中性灰阶Neutral
界面骨架色,决定整体气质。
| Token | 色值 | 说明 |
|---|---|---|
| `--neutral-0` | `#FFFFFF` | 纯白 |
| `--neutral-25` | `#FCFCFD` | 面板底 |
| `--neutral-50` | `#F7F8FA` | 应用背景Light 工作区底) |
| `--neutral-100` | `#EFF1F4` | 次级背景、表头底、斑马纹 |
| `--neutral-200` | `#E3E6EB` | 边框、分隔线 |
| `--neutral-300` | `#CDD2DA` | 输入框边框、禁用边框 |
| `--neutral-400` | `#A8AFBC` | 占位文字、禁用文字 |
| `--neutral-500` | `#7C8493` | 次要文字、图标默认 |
| `--neutral-600` | `#5A626F` | 正文次级 |
| `--neutral-700` | `#3E4551` | 正文 |
| `--neutral-800` | `#272C35` | 标题文字 |
| `--neutral-900` | `#161A20` | 最深文字、Dark 面板底 |
| `--neutral-950` | `#0E1116` | Dark 应用背景 |
### 1.3 原始色板 · 视图画布专用深色Canvas
2D/3D/剖面画布的衬底,两种模式通用。
| Token | 色值 | 说明 |
|---|---|---|
| `--canvas-bg` | `#0B1320` | 视图画布主背景(原型深蓝黑) |
| `--canvas-bg-soft` | `#111B2D` | 画布内浮层(如「列表显示栏」浮窗)底 |
| `--canvas-grid` | `#1E2A3D` | 画布网格线、坐标轴 |
| `--canvas-overlay` | `rgba(10,17,28,0.82)` | 画布上的标签底(如 ERT1 标注牌) |
| `--canvas-text` | `#E6ECF5` | 画布上文字 |
| `--canvas-text-dim` | `#8A97AC` | 画布上次要文字(坐标、比例尺) |
### 1.4 语义色 · 状态色Status
来自原型异常分级(红=高/低阻异常、橙=中等、蓝=边界/信息)与通用反馈。每个状态含 `主色 / 浅底 / 边框` 三档。
| 语义 | 主色(Light) | 主色(Dark) | 浅底(Light) | 浅底(Dark) | 用途 |
|---|---|---|---|---|---|
| **Danger / 高(红)** | `#E5484D` | `#FF6166` | `#FDECEC` | `#3A1D1F` | 高等级异常、错误、删除 |
| **Warning / 中(橙)** | `#E08A1E` | `#F5A623` | `#FBF0DD` | `#3A2C12` | 中等异常、警告 |
| **Success绿** | `#2E9E5B` | `#46C07A` | `#E7F6ED` | `#16301F` | 成功、在线、就绪 |
| **Info / 低(蓝)** | `#3B73EC` | `#5E8DF5` | `#EFF5FF` | `#16243F` | 信息、低等级、边界过渡 |
| **Neutral / 离线(灰)** | `#7C8493` | `#8A93A3` | `#F0F1F4` | `#23282F` | 离线、停用、未知 |
> **异常分级专用**(原型异常列表左侧圆点 + 标签「高/中/低」):高=Danger、中=Warning、低=Info停用/隐藏=Neutral。三维视图与剖面图中的异常标注牌也用同一组色。
### 1.5 语义 token 映射表(组件取此层)
| 语义 token | Light 取值 | Dark 取值 |
|---|---|---|
| `bg/app` | `neutral-50` | `neutral-950` |
| `bg/panel` | `neutral-0` | `neutral-900` |
| `bg/panel-subtle` | `neutral-25` | `#161B22` |
| `bg/header` | `neutral-0` | `#12161C` |
| `bg/hover` | `neutral-100` | `#1B2129` |
| `bg/selected` | `primary-50` | `#16243F` |
| `bg/canvas` | `canvas-bg` | `canvas-bg` |
| `border/default` | `neutral-200` | `#262C35` |
| `border/strong` | `neutral-300` | `#333B45` |
| `border/focus` | `primary-500` | `primary-400` |
| `text/primary` | `neutral-800` | `#E6E9EF` |
| `text/secondary` | `neutral-600` | `#A4ADBB` |
| `text/tertiary` | `neutral-500` | `#7A8494` |
| `text/disabled` | `neutral-400` | `#5A626F` |
| `text/link` | `primary-500` | `primary-400` |
| `text/on-primary` | `neutral-0` | `neutral-0` |
| `accent/primary` | `primary-500` | `primary-400` |
| `accent/primary-hover` | `primary-600` | `primary-300` |
| `accent/primary-pressed` | `primary-700` | `primary-500` |
| `divider` | `neutral-200` | `#22272F` |
| `scrollbar/thumb` | `neutral-300` | `#3A424D` |
| `scrollbar/thumb-hover` | `neutral-400` | `#4A535F` |
---
## 2. 字体与排版Typography
### 2.1 字族
| 用途 | 字体栈 |
|---|---|
| 中文 UI | `"Microsoft YaHei UI", "PingFang SC", "Source Han Sans SC", "Noto Sans CJK SC", sans-serif` |
| 西文/数字 | `"Segoe UI", "Inter", "Helvetica Neue", Arial, sans-serif` |
| 等宽(坐标/数值/编号/日志) | `"Cascadia Code", "JetBrains Mono", "Consolas", monospace` |
> macOS 优先 PingFang SC + SF Pro。数值、坐标`103.85°E · 36.72°N · z=1.0x`)、批次号、深度刻度一律用等宽字体,保证对齐。
### 2.2 字号阶梯pt桌面端基准
| Token | 字号 | 字重 | 行高 | 用途 |
|---|---|---|---|---|
| `text/display` | 18 | 600 | 26 | 空状态大标题(少用) |
| `text/title` | 14 | 600 | 22 | 对话框标题、首选项分组标题 |
| `text/heading` | 12.5 | 600 | 20 | 面板标题栏(「对象显示栏」「属性」等) |
| `text/body` | 11.5 | 400 | 18 | 正文、列表项、表单值 |
| `text/body-strong` | 11.5 | 600 | 18 | 强调正文、选中项 |
| `text/label` | 11 | 400 | 16 | 表单标签、次要说明 |
| `text/caption` | 10 | 400 | 14 | 辅助信息(日期、计数、单位) |
| `text/mono` | 11 | 400 | 16 | 数值/坐标/编号 |
> 桌面端字号偏小(信息密度优先)。如需适配高 DPI按系统缩放因子整体放大不单独改 token。
---
## 3. 间距、圆角、阴影、边框Spacing / Radius / Elevation
### 3.1 间距阶梯px4 的倍数节奏)
| Token | 值 | 典型用途 |
|---|---|---|
| `space/3xs` | 2 | 图标与文字微距 |
| `space/2xs` | 4 | 紧凑控件内边距 |
| `space/xs` | 6 | 列表项上下内边距 |
| `space/sm` | 8 | 控件内边距、小间隔 |
| `space/md` | 12 | 面板内边距、表单行距 |
| `space/lg` | 16 | 分组间距 |
| `space/xl` | 24 | 对话框内边距 |
| `space/2xl` | 32 | 大区块分隔 |
### 3.2 圆角
| Token | 值 | 用途 |
|---|---|---|
| `radius/none` | 0 | 表格、树(贴合密集布局) |
| `radius/sm` | 4 | 按钮、输入框、标签 |
| `radius/md` | 6 | 卡片、列表项、浮层 |
| `radius/lg` | 8 | 对话框、画布浮窗 |
| `radius/pill` | 999 | 胶囊标签、开关、计数徽标 |
### 3.3 边框
- 默认边框宽度 `1px`,颜色 `border/default`
- 聚焦态边框 `1px` `border/focus` + 外发光 `0 0 0 2px primary-100`Light/ `primary-900 透明度` 描边Dark
- 分隔线用 `divider``1px`。
### 3.4 阴影(仅浮层使用,界面整体扁平)
| Token | Light | Dark |
|---|---|---|
| `shadow/popover` | `0 4px 16px rgba(20,30,50,0.12)` | `0 4px 16px rgba(0,0,0,0.5)` |
| `shadow/dialog` | `0 12px 40px rgba(20,30,50,0.18)` | `0 12px 40px rgba(0,0,0,0.6)` |
| `shadow/dropdown` | `0 2px 10px rgba(20,30,50,0.10)` | `0 2px 10px rgba(0,0,0,0.45)` |
> 树、列表、表格、面板**不使用阴影**扁平、紧凑。仅菜单、下拉、tooltip、对话框、画布浮窗使用。
---
## 4. 布局框架(依据原型还原)
原型即客户端「项目分析视图」工作台,整体为 **顶栏 + 三栏主体 + 中列上下分割** 的多面板结构(用 ADS 实现停靠)。
### 4.1 整体栅格
```
┌──────────────────────────────────────────────────────────────┐
│ 顶栏 TopBar (高 48px) │
├────────────┬───────────────────────────────┬─────────────────┤
│ 左栏 │ 中列上2D/3D 视图画布 │ 右栏上:异常列表 │
│ 对象显示栏 │ (深色 canvas含浮窗/工具) │ /对象属性 (Tab) │
│ (树) ├───────────────────────────────┤ │
│ │ 中列下:数据详情 ├─────────────────┤
│ 数据集显示栏 │ (Tab + 工具条 + 剖面图) │ 右栏下:属性 │
│ (列表) │ │ (键值表) │
└────────────┴───────────────────────────────┴─────────────────┘
```
### 4.2 各区尺寸建议
| 区域 | 尺寸 | 说明 |
|---|---|---|
| 顶栏 TopBar | 高 `48px` | 固定 |
| 左栏 | 默认宽 `280px`,可拖拽 `220400px` | 上下两段:对象树 + 数据集列表 |
| 右栏 | 默认宽 `340px`,可拖拽 `280460px` | 上下两段:异常/属性 Tab + 属性键值表 |
| 中列 | 自适应填充 | 上下分割:视图画布(占比大)+ 数据详情 |
| 面板标题栏 | 高 `36px` | 含图标 + 标题 + 右侧动作按钮 |
| 面板间分隔条splitter | `4px` 命中区,视觉 `1px` | hover 显示 `accent/primary` |
### 4.3 面板标题栏规范(所有可停靠面板统一)
- 左:`14px` 图标QtAwesome+ `text/heading` 标题,可带计数(如「异常列表 2/3」
- 右:动作按钮区(如 筛选漏斗、+、展开/折叠、全屏),图标按钮 `24×24px`hover 显示 `bg/hover` 底。
- 底部 `1px` `divider`
- 背景 `bg/panel`
---
## 5. 顶栏 TopBar依据原型
从左到右的元素与规范:
| 元素 | 规范 |
|---|---|
| **Logo** | 左起,高 `24px`,含下拉箭头(切换工作空间/企业) |
| **项目名 + 区域** | 图标 + 项目名(`text/body-strong`+ 区域下拉胶囊(`primary` 文字 + 浅底)|
| **主导航**(视图分析/项目管理/业务工具) | 文字 tab含图标当前项 `accent/primary` + 底部 `2px` 高亮条;业务工具带下拉箭头 |
| **右侧工具组** | 设备(主按钮样式,`accent/primary` 填充)、帮助(?)、通知(铃铛,含红点徽标)、设置(齿轮)、用户头像+姓名+角色 |
| 背景 | `bg/header`,底部 `1px` `divider` |
| 图标按钮尺寸 | `32×32px`hover `bg/hover` |
| 通知红点 | `8px` 圆点,`Danger` 色,右上角 |
---
## 6. 核心组件规范
### 6.1 对象树(左栏上 · 对象显示栏)
原型特征:多级树,每行含「复选框 + 状态圆点 + 类型图标 + 名称 + 右侧计数」。
| 元素 | 规范 |
|---|---|
| 行高 | `28px` |
| 缩进 | 每级 `16px`,展开箭头 `12px` |
| 复选框 | `14px`,选中 `accent/primary` 填充 + 白勾;三态(半选)用横线 |
| 状态圆点 | `8px` 实心圆,颜色映射对象状态/数据集色(蓝/绿/橙/红,与右侧异常色一致)|
| 类型图标 | `14px` QtAwesome`text/secondary` 色 |
| 名称 | `text/body`;选中行 `text/body-strong` |
| 计数徽标 | 右对齐,`text/caption``text/tertiary` 色;可加 `pill` 浅底 |
| 选中行 | 底 `bg/selected`,左侧 `2px` `accent/primary` 竖条 |
| hover 行 | 底 `bg/hover` |
| 分组节点GS | 名称略强,可加定位图标 |
### 6.2 数据集列表(左栏下 · 数据集显示栏)
原型特征:顶部 Tab数据 / 文件,带计数)+ 列表项(含状态点、标题、日期·通道数)。
| 元素 | 规范 |
|---|---|
| 段 Tab | 「数据 2 / 文件 3」胶囊式分段控件见 6.9|
| 列表项高 | `52px`(双行:标题 + 元信息)|
| 标题 | `text/body`,含前置状态圆点 |
| 元信息行 | `text/caption` `text/tertiary`如「2026-03-15 09:21 · 64 道」|
| 选中项 | `bg/selected` + 左 `2px` 竖条 + `radius/md` |
| 右上工具 | 筛选漏斗、导出图标 |
### 6.3 异常列表(右栏上 Tab1
原型特征:每条含左侧状态竖条 + 圆点 + 名称 + 等级标签 + 多行属性 + 右侧眼睛(显隐)。
| 元素 | 规范 |
|---|---|
| 卡片项 | `radius/md`,左 `3px` 状态色竖条,内距 `space/sm` |
| 卡片底 | 对应状态浅底(高=Danger 浅底、中=Warning 浅底、低=Info 浅底)|
| 名称 | `text/body-strong` + 圆点 |
| 等级标签 | 胶囊标签,状态色(见 6.8|
| 属性行 | `text/caption`如「140m · 18m / 32 Ω·m」数值用等宽 |
| 显隐开关 | 右侧眼睛图标,开=`text/secondary`,关=`text/disabled` + 斜杠眼 |
| 标题计数 | 「异常列表 2/3」=可见/总数 |
### 6.4 属性键值表(右栏下 · 属性)
原型特征:两列键值,左键右值,多组。
| 元素 | 规范 |
|---|---|
| 行高 | `28px` |
| 键 | 左列,`text/label` `text/secondary`,定宽约 `72px` |
| 值 | 右列,`text/body` `text/primary`,数值/日期用等宽;可右对齐 |
| 分组标题 | `text/heading`,上留 `space/md` |
| 可编辑值 | hover 显示编辑图标;进入编辑变为内联输入框 |
| 链接值 | 指向其他数据集的属性显示为 `text/link`,点击跳转新建详情页 |
### 6.5 视图画布(中列上 · 2D/3D
**始终深色**`bg/canvas`),不随模式切换变浅。
| 元素 | 规范 |
|---|---|
| 视图切换 | 左上「二维地图 / 三维视图」分段 tab深色版分段控件 |
| 画布浮窗(列表显示栏) | 左上浮层,底 `canvas-bg-soft` + `radius/lg` + `shadow/popover`,半透明 |
| 画布内文字 | `canvas-text` / `canvas-text-dim` |
| 标注牌ERT1 等) | 底 `canvas-overlay`,白字,`radius/sm` |
| 右上控件 | 底图切换下拉(天地图等)、缩放 +/-,深色按钮 |
| 右下状态条 | 坐标/比例尺,`canvas-text-dim`,等宽字体 |
| 缩放按钮 | `28×28px`深色半透明底hover 提亮 |
| 顶部状态徽标 | 如「3/4 测线可见」,圆点 + `text/caption` |
### 6.6 数据详情区(中列下 · 剖面图)
原型特征Tab采集批次+ 二级 Tab原数据/网格数据)+ 工具条 + 剖面图 + 色阶条。
| 元素 | 规范 |
|---|---|
| 标题栏 | 图标 +「数据详情」+ 右侧设置/下载图标 |
| 批次 Tab | 可关闭的多 Tab数据集详情以 Tab 呈现,见 6.10|
| 二级 Tab | 「原数据 / 网格数据」文字 tab |
| 工具条 | 一排工具按钮(异常标注/色阶配置/白化/滤波处理)= 次按钮(描边)样式,带图标;右侧为复选框组(显示异常/等值线等)+ 滑块(简化容差)+ 右端图标按钮(网格/另存为)|
| 复选框组 | 行内排列,`14px` 复选框 + `text/label` |
| 滑块 | 见 6.13 |
| 剖面图画布 | 深色衬底;色阶填充用数据色阶(见 §8|
| 色阶条legend | 底部水平色带 + 刻度(等宽数字)+ 单位标签 |
| 深度/距离轴 | 轴标题 `text/caption`,刻度等宽 |
### 6.7 按钮Button
| 类型 | Light | Dark | 用途 |
|---|---|---|---|
| **Primary** | 底 `accent/primary` 白字hover `accent/primary-hover` | 同(用 Dark 强调取值) | 设备、确定、主操作每个区域≤1个 |
| **Secondary次/描边)** | 描边 `border/strong` + `text/primary`hover `bg/hover` | 同 | 工具条按钮、取消 |
| **Tertiary文字/幽灵)** | 无边无底 `text/secondary`hover `bg/hover` | 同 | 次要动作、图标按钮 |
| **Danger** | 底/描边 `Danger` | 同 | 删除等破坏性操作 |
| **Link** | `text/link` 无底 | 同 | 内联跳转 |
- 高度:标准 `28px`,紧凑(工具条)`26px`,大(对话框主操作)`32px`。
- 内距:水平 `space/md`,图标与文字间距 `space/xs`
- 圆角 `radius/sm`。禁用态:`text/disabled` + 不变底色 + 光标禁用。
### 6.8 标签 / 徽标Tag / Badge
| 类型 | 规范 |
|---|---|
| 状态标签(高/中/低) | 胶囊 `radius/pill`,状态浅底 + 状态主色文字,`text/caption`,内距 `space/2xs space/sm` |
| 计数徽标 | 圆形/胶囊,`text/tertiary` + `neutral-100` 底;通知红点为纯 `Danger` 圆点 |
| 类型徽标 | 中性,`neutral-100` 底 + `text/secondary` |
### 6.9 分段控件Segmented / 视图切换、数据/文件 Tab
- 容器:`neutral-100` 底Dark `#1B2129`+ `radius/md`,内距 `2px`
- 选中段:`bg/panel` 底 + `text/primary` + 轻阴影(浮起感)。
- 未选段:透明 + `text/secondary`hover `text/primary`
- 深色画布上的分段控件(视图切换)用深色版:容器半透明深底,选中段 `accent/primary` 文字。
### 6.10 标签页Tabs · 数据集详情多 Tab
- 标签:`text/body`,当前项 `text/primary` + 底部 `2px` `accent/primary`;非当前 `text/secondary`
- 可关闭:每个 Tab 右侧 `×`hover 显示),底色 hover `bg/hover`
- 新建详情页 = 新增 Tab列表选中与 Tab 双向联动(选中 Tab 高亮)。
- 溢出:超出宽度显示左右滚动箭头或下拉列出全部 Tab。
### 6.11 大视图模式(剖面/属性页全屏)
- 触发后该详情页覆盖「标题菜单以下区域」,其余面板隐藏。
- 右上角显示「按 Esc 退出大视图」提示条(`canvas-overlay` 底,`text/caption`2 秒后淡出)。
### 6.12 复选框 / 单选 / 开关
| 控件 | 规范 |
|---|---|
| 复选框 | `14px`,未选描边 `border/strong`;选中 `accent/primary` 填充 + 白勾;半选横线;禁用降透明 |
| 单选框 | `14px` 圆,选中 `accent/primary` 圆环 + 实心点 |
| 开关 Switch | 宽 `36px``20px` 胶囊,关=`neutral-300` 底,开=`accent/primary` 底,滑块白色 `16px` |
### 6.13 滑块Slider · 如简化容差)
- 轨道 `4px` `neutral-200`Dark `#2A313B`),已填充段 `accent/primary`
- 滑块手柄 `14px` 白圆 + `1px` `border/strong` + 轻阴影hover 放大到 `16px`
- 当前值标签:右侧等宽数字。
---
## 7. 表单、表格、对话框等通用组件(补充 · 原型未全部出现但客户端必备)
### 7.1 输入框Text Input
| 状态 | 规范 |
|---|---|
| 默认 | 高 `28px`,底 `bg/panel`,描边 `1px border/default``radius/sm`,内距 `space/sm``text/body` |
| hover | 描边 `border/strong` |
| focus | 描边 `border/focus` + 外发光 |
| 禁用 | 底 `neutral-50`Dark `#1A1F26`+ `text/disabled` |
| 错误 | 描边 `Danger` + 下方 `text/caption` `Danger` 错误说明 |
| 占位符 | `text/disabled` |
| 前/后缀 | 单位、图标置于框内两端,`text/tertiary` |
### 7.2 下拉选择ComboBox / Select
- 外观同输入框 + 右侧 `12px` 下拉箭头。
- 展开菜单:`bg/panel` + `radius/md` + `shadow/dropdown`;项高 `28px`hover `bg/hover`,选中项 `bg/selected` + 勾。
- 可搜索下拉:顶部带搜索输入框。
- 多选下拉:选中项以 6.8 标签形式回填到框内。
### 7.3 数字输入 / 步进器SpinBox
- 输入框 + 右侧上下步进按钮(各 `14px` 高);数值等宽右对齐;支持单位后缀。
### 7.4 表格Table / DataGrid
| 元素 | 规范 |
|---|---|
| 表头 | 底 `neutral-100`Dark `#161B22``text/label` `text/secondary`,高 `32px`,可排序列带箭头 |
| 行高 | 标准 `32px`,紧凑 `28px` |
| 斑马纹 | 偶数行 `bg/panel-subtle`(可关闭) |
| 行 hover | `bg/hover` |
| 行选中 | `bg/selected` + 左 `2px` 竖条(多选用复选框列) |
| 单元格 | 内距 `space/sm`,文本 `text/body`,数值等宽右对齐 |
| 边框 | 仅横向 `1px divider`(无竖线,保持轻盈);密集模式可加竖线 |
| 固定列/表头 | 横向滚动时首列与表头固定 |
| 空状态 | 居中插画/图标 + `text/secondary` 说明 + 可选操作按钮 |
| 分页/加载 | 底部分页器或无限滚动;加载中显示骨架行 |
### 7.5 对话框Dialog / Modal
| 元素 | 规范 |
|---|---|
| 遮罩 | 全屏 `rgba(10,16,26,0.45)`Dark `rgba(0,0,0,0.6)` |
| 容器 | `bg/panel` + `radius/lg` + `shadow/dialog`;宽按内容(小 `420px` / 中 `560px` / 大 `720px` |
| 标题栏 | `text/title` + 右上 `×`;底 `1px divider`;内距 `space/xl` |
| 内容区 | 内距 `space/xl`,可滚动 |
| 底部操作栏 | 右对齐:次按钮(取消)+ 主按钮(确定);破坏性操作主按钮用 Danger内距 `space/lg space/xl` |
| 进入动画 | 淡入 + 轻微上移120ms |
### 7.6 确认 / 警示框Confirm / Alert
- 小对话框左侧状态图标Danger/Warning/Info 圆形图标)+ 标题 + 说明文 + 操作按钮。
- 删除类:图标 Danger主按钮 Danger文案明确后果。
### 7.7 提示框Toast / Notification
| 类型 | 规范 |
|---|---|
| Toast瞬时 | 右上或底部居中浮出,`bg/panel` + 状态色左竖条 + 图标 + 文案;`radius/md` + `shadow/popover`34s 自动消失;可手动关闭 |
| 行内提示Inline alert | 块状,状态浅底 + 状态色左竖条 + 图标 + 文案;用于表单顶部、面板内提示 |
| 通知中心(铃铛) | 顶栏铃铛下拉面板,列表项含图标/标题/时间/已读态;未读左侧圆点 |
### 7.8 Tooltip / Popover
- Tooltip深色小气泡Light 也用深色 `neutral-800` 底 + 白字),`text/caption``radius/sm`,延迟 `400ms` 显示。
- Popover`bg/panel` + `radius/md` + `shadow/popover`,可含富内容(如对象快速属性 tip
### 7.9 菜单(右键菜单 / 下拉菜单)
| 元素 | 规范 |
|---|---|
| 容器 | `bg/panel` + `radius/md` + `shadow/dropdown`,内距 `space/2xs` |
| 项 | 高 `28px`,左图标 `14px` + 文字 + 右侧快捷键(`text/tertiary` 等宽)|
| hover | `bg/hover` |
| 分隔 | `1px divider`,上下 `space/2xs` |
| 危险项 | `Danger` 文字(如删除)|
| 子菜单 | 右侧箭头hover 展开 |
| 禁用项 | `text/disabled` |
> 对象树/数据集右键菜单按规约功能:显示/隐藏、定位、属性、异常详情、编辑、新建 GS/TM、导入数据集、删除等。
### 7.10 首选项 / 设置Preferences
原型顶栏齿轮入口,客户端必备完整设置界面。
| 元素 | 规范 |
|---|---|
| 结构 | 左侧分类导航(垂直列表)+ 右侧设置项面板(主从布局)|
| 分类项 | 高 `32px`,图标 + 名称,选中 `bg/selected` + 左竖条 |
| 设置项行 | 左:标题(`text/body`+ 说明(`text/caption text/tertiary`);右:控件(开关/下拉/输入)|
| 分组 | 分组标题 `text/heading` + `divider` |
| 建议分类 | 外观(**主题:跟随系统/浅色/深色**、字体、字号缩放)、启动画面与停留时间、默认大屏、语言(中/英)、坐标系默认、底图与缓存、更新(检查/通道)、账户、关于(版本/许可/Qt 源码声明)|
### 7.11 空状态 / 加载 / 骨架屏
- **空状态**:居中 `48px` 灰度图标 + `text/body secondary` 主文案 + `text/caption` 辅助 + 可选主操作按钮。
- **加载**:局部用旋转 spinner`accent/primary`);区块用骨架屏(`neutral-100`/`#1B2129` 矩形微动画)。
- **进度**长任务如在线更新、VTK 大数据加载)用进度条 `accent/primary` + 百分比等宽数字。
### 7.12 滚动条
- 细滚动条:宽 `8px`thumb `scrollbar/thumb` + `radius/pill`hover `scrollbar/thumb-hover`轨道透明overlay 模式(悬停才显)。
### 7.13 树/列表筛选器(漏斗)
- 面板标题栏漏斗图标 → 弹出 Popover含搜索框 + 多选类型复选组 + 日期范围 + 「应用/重置」。
- 激活筛选时漏斗图标显示 `accent/primary` + 角标计数。
---
## 8. VTK 视图配色(与 UI 对齐)
> **关键约束**QSS/QPalette 不作用于 VTK 渲染窗口。VTK 画布配色须通过 VTK API 单独设置,并**与 §1.3 画布 token 手动对齐**避免「UI 深色、视图另一种灰」的割裂。VTK 画布在两种模式下**都用深色**。
### 8.1 渲染窗口基础
| VTK 元素 | 取值(对齐 token |
|---|---|
| 渲染器背景renderer background | `canvas-bg` `#0B1320`;可用上下渐变到 `#0E1626` |
| 三维网格/地面网格 | `canvas-grid` `#1E2A3D` |
| 坐标轴axes actor | X/Y/Z 用低饱和 红/绿/蓝,标签 `canvas-text-dim` |
| 文字标注vtkTextActor | `canvas-text` `#E6ECF5`,等宽字体 |
| 比例尺/方位 | `canvas-text-dim` |
| 拾取高亮 | `accent/primary` `#5E8DF5` 描边 |
| 选中对象包围盒 | `accent/primary` 虚线框 |
### 8.2 数据色阶Colormap / Lookup Table
原型剖面图色阶为**电阻率经典彩虹色阶**(紫蓝→青绿→黄→橙红→深红,对数刻度 51000 Ω·m。规范
| 色阶用途 | 推荐 colormap | 说明 |
|---|---|---|
| 电阻率/极化率(默认) | 彩虹/Jet 类(紫→蓝→青→绿→黄→橙→红) | 与原型一致;对数刻度;端点可标注异常 |
| 连续物理量(通用) | Viridis / Turbo | 感知均匀,科学制图推荐 |
| 发散量(正负、相对基线) | Coolwarm蓝-白-红) | 如电位差、变化量 |
| 地层/分类(离散) | 离散调色板(命名色板) | 类别明确,高区分度 |
| 单色强度 | 单色渐变(如蓝系) | 单一属性强度 |
- 色阶须**可配置、可命名保存**(规约「色阶定义工具」),用户调整后视图实时刷新。
- 色阶条legend渲染在 UI 侧或画布侧均可,但**刻度数字用等宽字体**,单位明确。
- 等值线contour默认 `canvas-text-dim` 细线;标注牌用 `canvas-overlay` 底。
### 8.3 异常标注(三维视图 + 剖面图)
- 异常圈/标注牌颜色 = §1.4 异常分级色(高=Danger、中=Warning、低=Info与右栏异常列表**严格一致**。
- 三维视图中的测线:可见用亮色(红/绿区分极性或测线编号),不可见降透明。
---
## 9. 图标系统QtAwesome
- **统一用 QtAwesome**(图标字体),全局禁止混用位图图标,保证矢量、可染色、随主题变色。
- 图标字体集:优先 **Material Design Icons (mdi)****Font Awesome**,全项目统一一套。
- 标准尺寸:行内 `14px`,按钮内 `16px`,面板标题 `14px`,空状态 `48px`
- 默认色 `text/secondary`;激活/选中 `accent/primary`;禁用 `text/disabled`
- 图标语义映射(建议固定,供 Claude Code 一致引用):
| 语义 | 图标mdi 名参考) |
|---|---|
| 对象树/层级 | `file-tree` / `sitemap` |
| GS 分组/定位 | `map-marker` |
| 数据集 | `database` / `chart-line` |
| 文件 | `file-document` |
| 二维地图 | `map` |
| 三维视图 | `cube-outline` |
| 异常 | `alert-circle` |
| 属性 | `information-outline` |
| 筛选 | `filter-variant` |
| 新增 | `plus` |
| 导出/下载 | `download` / `export` |
| 显隐 | `eye` / `eye-off` |
| 全屏 | `fullscreen` / `fullscreen-exit` |
| 设置 | `cog` |
| 通知 | `bell` |
| 帮助 | `help-circle` |
| 设备 | `access-point` / `chip` |
| 色阶配置 | `palette` |
| 白化/滤波 | `blur` / `wave` |
| 网格 | `grid` |
| 缩放 | `magnify-plus` / `magnify-minus` |
---
## 10. 状态与交互反馈
| 交互态 | 表现 |
|---|---|
| hover | 背景 `bg/hover` 或控件提亮,`120ms` 过渡 |
| pressed | 加深一档 |
| focus键盘 | `border/focus` + 外发光,**键盘可达性必须保留** |
| selected | `bg/selected` + 左竖条 |
| disabled | `text/disabled` + 降透明 + 禁用光标 |
| loading | spinner / 骨架 / 进度条 |
| drag异常合并、图层排序 | 拖拽项半透明跟随 + 目标位置 `accent/primary` 插入线 |
| 实时刷新(色阶/图例调整) | 视图即时重绘,无整页闪烁 |
> 动效克制:仅用 `120200ms` 的 hover/展开/淡入过渡,专业工具避免花哨动画。
---
## 11. 双模式切换规范
- 三种选项:**跟随系统 / 浅色 / 深色**(首选项 §7.10 + 顶栏快捷切换可选)。
- 切换时:外围 UI顶栏、面板、树、列表、表单、对话框整体换肤**视图画布与剖面图保持深色基调不变**(仅 UI 边框等微调)。
- 切换应即时、无重启;通过统一主题对象广播刷新所有 widget 的 QSS + QPalette。
- VTK 渲染器背景在两模式下均深色,不参与切换(仅当用户在首选项单独设置画布背景时才改)。
---
## 12. 可访问性与适配
- **对比度**:正文文字与背景对比度 ≥ 4.5:1大字 ≥ 3:1两种模式都须校验Dark 模式尤其注意状态浅底上的文字)。
- **不以颜色为唯一信息**:异常分级除颜色外必须带文字标签(高/中/低)或图标,照顾色觉障碍。
- **高 DPI**:所有尺寸用逻辑像素,随系统缩放因子整体放大;图标用矢量字体。
- **键盘导航**Tab 焦点环必须可见;树/列表/表格支持方向键。
- **最小命中区**:可点击图标按钮命中区 ≥ `24×24px`
---
## 13. 实现约定(给 Claude Code
1. **集中定义 token**:建一个主题模块(如 `src/ui/theme/`),包含 `Theme` 对象,集中持有所有颜色/间距/字号 token 的两套取值light/dark。所有 widget 通过 `theme.color("bg/panel")` 一类接口取值,**禁止散落硬编色值**。
2. **QSS 模板化**QSS 写成带占位符的模板,运行时用当前 token 填充生成最终 QSS 字符串并 `qApp->setStyleSheet()`;切换模式时重新生成并应用。
3. **QPalette 同步**:除 QSS 外**必须同步设置 QPalette**Window/Base/Text/Highlight/ButtonText 等角色否则原生绘制控件菜单、tooltip、部分原生项颜色不统一。两者取值来自同一 token。
4. **Fusion 为底座**`QApplication::setStyle("Fusion")`,在其上叠加 QSS + QPalette不替换为第三方 QStyle。
5. **VTK 配色独立设置**VTK 渲染器背景、坐标轴、文字、色阶用 VTK API 设置,取值引用 §8 / §1.3,与 UI token 对齐;模式切换时画布保持深色。
6. **图标统一走 QtAwesome**:封装一个图标工具函数 `icon(name, role)`role 决定染色(默认/激活/禁用/状态色),全局复用 §9 映射。
7. **主题切换广播**:主题切换时通过信号通知所有顶层窗口重应用 QSS/QPalette 并刷新 QtAwesome 图标颜色。
8. **可视化校验**:实现后用 light/dark 各截图核对本规范(对比度、状态色一致性、视图画布与 UI 的衔接)。
---
## 附录 A核心语义色速查开发对照
### Light
| 用途 | 色值 |
|---|---|
| 应用背景 | `#F7F8FA` |
| 面板背景 | `#FFFFFF` |
| hover | `#EFF1F4` |
| 选中 | `#EFF5FF` |
| 边框 | `#E3E6EB` |
| 主文字 | `#272C35` |
| 次文字 | `#5A626F` |
| 主强调 | `#3B73EC` |
| 视图画布 | `#0B1320` |
| 高/Danger | `#E5484D` |
| 中/Warning | `#E08A1E` |
| 低/Info | `#3B73EC` |
| 成功 | `#2E9E5B` |
### Dark
| 用途 | 色值 |
|---|---|
| 应用背景 | `#0E1116` |
| 面板背景 | `#161A20` |
| hover | `#1B2129` |
| 选中 | `#16243F` |
| 边框 | `#262C35` |
| 主文字 | `#E6E9EF` |
| 次文字 | `#A4ADBB` |
| 主强调 | `#5E8DF5` |
| 视图画布 | `#0B1320`(同 Light |
| 高/Danger | `#FF6166` |
| 中/Warning | `#F5A623` |
| 低/Info | `#5E8DF5` |
| 成功 | `#46C07A` |
---
*本规范依据客户提供的 Web 原型(浅色)提取并派生深色模式,覆盖项目分析视图工作台及客户端通用组件。色值为初始建议值,落地后应结合实机截图微调对比度与一致性,并随设计迭代维护版本。*

View File

@ -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。每步构建通过后由用户运行+截图确认再进下一步。

View File

@ -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 + Ninjapreset `msvc-release`)。
- 在 PowerShell 下 `& .\build.bat app` 会打印一行 `vswhere.exe is not recognized` 的 stderr 噪声,但 ninja 仍会运行——以最终的 ninja/cl 输出与 exit code 为准,不要被该行误导。

View File

@ -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()` APIQSS 模板用 `{{token}}` 占位,运行时按当前明暗填充;`QPalette` 同一令牌表构建;主题切换时广播重填。逐步把内联 QSS 调用方迁移到令牌,最后删除遗留的 `kDarkMap` 字符串替换路径。
**Tech Stack:** C++17 / Qt 6 (QtWidgets) / Fusion QStyle + QSS + QPalette / VTK / ADS。构建`build.bat app`MSVC + NinjaCMake 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: 规范一致性校验(门禁)**
派 subagentopus执行读取 `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 45 迁移完后于 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: 终版规范一致性总校验(门禁)**
派 subagentopus做一次全量复核`docs/Geopro3.0_视觉设计规范.md` 为准,遍历本计划范围内的所有改动文件,产出一份「规范条款 → 实现位置 → 一致/偏离」对照表。偏离项须落在 Task 0 记录的三条「有意偏离」内,否则修正。
- [ ] **Step 3: 用户最终验收截图**(浅/深 × 工作台全貌 + 各面板)
- [ ] **Step 4: Commit 复核报告**
```bash
git add docs/superpowers/plans/
git commit -m "docs: 设计规范落地终版一致性复核报告"
```

18
spike/ela/CMakeLists.txt Normal file
View File

@ -0,0 +1,18 @@
# ElaWidgetTools spike demo exe feat/elawidgettools
# geopro_desktop ElaWidgetTools(Fluent ) + ADS + VTK
add_executable(geopro_ela_spike WIN32 main.cpp)
target_link_libraries(geopro_ela_spike PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets
ElaWidgetTools
ads::qt6advanceddocking
${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_ela_spike MODULES ${VTK_LIBRARIES})
if(WIN32)
add_custom_command(TARGET geopro_ela_spike POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:geopro_ela_spike> $<TARGET_FILE_DIR:geopro_ela_spike>
COMMAND_EXPAND_LISTS)
endif()

121
spike/ela/main.cpp Normal file
View File

@ -0,0 +1,121 @@
// ElaWidgetTools 评估 spike隔离 demo不属于产品 app仅 feat/elawidgettools 分支评估用)。
// 一锤定音验证四件事:
// ① 用你们官方 Qt 6.11.1 + MSVC 经 FetchContent 能否构建 ElaWidgetTools(RainbowCandyX fork)
// ② ElaWindow 的 Fluent 观感在你们机器上渲染是否正常(重点看 Qt6.11 的 Popup/弹窗);
// ③ Qt Advanced Docking System(ADS) 能否内嵌进 ElaWindow
// ④ QVTKOpenGLStereoWidget 视口在 ElaWindow + ADS 内能否正常渲染。
// 结论决定是否值得对真实 app 做外壳重构。
#include <QApplication>
#include <QLabel>
#include <QSurfaceFormat>
#include <QVBoxLayout>
#include <QWidget>
#include "ElaApplication.h"
#include "ElaDef.h"
#include "ElaPushButton.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaWindow.h"
#include <DockManager.h>
#include <DockWidget.h>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkConeSource.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkNew.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderer.h>
int main(int argc, char* argv[])
{
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
eApp->init(); // ElaApplication 初始化Fluent 主题/字体/动画基建)
ElaWindow window;
window.setWindowTitle(QStringLiteral("ElaWidgetTools Spike — Fluent + ADS + VTK"));
window.resize(1200, 760);
// 「工作台」页内嵌 ADS 停靠管理器:验证 ADS 能否在 ElaWindow 内正常工作。
auto* dockHost = new QWidget;
auto* hostLay = new QVBoxLayout(dockHost);
hostLay->setContentsMargins(0, 0, 0, 0);
auto* dockManager = new ads::CDockManager(dockHost);
hostLay->addWidget(dockManager);
// dock1Fluent 控件样例看观感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);
// dock2QVTK 视口(蓝色锥体),验证 VTK 在 ElaWindow + ADS 内渲染。
auto* vtkWidget = new QVTKOpenGLStereoWidget;
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
vtkNew<vtkRenderer> renderer;
renderer->SetBackground(1.0, 1.0, 1.0);
vtkWidget->setRenderWindow(renderWindow);
renderWindow->AddRenderer(renderer);
vtkNew<vtkConeSource> cone;
cone->SetResolution(48);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(cone->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(0.18, 0.42, 0.71); // 品牌蓝 #2D6CB5 近似
renderer->AddActor(actor);
renderer->ResetCamera();
auto* d2 = new ads::CDockWidget(QStringLiteral("VTK 视口(锥体)"));
d2->setWidget(vtkWidget);
dockManager->addDockWidget(ads::RightDockWidgetArea, d2);
// 关键发现演示ADS 停靠区与普通 QWidget 不会自动跟随 ElaTheme只有 Ela* 控件与
// ElaWindow 外壳跟随)。这里手动把「停靠区背景 + 普通容器 + VTK 背景」同步到当前主题,
// 并监听 ElaTheme::themeModeChanged。这段「同步」正是真集成时要为每个非 Ela 面板付出的成本。
auto* rendererPtr = renderer.Get();
auto* rwPtr = renderWindow.Get();
auto applyContentTheme = [dockManager, sample, rendererPtr, rwPtr](ElaThemeType::ThemeMode mode) {
const bool dark = (mode == ElaThemeType::Dark);
const QString bg = dark ? QStringLiteral("#1E1F22") : QStringLiteral("#FFFFFF");
const QString fg = dark ? QStringLiteral("#E3E3E3") : QStringLiteral("#1F2A3D");
dockManager->setStyleSheet(
QStringLiteral("ads--CDockAreaWidget, ads--CDockContainerWidget { background:%1; }")
.arg(bg));
sample->setStyleSheet(
QStringLiteral("#sampleHost { background:%1; } #sampleHost QLabel { color:%2; }")
.arg(bg, fg));
rendererPtr->SetBackground(dark ? 0.11 : 1.0, dark ? 0.12 : 1.0, dark ? 0.14 : 1.0);
rwPtr->Render();
};
QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager,
[applyContentTheme](ElaThemeType::ThemeMode m) { applyContentTheme(m); });
applyContentTheme(eTheme->getThemeMode()); // 初始同步当前主题
window.addPageNode(QStringLiteral("工作台"), dockHost);
window.show();
return app.exec();
}

View File

@ -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
@ -51,4 +52,14 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:geopro_desktop> $<TARGET_FILE_DIR:geopro_desktop>
COMMAND_EXPAND_LISTS)
# Qt platforms imageformats/iconengines ElaWidgetTools SVG
# styles Qt6::Core Qt plugins exe windeployqt
# ADS Qt DLL copy
foreach(_pl platforms styles imageformats iconengines)
add_custom_command(TARGET geopro_desktop POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"$<TARGET_FILE_DIR:Qt6::Core>/../plugins/${_pl}"
"$<TARGET_FILE_DIR:geopro_desktop>/${_pl}")
endforeach()
endif()

View File

@ -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;
// 背景永远深色规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。
double bgR, bgG, bgB;
geopro::app::vtkBackground(bgR, bgG, bgB);
renderer->SetBackground(bgR, bgG, bgB);
for (const auto& s : sections) {
if (is2D) {

View File

@ -1,16 +1,22 @@
#include "Glyphs.hpp"
#include <QAbstractButton>
#include <QByteArray>
#include <QDir>
#include <QImage>
#include <QLabel>
#include <QObject>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QSvgRenderer>
#include "Theme.hpp"
namespace geopro::app {
namespace {
@ -93,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("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
@ -105,12 +111,13 @@ QIcon makeGlyph(Glyph type, const QColor& color, int px)
// 以 3x 超采样渲染再设 devicePixelRatio保证在任意缩放/DPI 下都清晰。
constexpr qreal kSuper = 3.0;
const int dim = qRound(px * kSuper);
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
const int dim = qRound(px * kSuper); // 图标本体边长(方形)
const int dimW = qRound((px + padRight) * kSuper); // 含右透明内边距的画布宽
QImage img(dimW, dim, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&p, QRectF(0, 0, dim, dim));
renderer.render(&p, QRectF(0, 0, dim, dim)); // 图标居左方形渲染,右侧 padRight 留透明
p.end();
QPixmap pm = QPixmap::fromImage(img);
@ -150,4 +157,74 @@ 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()
{
return isDarkTheme() ? QColor(0xE6, 0xE8, 0xEB) : QColor(0x1F, 0x2A, 0x3D); // 主文字明/暗
}
} // namespace
void setThemedGlyph(QLabel* label, Glyph type, int px)
{
if (!label) return;
auto apply = [label, type, px]() {
label->setPixmap(makeGlyph(type, themedIconColor(), px).pixmap(px, px));
};
apply();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, label, [apply]() { apply(); });
}
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight)
{
if (!button) return;
auto apply = [button, type, px, padRight]() {
button->setIcon(makeGlyph(type, themedIconColor(), px, padRight));
if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推
};
apply();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
}
} // namespace geopro::app

View File

@ -7,6 +7,9 @@
#include <QIcon>
#include <QString>
class QLabel;
class QAbstractButton;
namespace geopro::app {
enum class Glyph {
@ -31,11 +34,27 @@ 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;
// 生成指定颜色、像素尺寸的图标(默认 16px内部按 3x 绘制保证清晰)。
// padRight>0 时图标画布右侧留透明内边距(用于「图标+文字」按钮统一间距),图标本体仍为 px×px 居左。
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16, int padRight = 0);
// 随主题明暗自动着色的 glyph取主题文本色暗色用浅色、亮色用深色主题切换时自动重绘。
// 用于面板表头/页签等 chrome 图标,避免固定色在暗色下看不清。
// 按钮版的 padRight见 kGlyphTextGapPad>0 时本函数同时把 iconSize 设为 (px+padRight)×px。
void setThemedGlyph(QLabel* label, Glyph type, int px);
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight = 0);
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
// 配合 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

View File

@ -1,5 +1,7 @@
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include <QButtonGroup>
#include <QColor>
#include <QHBoxLayout>
@ -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; }"
// 表头统一样式(标准表头 + Tab 表头共用)。字号/字重引用 Theme 排版令牌:
// 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。
// #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色),
// 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。
// 表头底/标题/徽标 + 页签样式。页签选中态 = 强调色文字 + 2px 强调色下划线,
// 与视图/详情工具条完全一致(全 UI 切换控件统一这一套)。操作按钮(ElaIconButton)自绘 Fluent。
QString headerQss()
{
return QStringLiteral(
"#panelHeader { background:{{bg/panel}}; border-bottom:1px solid {{divider}}; }"
"#panelTitle { color:{{text/primary}}; font-size:%1px; font-weight:%3; }"
"#panelBadge { background:{{bg/hover}}; color:{{text/secondary}}; border-radius:9px;"
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
"#panelBadgeWarn { background:{{status/warning-bg}}; color:{{status/warning}}; border-radius:9px;"
" padding:1px 7px; font-size:%2px; font-weight:%3; }"
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
"QToolButton#panelAction: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; }";
"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 显示)。
QLabel* makeBadge(QWidget* parent)
@ -45,12 +61,12 @@ QLabel* makeBadge(QWidget* parent)
return badge;
}
// 表头操作按钮(静态占位)。
QToolButton* makeActionButton(QWidget* parent, const HeaderAction& a)
// 表头操作按钮(QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #panelAction QSS 给)。
QWidget* makeActionButton(QWidget* parent, const HeaderAction& a)
{
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("panelAction"));
btn->setIcon(makeGlyph(a.first, QColor("#5A6B85"), kActionIcon));
setThemedGlyph(btn, a.first, kActionIcon);
btn->setIconSize(QSize(kActionIcon, kActionIcon));
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(a.second + QStringLiteral("(占位)"));
@ -65,14 +81,14 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<Header
auto* header = new QWidget();
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
geopro::app::applyTokenizedStyleSheet(header, headerQss());
auto* lay = new QHBoxLayout(header);
lay->setContentsMargins(12, 0, 8, 0);
lay->setSpacing(8);
lay->setSpacing(geopro::app::space::kSm);
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);
@ -98,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
auto* header = new QWidget(box);
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
geopro::app::applyTokenizedStyleSheet(header, headerQss());
auto* hlay = new QHBoxLayout(header);
hlay->setContentsMargins(10, 0, 8, 0);
hlay->setSpacing(2);
@ -112,15 +128,13 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
for (int i = 0; i < tabs.size(); ++i) {
const PanelTab& t = tabs[i];
auto* btn = new QToolButton(header);
auto* btn = new QToolButton(header); // 页签与工具条统一: QToolButton + 强调色下划线 QSS
btn->setObjectName(QStringLiteral("tabBtn"));
btn->setText(t.title);
btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), 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);
btn->setAutoRaise(true);
group->addButton(btn, i);
hlay->addWidget(btn);
@ -148,4 +162,41 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
return result;
}
SegmentedHeader buildSegmentedHeader(const QVector<QString>& segments,
const QVector<HeaderAction>& actions)
{
auto* header = new QWidget();
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
geopro::app::applyTokenizedStyleSheet(header, headerQss());
auto* hlay = new QHBoxLayout(header);
hlay->setContentsMargins(10, 0, 8, 0);
hlay->setSpacing(2);
auto* group = new QButtonGroup(header);
group->setExclusive(true);
SegmentedHeader result;
result.header = header;
for (int i = 0; i < segments.size(); ++i) {
auto* btn = new QToolButton(header); // 与异常/属性页签统一: tabBtn 样式 + 强调色下划线
btn->setObjectName(QStringLiteral("tabBtn"));
btn->setText(segments[i]);
btn->setCheckable(true);
btn->setCursor(Qt::PointingHandCursor);
group->addButton(btn, i);
hlay->addWidget(btn);
hlay->addSpacing(10);
result.buttons.append(btn);
}
hlay->addStretch();
for (const auto& a : actions) hlay->addWidget(makeActionButton(header, a));
if (!result.buttons.isEmpty()) result.buttons[0]->setChecked(true);
return result;
}
} // namespace geopro::app

View File

@ -14,6 +14,7 @@
class QWidget;
class QLabel;
class QToolButton;
namespace geopro::app {
@ -43,4 +44,16 @@ struct TabbedPanel {
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs,
const QVector<HeaderAction>& actions = {});
// 分段切换表头构建结果:表头容器 + 各分段按钮(互斥,首个默认激活,供调用方接 clicked
struct SegmentedHeader {
QWidget* header;
QVector<QToolButton*> buttons;
};
// 构建「分段切换表头」:一行 Tab 风格互斥按钮(与异常/属性页签同款:选中=强调色文字 + 2px
// 强调色下划线)+ 右侧操作按钮。表头底/高度/边框与 buildTabbedPanel 完全一致;内容由调用方
// 自行 addWidget 到表头下方(不建堆叠,因 2D/3D 共用同一画布部件)。
SegmentedHeader buildSegmentedHeader(const QVector<QString>& segments,
const QVector<HeaderAction>& actions = {});
} // namespace geopro::app

View File

@ -3,6 +3,7 @@
#include <QAbstractItemView>
#include <QColor>
#include <QComboBox>
#include <QFont>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
@ -13,6 +14,9 @@
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include "Theme.hpp"
namespace geopro::app {
namespace {
QString statusText(int s) {
@ -22,6 +26,15 @@ QString statusText(int s) {
default: return QString::number(s);
}
}
// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。
QColor statusColor(int s) {
switch (s) {
case 1: return tokenColor("text/tertiary"); // 未开始:弱化
case 2: return tokenColor("accent/primary"); // 进行中:活动中
default: return tokenColor("text/secondary"); // 未知:中性
}
}
} // namespace
ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent)
@ -50,7 +63,7 @@ ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* pa
filter->addStretch();
root->addLayout(filter);
table_ = new QTableWidget(this);
table_ = new QTableWidget(this); // Ela item 版表格(继承 QTableWidget),直替
table_->setColumnCount(8);
table_->setHorizontalHeaderLabels(QStringList{
QStringLiteral("序号"), QStringLiteral("项目名称"), QStringLiteral("项目编号"),
@ -142,10 +155,18 @@ 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));
set(3, statusText(p.status));
// 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。
auto* statusItem = new QTableWidgetItem(statusText(p.status));
statusItem->setForeground(statusColor(p.status));
if (p.status == 2) {
QFont f = statusItem->font();
f.setBold(true);
statusItem->setFont(f);
}
table_->setItem(i, 3, statusItem);
set(4, QString::fromStdString(p.typeName));
set(5, QString::fromStdString(p.ownerCompany));
set(6, QString::fromStdString(p.responsiblePerson));

157
src/app/SettingsDialog.cpp Normal file
View File

@ -0,0 +1,157 @@
#include "SettingsDialog.hpp"
#include <QComboBox>
#include <QCoreApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QProcess>
#include <QPushButton>
#include <QStackedWidget>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QWidget>
#include "Theme.hpp"
namespace geopro::app {
namespace {
// 「标签 + 控件」一行(标签定宽左对齐,控件右随)。
QWidget* makeRow(const QString& label, QWidget* control) {
auto* row = new QWidget();
auto* lay = new QHBoxLayout(row);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(12);
auto* lbl = new QLabel(label, row);
lbl->setMinimumWidth(96);
lay->addWidget(lbl);
lay->addWidget(control, 1);
return row;
}
// 区段标题。
QLabel* sectionTitle(const QString& text, QWidget* parent) {
auto* t = new QLabel(text, parent);
t->setStyleSheet(QStringLiteral("font-size:%1px; font-weight:600;")
.arg(geopro::app::scaledPx(geopro::app::type::kHeading)));
return t;
}
QWidget* buildAppearancePage() {
auto* page = new QWidget();
auto* v = new QVBoxLayout(page);
v->setContentsMargins(24, 20, 24, 20);
v->setSpacing(16);
v->addWidget(sectionTitle(QStringLiteral("外观"), page));
// 主题:跟随系统 / 浅色 / 深色(热切)。
auto* themeCombo = new QComboBox(page);
themeCombo->addItem(QStringLiteral("跟随系统"), QStringLiteral("system"));
themeCombo->addItem(QStringLiteral("浅色"), QStringLiteral("light"));
themeCombo->addItem(QStringLiteral("深色"), QStringLiteral("dark"));
const QString curTheme = geopro::app::themeModePreference();
themeCombo->setCurrentIndex(themeCombo->findData(curTheme) >= 0 ? themeCombo->findData(curTheme) : 0);
QObject::connect(themeCombo, &QComboBox::activated, page, [themeCombo](int) {
geopro::app::setThemeModePreference(themeCombo->currentData().toString());
});
v->addWidget(makeRow(QStringLiteral("主题"), themeCombo));
// 界面字号:小/标准/大/特大(重启生效)。
auto* fontCombo = new QComboBox(page);
fontCombo->addItem(QStringLiteral(""), 90);
fontCombo->addItem(QStringLiteral("标准"), 100);
fontCombo->addItem(QStringLiteral(""), 115);
fontCombo->addItem(QStringLiteral("特大"), 130);
const int curScale = geopro::app::fontScalePreference();
fontCombo->setCurrentIndex(fontCombo->findData(curScale) >= 0 ? fontCombo->findData(curScale) : 1);
v->addWidget(makeRow(QStringLiteral("界面字号"), fontCombo));
// 字号改动:持久化 + 提示重启(提供立即重启)。
auto* restartRow = new QWidget(page);
auto* rlay = new QHBoxLayout(restartRow);
rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐
rlay->setSpacing(10);
auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow);
geopro::app::applyTokenizedStyleSheet(
hint, QStringLiteral("color:{{text/secondary}}; font-size:%1px;")
.arg(geopro::app::scaledPx(geopro::app::type::kCaption)));
auto* restartBtn = new QPushButton(QStringLiteral("立即重启"), restartRow);
rlay->addWidget(hint);
rlay->addWidget(restartBtn);
rlay->addStretch();
restartRow->setVisible(false);
v->addWidget(restartRow);
QObject::connect(fontCombo, &QComboBox::activated, page, [fontCombo, restartRow](int) {
geopro::app::setFontScalePreference(fontCombo->currentData().toInt());
restartRow->setVisible(true);
});
QObject::connect(restartBtn, &QPushButton::clicked, restartBtn, [] {
QProcess::startDetached(QCoreApplication::applicationFilePath(),
QCoreApplication::arguments().mid(1));
qApp->quit();
});
v->addStretch();
return page;
}
QWidget* buildAboutPage() {
auto* page = new QWidget();
auto* v = new QVBoxLayout(page);
v->setContentsMargins(24, 20, 24, 20);
v->setSpacing(12);
v->addWidget(sectionTitle(QStringLiteral("关于"), page));
auto* ver = new QLabel(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"), page);
ver->setStyleSheet(QStringLiteral("font-size:%1px; font-weight:600;")
.arg(geopro::app::scaledPx(geopro::app::type::kTitle)));
v->addWidget(ver);
auto* license = new QTextBrowser(page);
license->setOpenExternalLinks(true);
license->setHtml(QStringLiteral(
"<b>第三方组件与许可证</b>"
"<table cellpadding='3' style='margin-top:6px'>"
"<tr><td>Qt 6</td><td>GUI 框架</td><td>LGPL-3.0</td></tr>"
"<tr><td>VTK 9</td><td>二维/三维渲染</td><td>BSD-3-Clause</td></tr>"
"<tr><td>Qt-Advanced-Docking-System</td><td>停靠布局</td><td>LGPL-2.1</td></tr>"
"<tr><td>QtKeychain</td><td>凭证安全存取</td><td>BSD-3-Clause</td></tr>"
"</table>"
"<p style='margin-top:8px'>完整声明见随附 <i>NOTICE.md</i>。</p>"));
v->addWidget(license, 1);
return page;
}
} // namespace
SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) {
setWindowTitle(QStringLiteral("设置"));
resize(720, 480);
auto* root = new QHBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// 左:分类列表。
auto* sidebar = new QListWidget(this);
sidebar->setObjectName(QStringLiteral("settingsSidebar"));
sidebar->setFixedWidth(150);
sidebar->addItem(QStringLiteral("外观"));
sidebar->addItem(QStringLiteral("关于"));
root->addWidget(sidebar);
// 右:分页。
auto* stack = new QStackedWidget(this);
stack->addWidget(buildAppearancePage());
stack->addWidget(buildAboutPage());
root->addWidget(stack, 1);
QObject::connect(sidebar, &QListWidget::currentRowChanged, stack,
&QStackedWidget::setCurrentIndex);
sidebar->setCurrentRow(0);
}
} // namespace geopro::app

View File

@ -0,0 +1,15 @@
#pragma once
#include <QDialog>
namespace geopro::app {
// 设置对话框(左分类 + 右内容页,参考常见客户端布局):
// 外观 —— 主题(跟随系统/浅色/深色,热切) + 界面字号(小/标准/大/特大,重启生效)
// 关于 —— 版本号 + 第三方组件与许可证
class SettingsDialog : public QDialog {
Q_OBJECT
public:
explicit SettingsDialog(QWidget* parent = nullptr);
};
} // namespace geopro::app

View File

@ -1,102 +1,176 @@
#include "Theme.hpp"
#include "Glyphs.hpp"
#include <QApplication>
#include <QColor>
#include <QFont>
#include <QObject>
#include <QPalette>
#include <QProxyStyle>
#include <QSettings>
#include <QStyleFactory>
#include <QStyleHints>
#include <QWidget>
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);
}
};
// ── 语义令牌表(全 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 原生绘制,
// 它会自动采用调色板的 Highlight(#2D6CB5) 作勾选色,省去打包图片资源。
// 它会自动采用调色板的 Highlight(accent/primary) 作勾选色,省去打包图片资源。
const char* kStyleSheet = R"QSS(
/* ── 基础 ───────────────────────────────────────────────── */
QWidget {
color: #1F2A3D;
color: {{text/primary}};
}
QMainWindow, QDialog {
background: #F4F6FA;
}
QToolTip {
background: #1F2A3D;
color: #F4F6FA;
border: 1px solid #2D6CB5;
border-radius: 4px;
padding: 4px 8px;
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: 7px;
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;
}
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
padding: 7px 8px;
border-radius: 6px;
margin: 1px 4px;
padding: 4px 8px;
}
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}};
}
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——一旦改写 branchQt 会停止绘制
/ 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;
}
@ -104,82 +178,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: 5px 8px;
selection-background-color: #2D6CB5;
selection-color: #FFFFFF;
padding: 6px 8px;
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}};
}
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
@ -189,12 +263,12 @@ QScrollBar:vertical {
margin: 2px;
}
QScrollBar::handle:vertical {
background: #C2CCDA;
border-radius: 5px;
background: {{scrollbar/thumb}};
border-radius: 6px;
min-height: 28px;
}
QScrollBar::handle:vertical:hover {
background: #A7B4C7;
background: {{scrollbar/thumb-hover}};
}
QScrollBar:horizontal {
background: transparent;
@ -202,12 +276,12 @@ QScrollBar:horizontal {
margin: 2px;
}
QScrollBar::handle:horizontal {
background: #C2CCDA;
border-radius: 5px;
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;
@ -219,98 +293,111 @@ 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: #EDF1F7;
color: #1F2A3D;
border-bottom: 1px solid #D5DBE5;
background: {{bg/panel}};
color: {{text/primary}};
border-bottom: 1px solid {{divider}};
padding: 2px 6px;
}
QMenuBar::item {
background: transparent;
padding: 5px 12px;
padding: 6px 12px;
border-radius: 6px;
}
QMenuBar::item:selected {
background: #DCE6F4;
background: {{bg/hover}};
color: {{accent/primary}};
}
QMenuBar::item:pressed {
background: {{bg/selected}};
}
QMenu {
background: #FFFFFF;
color: #1F2A3D;
border: 1px solid #D5DBE5;
border-radius: 8px;
padding: 5px;
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
padding: 4px;
}
QMenu::item {
padding: 6px 24px 6px 14px;
border-radius: 5px;
border-radius: 6px;
}
QMenu::item:selected {
background: #DCE9F8;
color: #1B3D67;
background: {{bg/hover}};
color: {{accent/primary}};
}
QMenu::separator {
height: 1px;
background: #E1E6EE;
margin: 5px 8px;
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: 5px 10px;
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;
border-radius: 6px;
selection-background-color: #DCE9F8;
selection-color: #1B3D67;
background: {{bg/panel}};
border: 1px solid {{border/default}};
outline: none;
padding: 2px;
}
QComboBox QAbstractItemView::item {
border: none;
padding: 6px 10px;
min-height: 20px;
color: {{text/primary}};
}
QComboBox QAbstractItemView::item:selected {
background: {{bg/hover}};
color: {{accent/primary}};
}
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
QGroupBox {
border: 1px solid #D5DBE5;
border: 1px solid {{border/default}};
border-radius: 8px;
margin-top: 10px;
padding-top: 6px;
@ -320,109 +407,267 @@ 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: 5px;
border-radius: 6px;
height: 8px;
text-align: center;
color: #5A6B85;
color: {{text/secondary}};
}
QProgressBar::chunk {
background: #2D6CB5;
border-radius: 5px;
background: {{accent/primary}};
border-radius: 6px;
}
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
(//) +
线 + */
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";
// 浅色专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。
QPalette buildPalette()
// 全局复选指示器:用 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*/)
{
const bool dark = isDarkTheme();
return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark);
}
// 调色板同样取自 ElaTheme让无 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");
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("#F0F3F8"));
p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark)));
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, QColor("#EDF1F7"));
p.setColor(QPalette::Button, hoverBg);
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D"));
p.setColor(QPalette::ToolTipText, shell);
p.setColor(QPalette::ToolTipBase, text);
p.setColor(QPalette::ToolTipText, panel);
p.setColor(QPalette::Highlight, accent);
p.setColor(QPalette::HighlightedText, panel);
p.setColor(QPalette::PlaceholderText, mutedText);
p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark)));
p.setColor(QPalette::PlaceholderText, muted);
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"));
// 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 applyTheme(QApplication& app)
void applyThemeMode(QApplication& app, bool dark)
{
// Fusion:跨平台一致且对 QSS 友好Windows 原生风对部分控件会忽略样式表)
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
// Fusion + 下拉框弹窗修正AppProxyStyle跨平台一致、对 QSS 友好
app.setStyle(new AppProxyStyle());
// 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退
// 10pt≈13px对齐主流商用客户端基准9pt 偏小显拥挤。抗锯齿优先,观感更精致。
QFont base(QStringLiteral("Microsoft YaHei UI"), 10);
// 基础字体:微软雅黑 UI基准字号取令牌 type::kBody(13px),与 QSS 同单位
QFont base(QStringLiteral("Microsoft YaHei UI"));
base.setPixelSize(scaledPx(type::kBody)); // 随界面字号缩放
base.setStyleStrategy(QFont::PreferAntialias);
app.setFont(base);
app.setPalette(buildPalette());
app.setStyleSheet(QString::fromUtf8(kStyleSheet));
app.setPalette(buildPalette(dark));
app.setStyleSheet(styleSheetForMode(dark));
}
void applyTheme(QApplication& app)
{
applyThemeMode(app, false);
}
// ── 主题管理器(替代 ElaTheme+ 设置:主题 / 字号 偏好 ──────────────────
namespace {
constexpr int kBaseFontPx = 13; // 基准字号
int g_fontScale = 100; // 当前字号缩放百分比
} // namespace
ThemeManager& ThemeManager::instance()
{
static ThemeManager inst;
return inst;
}
ThemeManager::ThemeManager(QObject* parent) : QObject(parent)
{
applyPersisted();
// 跟随系统时,系统明暗变化即同步并发 changed。
QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
[this](Qt::ColorScheme) {
if (!follow_) return;
const bool d = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
if (d != dark_) {
dark_ = d;
emit changed();
}
});
}
void ThemeManager::applyPersisted()
{
const QString m =
QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
if (m == QStringLiteral("light")) {
follow_ = false;
dark_ = false;
} else if (m == QStringLiteral("dark")) {
follow_ = false;
dark_ = true;
} else { // system
follow_ = true;
dark_ = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
}
}
void ThemeManager::setMode(const QString& mode)
{
QSettings().setValue(QStringLiteral("ui/themeMode"), mode);
applyPersisted();
emit changed(); // 热切:全 UI 重着色
}
QString themeModePreference()
{
return QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
}
void applyPersistedThemeMode()
{
ThemeManager::instance().applyPersisted();
}
void setThemeModePreference(const QString& mode)
{
ThemeManager::instance().setMode(mode); // 持久化 + 热切
}
int fontScalePreference()
{
return QSettings().value(QStringLiteral("ui/fontScale"), 100).toInt();
}
void setFontScalePreference(int percent)
{
QSettings().setValue(QStringLiteral("ui/fontScale"), percent); // 重启后生效
}
void applyPersistedFontScale()
{
g_fontScale = fontScalePreference();
QFont f = qApp->font();
f.setPixelSize(kBaseFontPx * g_fontScale / 100);
qApp->setFont(f);
}
int scaledPx(int basePx)
{
return basePx * g_fontScale / 100;
}
bool isDarkTheme()
{
return ThemeManager::instance().isDark();
}
void vtkBackground(double& r, double& g, double& b)
{
// 规范 §0.5/§11数据画布永远深色不随明暗切换。取 canvas/bg。
const QColor c = tokenColor("canvas/bg"); // #0B1320
r = c.redF();
g = c.greenF();
b = c.blueF();
}
QString token(const char* name) { return tokenHex(name, isDarkTheme()); }
QColor tokenColor(const char* name) { return QColor(token(name)); }
QString fillTokens(const QString& tmpl)
{
const bool dark = isDarkTheme();
QString s = tmpl;
for (const auto& t : kTokens)
s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)),
QString::fromLatin1(dark ? t.dark : t.light));
return s;
}
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl)
{
if (!w) return;
w->setStyleSheet(fillTokens(tmpl));
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
[w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); });
}
} // namespace geopro::app

View File

@ -9,11 +9,134 @@
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
// 危险 #C0392B
#include <QColor>
#include <QObject>
#include <QString>
class QApplication;
class QWidget;
namespace geopro::app {
// 应用浅色专业主题Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。
// 主题管理器(纯 Qt替代 ElaTheme持有当前明暗 + 是否跟随系统;切换发 changed() 信号,
// 全 UI全局 QSS 由 main 重应用、内联 chrome 由 applyTokenizedStyleSheet据此热切重着色。
class ThemeManager : public QObject {
Q_OBJECT
public:
static ThemeManager& instance();
bool isDark() const { return dark_; }
void setMode(const QString& mode); // "system"|"light"|"dark":持久化 + 应用 + 发 changed
void applyPersisted(); // 按持久化偏好设置状态(系统模式则取系统明暗)
signals:
void changed();
private:
explicit ThemeManager(QObject* parent = nullptr);
bool dark_ = false;
bool follow_ = true;
};
// ── 排版令牌(全项目唯一字号阶 + 字重角色)──────────────────────────
// 各处 QSS 的 font-size / font-weight 一律引用这些值,不再散落硬编码 px。
// 阶比 ~1.18body→title→heading刻意拉开层级——避免 11/12/13/14
// 这类只差 1px 的"糊层级",让标题/正文/说明三档一眼可分。
// 单位px与全局 px 化的 QSS、固定像素行高对齐后续若做无障碍字号
// 缩放再统一切 pt。字重400 正文 / 500 可交互激活 /
// 600 标签·标题 / 700 仅展示性大标题。
namespace type {
inline constexpr int kCaption = 12; // 徽标·提示·角色名·错误·副标题·字段标签
inline constexpr int kBody = 13; // 树/列表/菜单/正文(= 全局基准字号)
inline constexpr int kLabel = 13; // 表头·用户名等需加粗的同级标签(配 600
inline constexpr int kTitle = 15; // 面板/停靠区/区段标题·主操作按钮
inline constexpr int kHeading = 18; // 视图/对话框级标题(预留)
inline constexpr int kDisplay = 24; // 登录品牌名(唯一展示性大字)
inline constexpr int kWeightRegular = 400;
inline constexpr int kWeightMedium = 500;
inline constexpr int kWeightSemibold = 600;
inline constexpr int kWeightBold = 700;
} // namespace type
// ── 间距令牌(全项目唯一间距阶)──────────────────────────────────
// 取代散落的 5/7/9/11/13/15/26 等任意值。这是密集专业工具的实际节奏
// (非外加的 8pt 网格):相邻档 2px 粒度,足够紧凑又不糊成一片。
// 用法:布局 setContentsMargins/setSpacing/addSpacing 与 QSS padding/margin
// 一律引用这些档明显的奇数值就近归档13/15→lg, 11→ml, 26→xxl
namespace space {
inline constexpr int kXxs = 2; // 发丝级:下划线偏移、滚动条边距
inline constexpr int kXs = 4; // 紧凑内边距、最小间隙
inline constexpr int kSm = 6; // 行内紧凑(控件竖向 padding
inline constexpr int kMd = 8; // 标准间隔(最常用)
inline constexpr int kMl = 10; // 偏大行距(密集行/验证码行)
inline constexpr int kLg = 12; // 分组间隔、面板内左右边距
inline constexpr int kXl = 16; // 区块内边距
inline constexpr int kXxl = 24; // 区块间距、表单纵向边距
inline constexpr int kXxxl = 32; // 页面级留白(登录窗左右边距)
} // namespace space
// ── 圆角令牌(统一原先 4/5/6/7/8/9 共 6 档为 3 档)────────────────
// 圆形元素(头像等)用 直径/2 单独写字面量,不入档。
namespace radius {
inline constexpr int kSm = 6; // 按钮·输入·菜单项·滚动条·进度条
inline constexpr int kMd = 8; // 卡片·面板·对话框·菜单·分组框
inline constexpr int kPill = 9; // 数量徽标胶囊
} // namespace radius
// ── 语义色令牌(状态/反馈,产品语境:只在承载含义处用,不作装饰)──────────
// 文字值均针对白底面板(#FFFFFF)选深色,对比度 ≥4.5:1正文级与冷调中性
// 调色板调和。danger 沿用既有红,避免引入第二种红。
namespace semantic {
inline constexpr const char* kInfo = "#2D6CB5"; // 信息·进行中(= 品牌蓝)
inline constexpr const char* kSuccess = "#15803D"; // 成功·已完成(深绿)
inline constexpr const char* kWarning = "#B45309"; // 警告·需注意(深琥珀)
inline constexpr const char* kDanger = "#C0392B"; // 危险·错误(沿用既有红)
// 浅色填充(徽标/标签底色,配同族深色文字使用)。
inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kWarning 文字)
} // namespace semantic
// 应用专业主题Fusion + 调色板 + 全局样式表。dark=true 走暗色P2 主题桥用)。
// 暗色复用同一 QSS 结构,颜色全由 kTokens 双值fillTokens/tokenHex驱动幂等可随主题切换重复调用。
void applyThemeMode(QApplication& app, bool dark);
// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。
void applyTheme(QApplication& app);
// ── 设置:主题 / 界面字号 偏好QSettings 持久化)────────────────────────
// 启动时eApp->init + applyBrandAccent 之后、弹登录窗之前)各调一次,使登录页与主页统一。
void applyPersistedThemeMode(); // 应用持久化主题:跟随系统 / 浅色 / 深色 → ElaTheme
void applyPersistedFontScale(); // 应用持久化字号:设 qApp 基准字体 + 记录缩放(供 scaledPx)
QString themeModePreference(); // "system" | "light" | "dark"(默认 system
void setThemeModePreference(const QString& mode); // 持久化 + 立即应用(主题可热切)
int fontScalePreference(); // 缩放百分比 90/100/115/130默认 100
void setFontScalePreference(int percent); // 仅持久化(字号改动重启后生效)
int scaledPx(int basePx); // basePx × 当前字号% / 100内联 QSS 字号用,使自定义 chrome 也随字号缩放)
// 当前 ElaTheme 是否暗色(供内联样式判断)。
bool isDarkTheme();
// VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b01
void vtkBackground(double& r, double& g, double& b);
// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens规范 §1.5 + 附录 A + §1.3)──
// 组件只引语义 token禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。
QString token(const char* name); // 当前明暗下的 hex未知名返回品红 "#FF00FF" 以便一眼发现漏配)
QColor tokenColor(const char* name); // 同上QColor 形式
// 把 QSS 模板里的 {{token}} 占位替换为当前明暗的 hex 后返回。
QString fillTokens(const QString& tmpl);
// 应用一段 {{token}} 模板 QSS 到 widget并随主题切换自动重填。
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl);
} // namespace geopro::app

View File

@ -1,19 +1,27 @@
#include "TopBar.hpp"
#include <QAbstractButton>
#include <QActionGroup>
#include <QColor>
#include <QEvent>
#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QPainter>
#include <QPixmap>
#include <QSize>
#include <QVBoxLayout>
#include <QStringList>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
#include "Glyphs.hpp"
#include "Theme.hpp"
namespace geopro::app {
@ -34,12 +42,12 @@ QFrame* makeDivider(QWidget* parent)
return line;
}
// 右侧图标按钮(仅图标,悬停显示文本)。
QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip)
// 右侧图标按钮(QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #iconBtn QSS 给)。
QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip)
{
auto* btn = new QToolButton(parent);
btn->setObjectName(QStringLiteral("iconBtn"));
btn->setIcon(makeGlyph(g, QColor("#5A6B85"), kToolIcon));
setThemedGlyph(btn, icon, kToolIcon);
btn->setIconSize(QSize(kToolIcon, kToolIcon));
btn->setToolTip(tip);
btn->setCursor(Qt::PointingHandCursor);
@ -47,6 +55,29 @@ QToolButton* makeIconButton(QWidget* parent, Glyph g, 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<int>(s * 0.4));
f.setBold(true);
p.setFont(f);
p.setPen(fg);
p.drawText(QRect(0, 0, s, s), Qt::AlignCenter, initials);
p.end();
pm.setDevicePixelRatio(kScale);
return pm;
}
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
QMenu* buildViewMenu(QWidget* p)
{
@ -114,12 +145,7 @@ QWidget* buildMenuBar(QWidget* parent)
{
auto* mb = new QMenuBar(parent);
mb->setObjectName(QStringLiteral("appMenuBar"));
// 自带样式(覆盖全局),加大字号/内边距,专业观感。
mb->setStyleSheet(QStringLiteral(
"#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }"
"#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:14px; color:#1F2A3D; }"
"#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }"
"#appMenuBar::item:pressed { background:#DCE6F4; }"));
// ElaMenuBar 自绘 Fluent 外观并自动随 ElaTheme 明暗,不再写内联 QSS。
mb->addMenu(buildViewMenu(mb));
mb->addMenu(buildProjectMenu(mb));
mb->addMenu(buildToolsMenu(mb));
@ -130,33 +156,52 @@ QWidget* buildMenuBar(QWidget* parent)
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
setObjectName(QStringLiteral("appToolBar"));
setFixedHeight(56);
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; }"
// 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、
// 角色名=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; }"
"#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; }"));
"#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:4px 10px 4px 6px;"
" color:{{text/primary}}; font-size:%3px; }"
"#userBtn:hover { background:{{bg/hover}}; }"
"#userBtn::menu-indicator { image:none; }"
"#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;"
" font-size:%1px; }"
"#userName { color:{{text/primary}}; font-size:%3px; font-weight:%4; }"
"#userRole { color:{{text/tertiary}}; font-size:%5px; }")
.arg(scaledPx(type::kBody))
.arg(type::kWeightBold)
.arg(scaledPx(type::kLabel))
.arg(type::kWeightSemibold)
.arg(scaledPx(type::kCaption))
.arg(scaledPx(type::kTitle))
.arg(chevron));
auto* lay = new QHBoxLayout(this);
lay->setContentsMargins(14, 0, 14, 0);
lay->setSpacing(0);
// 工作空间切换器(数据驱动;初始占位文本,待 setWorkspaces 填充)。
// 工作空间切换器(QToolButton + 主题化 QSS下拉箭头用高清 chevron menu-indicator数据驱动)。
wsBtn_ = new QToolButton(this);
wsBtn_->setObjectName(QStringLiteral("wsSwitcher"));
wsBtn_->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), 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);
wsBtn_->setText(QStringLiteral("(加载中…)"));
wsBtn_->setText(QStringLiteral("正在加载工作空间…"));
wsBtn_->setMenu(new QMenu(wsBtn_));
lay->addWidget(wsBtn_);
@ -164,15 +209,14 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
lay->addWidget(makeDivider(this));
lay->addSpacing(10);
// 项目切换器(数据驱动)。
// 项目切换器(QToolButton + 主题化 QSS数据驱动)。
projBtn_ = new QToolButton(this);
projBtn_->setObjectName(QStringLiteral("wsSwitcher"));
projBtn_->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), 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);
projBtn_->setText(QStringLiteral("(加载中…)"));
projBtn_->setText(QStringLiteral("正在加载项目…"));
projBtn_->setMenu(new QMenu(projBtn_));
lay->addWidget(projBtn_);
@ -180,30 +224,73 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助")));
lay->addWidget(makeIconButton(this, Glyph::Bell, QStringLiteral("通知")));
lay->addWidget(makeIconButton(this, Glyph::Gear, QStringLiteral("设置")));
auto* gearBtn = makeIconButton(this, Glyph::Gear, QStringLiteral("设置"));
if (auto* gb = qobject_cast<QAbstractButton*>(gearBtn))
QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); });
lay->addWidget(gearBtn);
lay->addSpacing(10);
lay->addWidget(makeDivider(this));
lay->addSpacing(12);
// 用户区(本轮静态)。
auto* avatar = new QLabel(QStringLiteral("ZL"), this);
avatar->setObjectName(QStringLiteral("avatar"));
avatar->setFixedSize(34, 34);
avatar->setAlignment(Qt::AlignCenter);
lay->addWidget(avatar);
lay->addSpacing(8);
// 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。
// 用普通 QWidget + eventFilterQWidget 按子布局正确撑开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* userBox = new QWidget(this);
auto* userLay = new QVBoxLayout(userBox);
userLay->setContentsMargins(0, 0, 0, 0);
userLay->setSpacing(0);
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
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("高级工程师"), userBox);
auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox);
userRole->setObjectName(QStringLiteral("userRole"));
userLay->addWidget(userName);
userLay->addWidget(userRole);
lay->addWidget(userBox);
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(); });
lay->addWidget(userRow_);
}
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
if (userMenu_)
userMenu_->exec(userRow_->mapToGlobal(QPoint(0, userRow_->height() + 2)));
return true;
}
return QWidget::eventFilter(obj, event);
}
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
@ -223,7 +310,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& 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);
});
}
@ -232,8 +319,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& 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<data::ProjectSummary>& list, const QString& currentId,
@ -254,7 +340,7 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& 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);
});
}
@ -268,12 +354,11 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& 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

View File

@ -4,6 +4,8 @@
#include "repo/RepoTypes.hpp"
class QToolButton;
class QEvent;
class QMenu;
namespace geopro::app {
@ -21,14 +23,21 @@ 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);
void allProjectsRequested(); // 点击"全部项目…"
void logoutRequested(); // 头像菜单「退出登录」
void settingsRequested(); // 点击齿轮图标 → 打开设置
private:
QToolButton* wsBtn_ = nullptr;
QToolButton* projBtn_ = nullptr;
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
QMenu* userMenu_ = nullptr; // 用户下拉菜单
};
} // namespace geopro::app

View File

@ -2,19 +2,23 @@
#include <QCheckBox>
#include <QColor>
#include <QEasingCurve>
#include <QFont>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#include <QWidget>
#include "AuthService.hpp"
#include "Theme.hpp"
namespace geopro::app {
@ -82,21 +86,25 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
setStyleSheet(QStringLiteral(
"QDialog { background: #F4F6FA; }"
// 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。
// 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。
// 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。
geopro::app::applyTokenizedStyleSheet(
this, QStringLiteral(
"QDialog { background: {{bg/app}}; }"
"#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; }"));
" 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: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
.arg(scaledPx(type::kDisplay))
.arg(type::kWeightBold)
.arg(scaledPx(type::kCaption))
.arg(scaledPx(type::kCaption))
.arg(type::kWeightSemibold));
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
@ -122,8 +130,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) {
@ -169,21 +178,23 @@ 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::applyTokenizedStyleSheet(
refreshBtn_,
QStringLiteral(
"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);
// 记住登录:勾选后成功登录将安全存储 token30 天内免登录。默认不勾(更安全)。
rememberChk_ = new QCheckBox(QStringLiteral("记住登录30 天内免登录)"), body);
rememberChk_->setCursor(Qt::PointingHandCursor);
rememberChk_->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:13px;"));
rememberChk_->setCursor(Qt::PointingHandCursor); // ElaCheckBox 自绘 Fluent + 自动明暗
form->addWidget(rememberChk_);
// 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
geopro::app::applyTokenizedStyleSheet(
errorLabel_, QStringLiteral("color: {{status/danger}}; font-size: %1px;").arg(scaledPx(type::kCaption)));
errorLabel_->setWordWrap(true);
errorLabel_->setMinimumHeight(18);
form->addWidget(errorLabel_);
@ -191,15 +202,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
form->addStretch();
// 主操作满宽强调主按钮von Restorff唯一高强调元素引导主流程
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body);
loginBtn_ = new QPushButton(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: 15px; font-weight: 600; }"
"QPushButton:hover { background: #2862A6; }"
"QPushButton:pressed { background: #234F87; }"
"QPushButton:disabled { background: #9FB4CC; }"));
loginBtn_->setDefault(true);
form->addWidget(loginBtn_);
@ -279,6 +284,20 @@ bool LoginWindow::remember() const
void LoginWindow::showError(const QString& msg)
{
errorLabel_->setText(msg);
// 错误淡入:柔化失败时刻(仅透明度 200mserrorLabel_ 已预留固定高度,
// 不引发布局跳动)。复用同一 opacity effect重复报错每次重新淡入。
auto* fx = qobject_cast<QGraphicsOpacityEffect*>(errorLabel_->graphicsEffect());
if (!fx) {
fx = new QGraphicsOpacityEffect(errorLabel_);
errorLabel_->setGraphicsEffect(fx);
}
auto* anim = new QPropertyAnimation(fx, "opacity", errorLabel_);
anim->setDuration(200);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutQuad);
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
} // namespace geopro::app

View File

@ -28,18 +28,32 @@
#include <QCheckBox>
#include <QColor>
#include <QDialog>
#include <QEasingCurve>
#include <QEvent>
#include <QFile>
#include <QButtonGroup>
#include <QCheckBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QToolButton>
#include <QKeySequence>
#include <QProcess>
#include <QSettings>
#include <QShortcut>
#include <QSignalBlocker>
#include <QPropertyAnimation>
#include <QVariantAnimation>
#include <QStringList>
#include <QTabWidget>
#include <QMainWindow>
#include <QStatusBar>
#include <QStyle>
#include <QSurfaceFormat>
#include <QTimer>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
@ -51,6 +65,7 @@
#include <DockManager.h>
#include <DockWidget.h>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
@ -61,6 +76,7 @@
#include "Glyphs.hpp"
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "SettingsDialog.hpp"
#include "TopBar.hpp"
#include "CentralScene.hpp"
#include "ProjectListDialog.hpp"
@ -93,15 +109,90 @@
#include <vector>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCameraInterpolator.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
#include <vtkProperty.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
namespace {
// 居中浮层定位器:监视 host中央 QVTK尺寸/显示变化,把 overlay 浮层
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
class CenterOverlay : public QObject {
public:
CenterOverlay(QWidget* overlay, QWidget* host)
: QObject(host), overlay_(overlay), host_(host)
{
host_->installEventFilter(this);
}
void reposition()
{
overlay_->adjustSize();
const QSize h = host_->size();
const QSize o = overlay_->size();
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
host_->y() + (h.height() - o.height()) / 2);
overlay_->raise();
}
protected:
bool eventFilter(QObject* obj, QEvent* e) override
{
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
reposition();
return QObject::eventFilter(obj, e);
}
private:
QWidget* overlay_;
QWidget* host_;
};
// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。
// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve单条 QVariantAnimation
// 逐帧驱动并 Render结束回调锁定到目标态防插值末值误差/残留半透明)。
// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。
void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw,
vtkSmartPointer<vtkCamera> fromCam, vtkSmartPointer<vtkCamera> toCam,
std::vector<vtkSmartPointer<vtkActor>> actors, int durationMs, QObject* owner)
{
auto interp = vtkSmartPointer<vtkCameraInterpolator>::New();
interp->SetInterpolationTypeToLinear();
interp->AddCamera(0.0, fromCam);
interp->AddCamera(1.0, toCam);
auto* anim = new QVariantAnimation(owner);
anim->setDuration(durationMs);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutCubic);
QObject::connect(anim, &QVariantAnimation::valueChanged, owner,
[interp, renderer, rw, actors](const QVariant& v) {
const double t = v.toDouble();
interp->InterpolateCamera(t, renderer->GetActiveCamera());
for (const auto& a : actors)
if (a) a->GetProperty()->SetOpacity(t);
renderer->ResetCameraClippingRange();
rw->Render();
});
QObject::connect(anim, &QVariantAnimation::finished, owner,
[renderer, rw, actors, toCam]() {
renderer->GetActiveCamera()->DeepCopy(toCam);
for (const auto& a : actors)
if (a) a->GetProperty()->SetOpacity(1.0);
renderer->ResetCameraClippingRange();
rw->Render();
});
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path)
{
@ -198,12 +289,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
window.setCentralWidget(dockManager);
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。这里在其后追加同选择器规则覆盖为极淡分隔。
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
// 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。
const QString dockBaseQss = dockManager->styleSheet();
auto applyDockSplitter = [dockManager, dockBaseQss]() {
dockManager->setStyleSheet(
dockManager->styleSheet() +
QStringLiteral(
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }"
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"));
dockBaseQss +
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,
dockManager, [applyDockSplitter]() { applyDockSplitter(); });
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮ADS 自带标题栏随后隐藏,
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
@ -224,34 +322,56 @@ 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);
viewGroup->setExclusive(true);
auto* act2D = viewToolBar->addAction(QStringLiteral("二维地图"));
auto* act3D = viewToolBar->addAction(QStringLiteral("三维视图"));
act2D->setCheckable(true);
act3D->setCheckable(true);
viewGroup->addAction(act2D);
viewGroup->addAction(act3D);
act2D->setChecked(true); // 默认二维地图
centerLayout->addWidget(viewToolBar);
// 分段工具条按钮样式QToolButton + 主题化 QSS选中=强调色文字 + 强调色下划线,明暗都清晰。
// ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。
const QString kBarBtnQss =
QStringLiteral(
"QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:{{text/primary}};"
" 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);
// 「二维地图/三维视图」分段切换表头:与「异常/属性」面板表头同款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 之上,控制三维图层显隐。
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 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;}"
"QCheckBox:disabled{color:#9AA6B6;}"));
geopro::app::applyTokenizedStyleSheet(
layerPanel,
// 不设 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);
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;"));
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)));
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
chkCurtain->setChecked(true);
auto* chkVoxel = new QCheckBox(QStringLiteral("体素dd_voxel"));
@ -278,6 +398,52 @@ 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);
// 背景取 canvas/bg(#0B1320)——与画布同色:在原生 GL 上覆盖透明无法生效(会回退成不透明浅底),
// 故用与画布等色的不透明底,卡片即「无缝隐形」,浅色提示字稳稳浮于深底(与左上视图详情浮层同法)。
geopro::app::applyTokenizedStyleSheet(
emptyState, QStringLiteral("#centralEmpty { background: {{canvas/bg}}; }"
"#centralEmpty QLabel { background: transparent; }"));
auto* esLay = new QVBoxLayout(emptyState);
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
geopro::app::space::kXl, geopro::app::space::kXl);
esLay->setSpacing(geopro::app::space::kMd);
esLay->setAlignment(Qt::AlignCenter);
auto* esIcon = new QLabel(emptyState);
esIcon->setPixmap(
geopro::app::makeGlyph(geopro::app::Glyph::Dataset,
geopro::app::tokenColor("canvas/text-dim"), 56)
.pixmap(56, 56));
esIcon->setAlignment(Qt::AlignCenter);
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
esTitle->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esTitle, QStringLiteral("color:{{canvas/text}}; font-size:%1px; font-weight:%2;")
.arg(geopro::app::scaledPx(geopro::app::type::kHeading))
.arg(geopro::app::type::kWeightSemibold));
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
"切到「三维视图」可叠加帘面、体素与地形图层"),
emptyState);
esHint->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esHint,
QStringLiteral("color:{{canvas/text-dim}}; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody)));
esLay->addWidget(esIcon);
esLay->addWidget(esTitle);
esLay->addWidget(esHint);
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
emptyCentering->reposition();
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
vtkDock->setWidget(centerWidget);
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
@ -287,11 +453,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto* detailWidget = new QVTKOpenGLStereoWidget();
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
vtkNew<vtkRenderer> detailRenderer;
detailRenderer->SetBackground(1.0, 1.0, 1.0); // 白底
{
double r, g, b;
geopro::app::vtkBackground(r, g, b); // 背景随主题
detailRenderer->SetBackground(r, g, b);
}
detailWidget->setRenderWindow(detailRenderWindow);
detailRenderWindow->AddRenderer(detailRenderer);
vtkRenderer* detailRendererPtr = detailRenderer.Get();
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
// 注VTK 背景随主题切换的连接放在 rebuildCentral/rebuildDetail 定义之后(直接重跑它们,
// 走完整渲染路径必重绘,比手动 SetBackground+Render 稳)。
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
auto* detailContainer = new QWidget();
@ -299,27 +471,46 @@ 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);
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关。
// 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 QToolButton(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);
actScatter->setObjectName(QStringLiteral("dataTab"));
actSection->setObjectName(QStringLiteral("dataTab"));
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();
geopro::app::applyTokenizedStyleSheet(detailToolBar, kBarBtnQss);
detailLayout->addWidget(detailToolBar);
detailLayout->addWidget(detailWidget, 1);
@ -333,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);
@ -342,19 +533,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget();
auto* datasetList = new QListWidget();
// 简洁分割:去隔行变色,改为 item 间极淡分割线 + 内边距 + hover/选中反馈(专业、不误导)。
datasetList->setStyleSheet(QStringLiteral(
"QListWidget{ background:#FFFFFF; border:none; outline:none; }"
"QListWidget::item{ padding:9px 12px; border-bottom:1px solid #EEF1F5; color:#1F2A3D; }"
"QListWidget::item:hover{ background:#F5F8FD; }"
"QListWidget::item:selected{ background:#EAF1FB; color:#1F2A3D; }"));
geopro::app::applyDatasetCardDelegate(datasetList);
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget();
fileList->setStyleSheet(datasetList->styleSheet()); // 与数据页签同款简洁分割
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);
@ -364,20 +550,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* anomalyList = new QListWidget();
anomalyList->setAlternatingRowColors(true);
geopro::app::applyAnomalyCardDelegate(anomalyList);
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
objAttrLabel->setWordWrap(true);
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
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("添加异常")}});
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
// colorize(C):异常计数用语义 warning“需注意”变体区别于普通中性计数徽标
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
// 注:徽标的填充/显隐在 loadDataset 内(当前被 park故此色与徽标本身同属休眠态
// 接 dd 详情渲染那轮一并可见。
if (anomalyBadge) {
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
anomalyBadge->style()->unpolish(anomalyBadge);
anomalyBadge->style()->polish(anomalyBadge);
}
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性"));
rightDock->setWidget(anomalyPanel.container);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
@ -386,19 +581,23 @@ 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);
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
// 注AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。
// 抽成 lambdaADS 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 +615,34 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
auto prevDsId = std::make_shared<QString>(); // 上次渲染的 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<vtkCamera>::New();
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();
return;
}
std::vector<vtkSmartPointer<vtkActor>> added; // 本次加入的 actor供淡入
const std::string id = currentDsId->toStdString();
if (*detailMode == DetailMode::Section18) {
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y
@ -435,10 +652,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 +665,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 +676,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 +687,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();
*prevDsId = *currentDsId;
if (animate) {
// 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。
auto toCam = vtkSmartPointer<vtkCamera>::New();
toCam->DeepCopy(detailRendererPtr->GetActiveCamera());
for (const auto& a : added) a->GetProperty()->SetOpacity(0.0);
detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam);
detailRendererPtr->ResetCameraClippingRange();
animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450,
detailWidget);
} else {
detailRenderWindowPtr->Render();
}
};
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
@ -503,9 +738,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
.arg(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()) {
@ -532,38 +772,38 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
});
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
QObject::connect(actSection, &QAction::triggered, detailWidget,
QObject::connect(actSection, &QAbstractButton::clicked, detailWidget,
[detailMode, rebuildDetail]() {
*detailMode = 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();
});
// 「视图详情」浮层显隐:仅三维显示,置于 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();
@ -573,13 +813,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
};
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
QObject::connect(act2D, &QAction::triggered, vtkWidget,
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
[viewMode, rebuildCentral, showLayerPanel]() {
*viewMode = 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);
@ -611,6 +851,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。
rebuildCentral();
// VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail走完整渲染路径、末尾必 Render
// 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window,
[rebuildCentral, rebuildDetail]() {
rebuildCentral();
rebuildDetail();
});
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
geopro::app::TopBar* topBar = nullptr;
@ -635,11 +883,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto addLoadMore = [](QListWidget* lw, int total) {
const int loaded = lw->count();
if (loaded < total) {
auto* m = new QListWidgetItem(
QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total), lw);
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;
};
@ -647,6 +893,18 @@ 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::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);
@ -677,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("文件"));
});
@ -688,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("数据"));
@ -713,8 +971,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) {
@ -737,14 +999,20 @@ 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();
if (!dockState.isEmpty()) dockManager->restoreState(dockState);
// 注意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 标题栏,再隐藏一次保持“无双标题”。
hideDockTitleBars();
}
}
// 退出时保存当前布局与几何aboutToQuit 早于 window 析构dockManager/window 仍存活)。
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());
});
}
@ -765,8 +1033,11 @@ int main(int argc, char* argv[])
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
// 浅色专业主题Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。
geopro::app::applyTheme(app);
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。
// 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。
geopro::app::applyPersistedThemeMode();
geopro::app::applyPersistedFontScale();
geopro::app::applyThemeMode(app, geopro::app::isDarkTheme());
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
@ -813,13 +1084,25 @@ 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); // 防止停靠面板被压到不可用尺寸
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其
// setCentralWidget/setMenuWidget/statusBar 承载工作台。
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
auto* window = new QMainWindow;
window->setWindowTitle(kTitle);
window->resize(1280, 800);
window->setMinimumSize(1024, 680);
buildWorkbench(*window, repo, projectRepo, nav);
buildWorkbench(window, repo, projectRepo, nav);
window.show();
// 主题桥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"));
});
window->show();
nav.start(); // 进入工作台后拉真实 空间/项目/结构

View File

@ -3,22 +3,26 @@
#include <cmath>
#include <cstddef>
#include <QAbstractItemModel>
#include <QColor>
#include <QIcon>
#include <QEvent>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPixmap>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QString>
#include <QStyledItemDelegate>
#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<QMouseEvent*>(e);
if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) {
const auto cur = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked,
Qt::CheckStateRole);
return true; // 吃掉点击:只切显隐,不改选中
}
}
return QStyledItemDelegate::editorEvent(e, model, opt, idx);
}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override
{
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
const QRect r = opt.rect.adjusted(4, 3, -4, -3);
const bool selected = opt.state & QStyle::State_Selected;
const bool hover = opt.state & QStyle::State_MouseOver;
// 卡底hover/选中高亮)
if (selected || hover) {
QPainterPath path; path.addRoundedRect(r, 6, 6);
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
}
// 左 3px 状态色竖条(取异常自身 lineColor
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
barColor(idx.data(kAnomalyColorRole).toString()));
const QString name = idx.data(Qt::DisplayRole).toString();
const QString type = idx.data(kAnomalyTypeRole).toString();
const QString summary = idx.data(kAnomalySummaryRole).toString();
const int left = r.left() + 14;
const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位
const int rowW = right - left;
// 第一行:名称(加粗)
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
p->setFont(nf);
p->setPen(geopro::app::tokenColor("text/primary"));
const QRect nameR(left, r.top() + 8, rowW, 20);
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(name, Qt::ElideRight, rowW));
// 第二行:类型胶囊 + 摘要
int x = left;
const int cy = r.top() + 38;
if (!type.isEmpty()) {
QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(pf);
const QFontMetrics fm(pf);
const int tw = fm.horizontalAdvance(type);
const int ph = fm.height() + 2;
const QRect pill(x, cy - ph / 2, tw + 12, ph);
QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0);
p->fillPath(pp, geopro::app::tokenColor("bg/hover"));
p->setPen(geopro::app::tokenColor("text/secondary"));
p->drawText(pill, Qt::AlignCenter, type);
x = pill.right() + 8;
}
if (!summary.isEmpty()) {
QFont sf = opt.font; sf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(sf);
p->setPen(geopro::app::tokenColor("text/secondary"));
const QRect sumR(x, cy - 10, right - x, 20);
p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width()));
}
// 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠
const bool visible =
static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked;
const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled");
const QRectF eb = anomalyEyeRect(opt.rect);
const QPointF c = eb.center();
const double w = eb.width() * 0.42, h = eb.height() * 0.24;
p->setPen(QPen(eyeCol, 1.4));
p->setBrush(Qt::NoBrush);
QPainterPath eye;
eye.moveTo(c.x() - w, c.y());
eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y());
eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y());
p->drawPath(eye);
p->setBrush(eyeCol);
p->drawEllipse(c, h * 0.95, h * 0.95);
p->setBrush(Qt::NoBrush);
if (!visible)
p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6));
p->restore();
}
};
} // namespace
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies)
@ -61,16 +178,24 @@ void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anom
for (std::size_t i = 0; i < anomalies.size(); ++i) {
const auto& a = anomalies[i];
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
const QString type = QString::fromStdString(a.typeName);
QString text = name;
if (!type.isEmpty()) text += QStringLiteral("%1").arg(type);
text += QStringLiteral("\n%1").arg(summarize(a));
auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list);
auto* item = new QListWidgetItem(name, list);
item->setData(kAnomalyIndexRole, static_cast<int>(i));
item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor));
item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName));
item->setData(kAnomalySummaryRole, summarize(a));
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->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

View File

@ -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 显隐。

View File

@ -3,7 +3,13 @@
#include <QColor>
#include <QListWidget>
#include <QListWidgetItem>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QString>
#include <QStyledItemDelegate>
#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<geopro::data::DsRow>& rows, bool append) {
@ -37,7 +125,6 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
if (!append && rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter);
return;
}
@ -53,4 +140,13 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
}
}
void applyDatasetCardDelegate(QListWidget* list) {
if (!list) return;
list->setItemDelegate(new DatasetCardDelegate(list));
list->setMouseTracking(true); // 让委托收到 hover 状态
list->setSpacing(0); // 卡间距由委托内边距控制
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
[list]() { list->viewport()->update(); });
}
} // namespace geopro::app

View File

@ -18,4 +18,7 @@ void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRo
// 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位。
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
// 给数据/文件列表套用卡片委托(标题+元信息双行、悬停/选中圆角高亮+左强调竖条规范§6.2)。
void applyDatasetCardDelegate(QListWidget* list);
} // namespace geopro::app

View File

@ -1,6 +1,5 @@
#include "panels/ObjectTreePanel.hpp"
#include <QColor>
#include <QLabel>
#include <QSignalBlocker>
#include <QTreeWidget>
@ -8,6 +7,7 @@
#include <QVBoxLayout>
#include "Glyphs.hpp"
#include "Theme.hpp"
#include "dto/NavDto.hpp"
namespace geopro::app {
@ -35,24 +35,16 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// Qt 原生标准树:复选框/展开箭头由 Fusion 绘制(明暗都清晰);配色取自全局主题 QSS。
tree_ = new QTreeWidget(this);
tree_->setHeaderHidden(true);
{
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));
}
tree_->setIndentation(14); // 收紧缩进
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;"));
geopro::app::applyTokenizedStyleSheet(hint_, QStringLiteral("color:{{text/disabled}}; padding:16px;"));
hint_->setVisible(false);
lay->addWidget(hint_);
@ -62,8 +54,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
});
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);
if (!tmId.isEmpty()) emit tmCheckToggled(tmId, item->checkState(0) == Qt::Checked);
});
}

View File

@ -24,7 +24,7 @@ signals:
void tmCheckToggled(const QString& tmObjectId, bool checked);
private:
QTreeWidget* tree_ = nullptr;
QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控)
QLabel* hint_ = nullptr;
};