#include "TopBar.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Glyphs.hpp" #include "Theme.hpp" namespace geopro::app { namespace { // ── 工具条图标尺寸(贴近常见桌面客户端:16px 紧凑)── constexpr int kToolIcon = 16; // 工具条右侧图标 constexpr int kWorkspaceIcon = 16; // 工作空间 / 项目图标 // 竖直分隔细线。 QFrame* makeDivider(QWidget* parent) { auto* line = new QFrame(parent); line->setObjectName(QStringLiteral("topDivider")); line->setFrameShape(QFrame::VLine); line->setFixedWidth(1); line->setFixedHeight(20); return line; } // 右侧图标按钮(QToolButton + 项目 glyph 图标,随主题着色;悬停底由 #iconBtn QSS 给)。 QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip) { auto* btn = new QToolButton(parent); btn->setObjectName(QStringLiteral("iconBtn")); setThemedGlyph(btn, icon, kToolIcon); btn->setIconSize(QSize(kToolIcon, kToolIcon)); btn->setToolTip(tip); btn->setCursor(Qt::PointingHandCursor); btn->setAutoRaise(true); return btn; } // 通知红点(规范 §5):在铃铛按钮右上角叠一个 8px 实心圆点作未读指示,token 色 status/danger。 // 做法:以按钮为父建一个圆形 QLabel,绝对定位到右上角;透明穿透鼠标,不影响按钮 hover/点击。 // 尺寸随 scaledPx 缩放;按钮 32×32 内右上偏内 2px。当前为常驻静态指示(UI 合规通过即可)。 void attachNotificationDot(QWidget* bellBtn) { const int dot = scaledPx(8); // 8px 圆点(随字号缩放) auto* badge = new QLabel(bellBtn); badge->setObjectName(QStringLiteral("notifDot")); badge->setAttribute(Qt::WA_TransparentForMouseEvents); // 不拦截按钮点击/hover badge->setFixedSize(dot, dot); applyTokenizedStyleSheet( badge, QStringLiteral("#notifDot { background:{{status/danger}}; border-radius:%1px; }") .arg(dot / 2)); // 右上角内收 2px 定位。延迟到布局/缩放生效后再按真实宽度定位(构造期 width 可能尚未确定), // 避免换字号/重排时红点错位。 const int margin = scaledPx(2); auto place = [badge, bellBtn, dot, margin] { badge->move(bellBtn->width() - dot - margin, margin); badge->raise(); badge->show(); }; place(); QTimer::singleShot(0, badge, place); } // 一级菜单工具按钮:纯文字 + 下拉箭头(chevron menu-indicator),InstantPopup 挂菜单。 // 文字取菜单标题(视图/项目管理/…),样式见 #menuBtn QSS。 QToolButton* makeMenuButton(QWidget* parent, QMenu* menu) { auto* btn = new QToolButton(parent); btn->setObjectName(QStringLiteral("menuBtn")); btn->setText(menu->title()); btn->setMenu(menu); btn->setPopupMode(QToolButton::InstantPopup); btn->setToolButtonStyle(Qt::ToolButtonTextOnly); btn->setCursor(Qt::PointingHandCursor); btn->setAutoRaise(true); return btn; } // 圆形头像图标:强调色填充 + 白色缩写。2x 绘制保证高 DPI 清晰。 QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QColor& fg) { constexpr int kScale = 2; const int s = px * kScale; QPixmap pm(s, s); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing, true); p.setPen(Qt::NoPen); p.setBrush(bg); p.drawEllipse(0, 0, s, s); QFont f = p.font(); f.setPixelSize(static_cast(s * 0.4)); f.setBold(true); p.setFont(f); p.setPen(fg); p.drawText(QRect(0, 0, s, s), Qt::AlignCenter, initials); p.end(); pm.setDevicePixelRatio(kScale); return pm; } // ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)── QMenu* buildViewMenu(QWidget* p) { auto* m = new QMenu(QStringLiteral("视图"), p); m->addAction(QStringLiteral("分析视图")); m->addAction(QStringLiteral("大屏视图")); return m; } QMenu* buildProjectMenu(QWidget* p) { auto* m = new QMenu(QStringLiteral("项目管理"), p); m->addAction(QStringLiteral("数据视图")); auto* cfg = m->addMenu(QStringLiteral("项目配置")); cfg->addAction(QStringLiteral("基本信息")); cfg->addAction(QStringLiteral("项目结构")); cfg->addAction(QStringLiteral("视图配置")); m->addAction(QStringLiteral("数据管理")); auto* biz = m->addMenu(QStringLiteral("业务管理")); biz->addAction(QStringLiteral("异常管理")); biz->addAction(QStringLiteral("异常体管理")); auto* mon = m->addMenu(QStringLiteral("在线监测")); mon->addAction(QStringLiteral("项目设备")); mon->addAction(QStringLiteral("在线任务管理")); auto* doc = m->addMenu(QStringLiteral("项目资料管理")); doc->addAction(QStringLiteral("项目资料管理")); doc->addAction(QStringLiteral("报告列表")); auto* tools = m->addMenu(QStringLiteral("工具组件")); tools->addAction(QStringLiteral("装置与脚本")); tools->addAction(QStringLiteral("色阶配置")); tools->addAction(QStringLiteral("异常类型管理")); tools->addAction(QStringLiteral("模型管理")); auto* exp = m->addMenu(QStringLiteral("批量导出")); exp->addAction(QStringLiteral("文件导出")); exp->addAction(QStringLiteral("报告导出")); auto* alarm = m->addMenu(QStringLiteral("告警管理")); alarm->addAction(QStringLiteral("设备告警")); alarm->addAction(QStringLiteral("告警查询")); m->addAction(QStringLiteral("自动任务")); m->addAction(QStringLiteral("模板管理")); return m; } QMenu* buildToolsMenu(QWidget* p) { auto* m = new QMenu(QStringLiteral("业务工具"), p); m->addAction(QStringLiteral("ERT 思维分析")); m->addAction(QStringLiteral("电法脚本与装置")); m->addAction(QStringLiteral("Geo 反演")); m->addAction(QStringLiteral("三维 GPR 综合分析")); return m; } QMenu* buildDeviceMenu(QWidget* p) { auto* m = new QMenu(QStringLiteral("设备"), p); m->addAction(QStringLiteral("连接设备")); m->addAction(QStringLiteral("设备管理")); return m; } } // namespace TopBar::TopBar(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("appToolBar")); setFixedHeight(40); // 字号引用 Theme 排版令牌:切换器/一级菜单按钮=body(13)、头像/用户名=body·label(13)、 // 角色名=caption(12)。工具条整体收紧到常见桌面客户端尺寸(行高 40 / 图标 16 / 字号 13)。 // 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent,不再写它们的 QSS。 // 仅保留:工具条底/分隔线、一级菜单按钮、头像(圆形自定义)、用户名/角色。头像白字用 {{text/on-primary}} 令牌。 // 切换器/菜单按钮下拉箭头:用生成的高清 chevron PNG 作 menu-indicator(中性灰双主题可读)。 const QString chevron = geopro::app::writeChevronIcon(true, QColor("#7C8493")); geopro::app::applyTokenizedStyleSheet( this, QStringLiteral( "#appToolBar { background:{{bg/header}}; border-bottom:1px solid {{divider}}; }" "#topDivider { color:{{divider}}; }" "QToolButton::menu-indicator { image:none; }" "#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:6px 24px 6px 10px;" " font-size:%6px; font-weight:%4; }" "#wsSwitcher:hover { background:{{bg/hover}}; }" "#wsSwitcher::menu-indicator { image:url(%7); width:12px; height:12px;" " subcontrol-position: right center; subcontrol-origin: padding; right:8px; }" "QToolButton#menuBtn { color:{{text/primary}}; border:none; border-radius:8px;" " padding:6px 24px 6px 10px; font-size:%3px; font-weight:%4; }" "QToolButton#menuBtn:hover { background:{{bg/hover}}; }" "QToolButton#menuBtn::menu-indicator { image:url(%7); width:12px; height:12px;" " subcontrol-position: right center; subcontrol-origin: padding; right:6px; }" "QToolButton#iconBtn { border:none; border-radius:8px; padding:6px; }" "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:15px; 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::kBody)) .arg(chevron)); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); lay->setSpacing(0); // 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用高清 chevron menu-indicator;数据驱动)。 wsBtn_ = new QToolButton(this); wsBtn_->setObjectName(QStringLiteral("wsSwitcher")); 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_->setMenu(new QMenu(wsBtn_)); lay->addWidget(wsBtn_); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(10); // 项目切换器(QToolButton + 主题化 QSS;数据驱动)。 projBtn_ = new QToolButton(this); projBtn_->setObjectName(QStringLiteral("wsSwitcher")); setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon, kGlyphTextGapPad); // 中性主题色 + 图标→文字6px projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); projBtn_->setPopupMode(QToolButton::InstantPopup); projBtn_->setCursor(Qt::PointingHandCursor); projBtn_->setText(QStringLiteral("正在加载项目…")); projBtn_->setMenu(new QMenu(projBtn_)); lay->addWidget(projBtn_); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(10); // 一级菜单 → 工具条按钮(视图/项目管理/业务工具/设备),纯文字 + 下拉箭头。 // 复用原菜单构造器;菜单作为 popup 挂到按钮(按钮文字取菜单标题)。 lay->addWidget(makeMenuButton(this, buildViewMenu(this))); lay->addWidget(makeMenuButton(this, buildProjectMenu(this))); lay->addWidget(makeMenuButton(this, buildToolsMenu(this))); lay->addWidget(makeMenuButton(this, buildDeviceMenu(this))); lay->addStretch(); lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助"))); // 通知铃铛:固定 32×32(规范 §5 图标按钮尺寸),右上角叠常驻未读红点。 auto* bellBtn = makeIconButton(this, Glyph::Bell, QStringLiteral("通知")); bellBtn->setFixedSize(scaledPx(32), scaledPx(32)); // §5 图标按钮 32×32(随字号缩放) attachNotificationDot(bellBtn); lay->addWidget(bellBtn); auto* gearBtn = makeIconButton(this, Glyph::Gear, QStringLiteral("设置")); if (auto* gb = qobject_cast(gearBtn)) QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); }); lay->addWidget(gearBtn); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(12); // 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。 // 用普通 QWidget + eventFilter:QWidget 按子布局正确撑开(QPushButton 装布局会按空文字算尺寸挤成一团)。 userRow_ = new QWidget(this); userRow_->setObjectName(QStringLiteral("userBtn")); userRow_->setAttribute(Qt::WA_StyledBackground, true); // 令 QSS 背景(hover)在 QWidget 上生效 userRow_->setCursor(Qt::PointingHandCursor); userRow_->installEventFilter(this); auto* uLay = new QHBoxLayout(userRow_); uLay->setContentsMargins(8, 2, 8, 2); uLay->setSpacing(8); auto* avatar = new QLabel(userRow_); avatar->setPixmap( renderAvatar(QStringLiteral("ZL"), 30, geopro::app::tokenColor("accent/primary"), Qt::white)); avatar->setFixedSize(30, 30); avatar->setAttribute(Qt::WA_TransparentForMouseEvents); uLay->addWidget(avatar, 0, Qt::AlignVCenter); auto* nameBox = new QWidget(userRow_); nameBox->setAttribute(Qt::WA_TransparentForMouseEvents); auto* nameLay = new QVBoxLayout(nameBox); nameLay->setContentsMargins(0, 0, 0, 0); nameLay->setSpacing(0); auto* userName = new QLabel(QStringLiteral("张磊"), nameBox); userName->setObjectName(QStringLiteral("userName")); auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox); userRole->setObjectName(QStringLiteral("userRole")); nameLay->addWidget(userName); nameLay->addWidget(userRole); uLay->addWidget(nameBox, 0, Qt::AlignVCenter); auto* chevronLbl = new QLabel(userRow_); chevronLbl->setPixmap(QPixmap(geopro::app::writeChevronIcon(true, QColor("#7C8493"))) .scaled(12, 12, Qt::KeepAspectRatio, Qt::SmoothTransformation)); chevronLbl->setAttribute(Qt::WA_TransparentForMouseEvents); uLay->addWidget(chevronLbl, 0, Qt::AlignVCenter); // 下拉菜单(加宽):账户 / 个人资料 / 偏好设置 / API 密钥 / 退出登录。 userMenu_ = new QMenu(this); userMenu_->setMinimumWidth(200); userMenu_->addAction(QStringLiteral("账户")); userMenu_->addAction(QStringLiteral("个人资料")); QObject::connect(userMenu_->addAction(QStringLiteral("偏好设置")), &QAction::triggered, this, [this] { emit settingsRequested(); }); userMenu_->addAction(QStringLiteral("API 密钥")); userMenu_->addSeparator(); QObject::connect(userMenu_->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, [this] { emit logoutRequested(); }); lay->addWidget(userRow_); } bool TopBar::eventFilter(QObject* obj, QEvent* event) { if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) { if (userMenu_) userMenu_->exec(userRow_->mapToGlobal(QPoint(0, userRow_->height() + 2))); return true; } return QWidget::eventFilter(obj, event); } void TopBar::setWorkspaces(const std::vector& list, const QString& currentId) { auto* menu = new QMenu(wsBtn_); auto* header = menu->addAction(QStringLiteral("切换空间")); header->setEnabled(false); menu->addSeparator(); auto* group = new QActionGroup(menu); group->setExclusive(true); // 互斥:只一个勾选,避免“多选” QString currentName; for (const auto& w : list) { const QString id = QString::fromStdString(w.id); const QString name = QString::fromStdString(w.name); auto* a = menu->addAction(name); a->setCheckable(true); a->setChecked(id == currentId); group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { wsBtn_->setText(name); // 立即反馈 emit workspaceSwitchRequested(id); }); } if (list.empty()) { auto* none = menu->addAction(QStringLiteral("(暂无空间)")); none->setEnabled(false); } wsBtn_->setMenu(menu); wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName); } void TopBar::setProjects(const std::vector& list, const QString& currentId, bool hasMore) { auto* menu = new QMenu(projBtn_); auto* header = menu->addAction(QStringLiteral("切换项目")); header->setEnabled(false); menu->addSeparator(); auto* group = new QActionGroup(menu); group->setExclusive(true); QString currentName; for (const auto& p : list) { const QString id = QString::fromStdString(p.id); const QString name = QString::fromStdString(p.name); auto* a = menu->addAction(name); a->setCheckable(true); a->setChecked(id == currentId); group->addAction(a); if (id == currentId) currentName = name; QObject::connect(a, &QAction::triggered, this, [this, id, name]() { projBtn_->setText(name); emit projectSwitchRequested(id); }); } if (list.empty()) { auto* none = menu->addAction(QStringLiteral("(暂无项目)")); none->setEnabled(false); } if (hasMore) { menu->addSeparator(); auto* all = menu->addAction(QStringLiteral("全部项目…")); QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); }); } projBtn_->setMenu(menu); projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName); } void TopBar::setProjectButtonText(const QString& name) { projBtn_->setText(name); } } // namespace geopro::app