413 lines
18 KiB
C++
413 lines
18 KiB
C++
#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-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<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 + 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<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
|