geopro/src/app/TopBar.cpp

413 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "TopBar.hpp"
#include <QAbstractButton>
#include <QActionGroup>
#include <QColor>
#include <QEvent>
#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
#include <QSize>
#include <QTimer>
#include <QVBoxLayout>
#include <QStringList>
#include <QToolButton>
#include <QWidget>
#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-indicatorInstantPopup 挂菜单。
// 文字取菜单标题(视图/项目管理/…),样式见 #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<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)
{
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<QAbstractButton*>(gearBtn))
QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); });
lay->addWidget(gearBtn);
lay->addSpacing(10);
lay->addWidget(makeDivider(this));
lay->addSpacing(12);
// 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。
// 用普通 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, 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<data::Workspace>& 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<data::ProjectSummary>& 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