#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 { // ── 专业图标尺寸(统一放大;菜单栏字号亦同步加大)── constexpr int kToolIcon = 22; // 工具条右侧图标 constexpr int kWorkspaceIcon = 20; // 工作空间 / 项目图标 // 竖直分隔细线。 QFrame* makeDivider(QWidget* parent) { auto* line = new QFrame(parent); line->setObjectName(QStringLiteral("topDivider")); line->setFrameShape(QFrame::VLine); line->setFixedWidth(1); line->setFixedHeight(24); 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; } // 圆形头像图标:强调色填充 + 白色缩写。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 QWidget* buildMenuBar(QWidget* parent) { auto* mb = new QMenuBar(parent); mb->setObjectName(QStringLiteral("appMenuBar")); // ElaMenuBar 自绘 Fluent 外观并自动随 ElaTheme 明暗,不再写内联 QSS。 mb->addMenu(buildViewMenu(mb)); mb->addMenu(buildProjectMenu(mb)); mb->addMenu(buildToolsMenu(mb)); mb->addMenu(buildDeviceMenu(mb)); return mb; } TopBar::TopBar(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("appToolBar")); setFixedHeight(56); // 字号引用 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; }" "#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); // 工作空间切换器(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->addStretch(); lay->addWidget(makeIconButton(this, Glyph::Help, QStringLiteral("帮助"))); lay->addWidget(makeIconButton(this, Glyph::Bell, QStringLiteral("通知"))); auto* gearBtn = makeIconButton(this, Glyph::Gear, QStringLiteral("设置")); if (auto* gb = qobject_cast(gearBtn)) QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); }); lay->addWidget(gearBtn); 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, 3, 8, 3); uLay->setSpacing(10); auto* avatar = new QLabel(userRow_); avatar->setPixmap( renderAvatar(QStringLiteral("ZL"), 34, geopro::app::tokenColor("accent/primary"), Qt::white)); avatar->setFixedSize(34, 34); avatar->setAttribute(Qt::WA_TransparentForMouseEvents); uLay->addWidget(avatar, 0, Qt::AlignVCenter); auto* nameBox = new QWidget(userRow_); nameBox->setAttribute(Qt::WA_TransparentForMouseEvents); auto* nameLay = new QVBoxLayout(nameBox); nameLay->setContentsMargins(0, 0, 0, 0); nameLay->setSpacing(0); auto* userName = new QLabel(QStringLiteral("张磊"), nameBox); userName->setObjectName(QStringLiteral("userName")); auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox); userRole->setObjectName(QStringLiteral("userRole")); nameLay->addWidget(userName); nameLay->addWidget(userRole); uLay->addWidget(nameBox, 0, Qt::AlignVCenter); auto* chevronLbl = new QLabel(userRow_); chevronLbl->setPixmap(QPixmap(geopro::app::writeChevronIcon(true, QColor("#7C8493"))) .scaled(12, 12, Qt::KeepAspectRatio, Qt::SmoothTransformation)); chevronLbl->setAttribute(Qt::WA_TransparentForMouseEvents); uLay->addWidget(chevronLbl, 0, Qt::AlignVCenter); // 下拉菜单(加宽):账户 / 个人资料 / 偏好设置 / API 密钥 / 退出登录。 userMenu_ = new QMenu(this); userMenu_->setMinimumWidth(200); userMenu_->addAction(QStringLiteral("账户")); userMenu_->addAction(QStringLiteral("个人资料")); QObject::connect(userMenu_->addAction(QStringLiteral("偏好设置")), &QAction::triggered, this, [this] { emit settingsRequested(); }); userMenu_->addAction(QStringLiteral("API 密钥")); userMenu_->addSeparator(); QObject::connect(userMenu_->addAction(QStringLiteral("退出登录")), &QAction::triggered, this, [this] { emit logoutRequested(); }); 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