Ver Fonte

备份更新

zhuizhu há 7 meses atrás
pai
commit
70d07dd6a6

+ 1 - 0
.gitignore

@@ -19,3 +19,4 @@
 /.xmake
 /ci_build
 /.vscode
+/bin

+ 3 - 0
.trae/rules/project_rules.md

@@ -0,0 +1,3 @@
+项目全局使用qt5.15
+项目全局使用c++17
+代码使用Qt风格编写

+ 2 - 0
LearningSmartClient.pro

@@ -45,6 +45,7 @@ SOURCES += \
     widgets/colorlistwidget.cpp \
     widgets/createmeetingdialog.cpp \
     widgets/framelessbase.cpp \
+    widgets/functionbutton.cpp \
     widgets/joinmeetingdialog.cpp \
     widgets/maskoverlay.cpp \
     widgets/meetingselectionwidget.cpp \
@@ -85,6 +86,7 @@ HEADERS += \
     widgets/colorlistwidget.h \
     widgets/createmeetingdialog.h \
     widgets/framelessbase.h \
+    widgets/functionbutton.h \
     widgets/joinmeetingdialog.h \
     widgets/maskoverlay.h \
     widgets/meetingselectionwidget.h \

+ 246 - 40
MainPanel.cpp

@@ -16,15 +16,112 @@
 #include "widgets/bubbletip.h"
 #include "widgets/chatView/chatwindow.h"
 
-#include "advanceddockingsystem/dockmanager.h"
+#include "widgets/functionbutton.h"
 #include "widgets/maskoverlay.h"
-#include "widgets/userprofilewidget.h"
 #include "widgets/statswidget.h"
+#include "widgets/userprofilewidget.h"
 
 #include "AvPlayer2/PlayWidget.h"
 #include "api/roomapi.h"
 #include "appevent.h"
 #include "ui/av_recorder.h"
+#include "widgets/audiodeviceselectoricon.h"
+// #include "widgets/audiodeviceselectoricon_decoupled.h"
+
+namespace IconUtils {
+QIcon createSettingsIcon()
+{
+    QPixmap icon(24, 24);
+    icon.fill(Qt::transparent);
+
+    QPainter painter(&icon);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    // 绘制齿轮形状
+    painter.setPen(Qt::white);
+    painter.setBrush(Qt::white);
+    painter.drawEllipse(4, 4, 16, 16);
+
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(QColor(64, 158, 255));
+    painter.drawEllipse(8, 8, 8, 8);
+
+    painter.setBrush(Qt::white);
+    painter.drawRect(11, 2, 2, 6);
+    painter.drawRect(11, 16, 2, 6);
+    painter.drawRect(2, 11, 6, 2);
+    painter.drawRect(16, 11, 6, 2);
+
+    return QIcon(icon);
+}
+
+QIcon createSearchIcon()
+{
+    QPixmap icon(24, 24);
+    icon.fill(Qt::transparent);
+
+    QPainter painter(&icon);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    // 绘制放大镜
+    painter.setPen(Qt::white);
+    painter.setBrush(Qt::white);
+    painter.drawEllipse(2, 2, 14, 14);
+
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(QColor(64, 158, 255));
+    painter.drawEllipse(4, 4, 10, 10);
+
+    painter.setBrush(Qt::white);
+    QPolygon handle;
+    handle << QPoint(14, 14) << QPoint(20, 20) << QPoint(18, 22) << QPoint(12, 16);
+    painter.drawPolygon(handle);
+
+    return QIcon(icon);
+}
+
+QIcon createUserIcon()
+{
+    QPixmap icon(24, 24);
+    icon.fill(Qt::transparent);
+
+    QPainter painter(&icon);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    // 绘制用户轮廓
+    painter.setPen(Qt::white);
+    painter.setBrush(Qt::white);
+    painter.drawEllipse(4, 2, 16, 16); // 头部
+    painter.drawRect(6, 18, 12, 4);    // 身体
+
+    return QIcon(icon);
+}
+
+QIcon createAudioDeviceIcon()
+{
+    QPixmap icon(24, 24);
+    icon.fill(Qt::transparent);
+
+    QPainter painter(&icon);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    // 绘制音频设备图标(麦克风+扬声器组合)
+    painter.setPen(QPen(Qt::white, 1.5));
+    painter.setBrush(Qt::white);
+    
+    // 麦克风部分
+    painter.drawRoundedRect(3, 2, 6, 8, 2, 2);
+    painter.drawLine(6, 10, 6, 13);
+    painter.drawLine(4, 13, 8, 13);
+    
+    // 扬声器部分
+    painter.drawRect(13, 8, 3, 3);
+    painter.drawPolygon(QPolygon() << QPoint(16, 8) << QPoint(19, 6) << QPoint(19, 13) << QPoint(16, 11));
+    painter.drawArc(20, 7, 2, 4, 0, 180 * 16);
+
+    return QIcon(icon);
+}
+} // namespace IconUtils
 
 MainPanel::MainPanel(QWidget *parent)
     : QWidget(parent)
@@ -42,54 +139,102 @@ MainPanel::MainPanel(QWidget *parent)
     chatView = new ChatWindow(webSocketClient);
     chatView->setMinimumWidth(400);
     statsWidget = new StatsWidget(this);
-    
-    // 初始化 DockManager
-    m_dockManager = new ADS::DockManager(this);
-    m_dockManager->setStyleSheet(""); // 使用应用程序的样式表
 
-    // 创建右侧面板作为可停靠的 DockWidget
     QWidget *rightWidget = new QWidget;
     QVBoxLayout *vbox = new QVBoxLayout(rightWidget);
     vbox->setContentsMargins(0, 0, 0, 0);
     vbox->addWidget(userProfile, 0);
     vbox->addWidget(statsWidget, 0);
     vbox->addWidget(chatView, 1);
-    
-    ADS::DockWidget* rightDockWidget = new ADS::DockWidget("聊天面板", this);
-    rightDockWidget->setWidget(rightWidget);
-    rightDockWidget->setFeature(ADS::DockWidget::DockWidgetClosable, true);
 
-    // 创建中央内容
+    splitter = new QSplitter(Qt::Horizontal, this);
     playerContainer = new QWidget(this);
-    
-    // 创建中央播放器 DockWidget
-    ADS::DockWidget* centerDockWidget = new ADS::DockWidget("播放器", this);
-    centerDockWidget->setWidget(playerContainer);
-    centerDockWidget->setFeature(ADS::DockWidget::DockWidgetClosable, false);
-    centerDockWidget->setFeature(ADS::DockWidget::DockWidgetMovable, false);
-    centerDockWidget->setFeature(ADS::DockWidget::DockWidgetFloatable, false);
-    
-    // 将 DockWidget 添加到 DockManager
-    // m_dockManager->addDockWidget(ADS::LeftDockWidgetArea, leftDockWidget);
-    m_dockManager->addDockWidget(ADS::CenterDockWidgetArea, centerDockWidget);
-    m_dockManager->addDockWidget(ADS::RightDockWidgetArea, rightDockWidget);
-    
-    // 保存 splitter 引用以保持兼容性
-    splitter = nullptr; // 不再使用 QSplitter
-    
+    splitter->addWidget(playerContainer);
+    splitter->addWidget(rightWidget);
+    splitter->setStretchFactor(0, 60);
+    splitter->setStretchFactor(1, 30);
     QVBoxLayout *mainLayout = new QVBoxLayout(this);
-    mainLayout->addWidget(m_dockManager, 1);
+    mainLayout->addWidget(splitter, 1);
     mainLayout->setContentsMargins(0, 0, 0, 0);
     mainLayout->setSpacing(0);
     setLayout(mainLayout);
 
+    // 为playerContainer设置初始布局
+    QVBoxLayout *playerLayout = new QVBoxLayout(playerContainer);
+    playerLayout->setContentsMargins(0, 0, 0, 0);
+    
+    buttonGroup = new PopoverButtonGroup(Qt::Horizontal, playerContainer);
+
+    // 添加功能按钮
+    FunctionButton *settingsBtn = new FunctionButton(IconUtils::createSettingsIcon(), "设置", this);
+    Popover *settingsPopover = new Popover(this);
+    // settingsPopover->setContentWidget(settingsContent);
+    buttonGroup->addButton(settingsBtn, settingsPopover);
+
+    FunctionButton *searchBtn = new FunctionButton(IconUtils::createSearchIcon(), "搜索", this);
+    Popover *searchPopover = new Popover(this);
+    // searchPopover->setContentWidget(searchContent);
+    buttonGroup->addButton(searchBtn, searchPopover);
+
+    FunctionButton *userBtn = new FunctionButton(IconUtils::createUserIcon(), "用户", this);
+    Popover *userPopover = new Popover(this);
+    // userPopover->setContentWidget(userContent);
+    buttonGroup->addButton(userBtn, userPopover);
+
+    // 添加音频设备选择按钮
+    FunctionButton *audioDeviceBtn = new FunctionButton(IconUtils::createAudioDeviceIcon(), "音频设备", this);
+    Popover *audioDevicePopover = new Popover(this);
+
+    // // 使用解耦版本的音频设备选择器
+    // m_audioDeviceSelectorDecoupled = new AudioDeviceSelectorIconDecoupled(this);
+    // audioDevicePopover->setContentWidget(m_audioDeviceSelectorDecoupled);
+    // buttonGroup->addButton(audioDeviceBtn, audioDevicePopover);
+
+    // // 初始化音频设备选择器
+    // initAudioDeviceSelectors();
+
+    // // 连接解耦版本的音频设备选择信号
+    // connect(m_audioDeviceSelectorDecoupled, &AudioDeviceSelectorIconDecoupled::microphoneDeviceSelected,
+    //         this, &MainPanel::onMicrophoneDeviceSelected);
+    // connect(m_audioDeviceSelectorDecoupled, &AudioDeviceSelectorIconDecoupled::speakerDeviceSelected,
+    //         this, &MainPanel::onSpeakerDeviceSelected);
+
+    // 保持原有的AudioDeviceSelectorIcon作为备用(兼容模式)
+    // m_audioDeviceSelector = new AudioDeviceSelectorIcon(this);
+    // connect(m_audioDeviceSelector, &AudioDeviceSelectorIcon::microphoneDeviceSelected,
+    //         this, [this](const AudioDeviceInfo& device) {
+    //             qDebug() << "[MainPanel] 麦克风设备已选择(兼容模式):" << device.name;
+    //             // 通知AvRecorder进行设备切换
+    //             if (AvRecorder *avRecorder = qobject_cast<AvRecorder *>(playerWidget)) {
+    //                 //avRecorder->switchMicrophoneDevice(device.id, device.name);
+    //             }
+    //         });
+    // connect(m_audioDeviceSelector, &AudioDeviceSelectorIcon::speakerDeviceSelected,
+    //         this, [this](const AudioDeviceInfo& device) {
+    //             qDebug() << "[MainPanel] 扬声器设备已选择:" << device.name;
+    //             // 通知AvRecorder进行设备切换
+    //             if (AvRecorder *avRecorder = qobject_cast<AvRecorder *>(playerWidget)) {
+    //                 // avRecorder->switchSpeakerDevice(device.id, device.name);
+    //             }
+    //         });
+
+    // 添加一个动作按钮到按钮组(没有Popover)
+    FunctionButton *actionButton = new FunctionButton(IconUtils::createSettingsIcon(),
+                                                      "执行操作",
+                                                      this);
+    buttonGroup->addButton(actionButton, nullptr);
+    
+    // 将buttonGroup添加到playerContainer的布局中
+    playerLayout->addStretch(1);  // 添加弹性空间,将buttonGroup推到底部
+    playerLayout->addWidget(buttonGroup, 0);  // 添加buttonGroup到底部,不拉伸
+    buttonGroup->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);  // 使用固定大小策略
+
     // initConnect
     connect(AppEvent::instance(), &AppEvent::connectionStateChanged, this, [this](bool connected) {
         if (userProfile) {
             userProfile->setStatus(connected ? "在线" : "离线");
         }
     });
-
     connect(userProfile, &UserProfileWidget::logoutClicked, this, &MainPanel::logoutClicked);
     connect(webSocketClient, &WebSocketClient::statsUpdate, statsWidget, &StatsWidget::updateStats);
     connect(webSocketClient, &WebSocketClient::liveStatus, this, [this](const QString &msg) {
@@ -106,7 +251,7 @@ MainPanel::MainPanel(QWidget *parent)
             qDebug() << "[MainPanel] liveStatus: 直播中" << chatView;
             if (chatView) {
                 const QString id = webSocketClient->roomId();
-                
+
                 // 使用防抖机制处理频繁的请求
                 m_pendingRoomId = id;
                 m_debounceTimer->start(); // 重新开始计时,如果在500ms内再次收到请求,会重置定时器
@@ -191,22 +336,41 @@ void MainPanel::setPlayerWidget(QWidget *newPlayer)
     }
     playerWidget = newPlayer;
     playerWidget->setParent(playerContainer);
-    QLayout *oldLayout = playerContainer->layout();
-    if (oldLayout) {
+    
+    // 获取现有布局并清理(保留buttonGroup)
+    QVBoxLayout *vbox = qobject_cast<QVBoxLayout*>(playerContainer->layout());
+    if (vbox) {
+        // 清理除了buttonGroup之外的所有项目
         QLayoutItem *item;
-        while ((item = oldLayout->takeAt(0)) != nullptr) {
-            if (item->widget())
+        while (vbox->count() > 0) {
+            item = vbox->takeAt(0);
+            if (item->widget() && item->widget() != buttonGroup) {
                 item->widget()->setParent(nullptr);
-            delete item;
+            }
+            if (item->spacerItem()) {
+                delete item;  // 删除spacer
+            } else if (item->widget() != buttonGroup) {
+                delete item;
+            }
         }
-        delete oldLayout;
+    } else {
+        // 如果没有布局或布局类型不对,创建新的
+        if (playerContainer->layout()) {
+            delete playerContainer->layout();
+        }
+        vbox = new QVBoxLayout(playerContainer);
+        vbox->setContentsMargins(0, 0, 0, 0);
     }
-    QVBoxLayout *vbox = new QVBoxLayout(playerContainer);
-    vbox->setContentsMargins(0, 0, 0, 0);
-    vbox->addWidget(playerWidget, 1);  // 添加拉伸因子,让播放器组件占据所有可用空间
     
+    // 重新添加组件:播放器在上,buttonGroup在下
+    vbox->addWidget(playerWidget, 1);  // 添加拉伸因子,让播放器组件占据所有可用空间
+    vbox->addWidget(buttonGroup, 0);   // 添加buttonGroup到底部,不拉伸
+
     // 确保播放器组件能够正确拉伸
     playerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    
+    // 确保buttonGroup固定在底部
+    buttonGroup->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);  // 使用固定大小策略
 }
 
 
@@ -230,3 +394,45 @@ void MainPanel::handleDebouncedPlay()
     // 清空待处理的房间ID
     m_pendingRoomId.clear();
 }
+
+void MainPanel::initAudioDeviceSelectors()
+{
+    // 注册音频设备选择器工厂
+    // DeviceManager* deviceManager = DeviceManager::instance();
+    // AudioDeviceSelectorFactory* audioFactory = new AudioDeviceSelectorFactory(this);
+    // deviceManager->registerFactory("audio", audioFactory);
+
+    // // 创建麦克风和扬声器选择器
+    // m_microphoneSelector = deviceManager->createSelector("audio", "microphone");
+    // m_speakerSelector = deviceManager->createSelector("audio", "speaker");
+
+    // // 设置到解耦版本的UI组件
+    // if (m_audioDeviceSelectorDecoupled) {
+    //     m_audioDeviceSelectorDecoupled->setMicrophoneSelector(m_microphoneSelector);
+    //     m_audioDeviceSelectorDecoupled->setSpeakerSelector(m_speakerSelector);
+    // }
+
+    qDebug() << "[MainPanel] 音频设备选择器初始化完成";
+}
+
+void MainPanel::onMicrophoneDeviceSelected(const DeviceInfo& device)
+{
+    qDebug() << "[MainPanel] 麦克风设备已选择:" << device.name;
+    
+    // 通知AvRecorder进行设备切换
+    if (AvRecorder *avRecorder = qobject_cast<AvRecorder *>(playerWidget)) {
+        // 这里可以添加设备切换逻辑
+        // avRecorder->switchMicrophoneDevice(device.id, device.name);
+    }
+}
+
+void MainPanel::onSpeakerDeviceSelected(const DeviceInfo& device)
+{
+    qDebug() << "[MainPanel] 扬声器设备已选择:" << device.name;
+    
+    // 通知AvRecorder进行设备切换
+    if (AvRecorder *avRecorder = qobject_cast<AvRecorder *>(playerWidget)) {
+        // 这里可以添加设备切换逻辑
+        // avRecorder->switchSpeakerDevice(device.id, device.name);
+    }
+}

+ 16 - 0
MainPanel.h

@@ -5,6 +5,7 @@
 #include <QMutex>
 #include <QWaitCondition>
 #include <QTimer>
+#include "util/deviceinterface.h"
 
 class QSplitter;
 
@@ -13,6 +14,8 @@ class ChatWindow;
 
 class WebSocketClient;
 class StatsWidget;
+class AudioDeviceSelectorIcon;
+class AudioDeviceSelectorIconDecoupled;
 
 namespace ADS {
     class DockManager;
@@ -55,7 +58,20 @@ private:
     QWaitCondition m_playCond;
     QTimer *m_debounceTimer = nullptr;
     QString m_pendingRoomId;
+
+    class PopoverButtonGroup *buttonGroup;
+    AudioDeviceSelectorIcon *m_audioDeviceSelector = nullptr;
+    AudioDeviceSelectorIconDecoupled *m_audioDeviceSelectorDecoupled = nullptr;
     
+    // 设备选择器
+    IDeviceSelector* m_microphoneSelector = nullptr;
+    IDeviceSelector* m_speakerSelector = nullptr;
+
     // DockManager 相关
     ADS::DockManager *m_dockManager = nullptr;
+    
+private slots:
+    void initAudioDeviceSelectors();
+    void onMicrophoneDeviceSelected(const DeviceInfo& device);
+    void onSpeakerDeviceSelected(const DeviceInfo& device);
 };

BIN
bin/2025-08-26-21-44-41.mp4


BIN
bin/LearningSmartClient.exe


BIN
bin/LearningSmartClient.exp


BIN
bin/LearningSmartClient.lib


BIN
bin/LearningSmartClient.pdb


BIN
bin/avcodec-61.dll


BIN
bin/avdevice-61.dll


BIN
bin/avfilter-10.dll


BIN
bin/avformat-61.dll


BIN
bin/avutil-59.dll


BIN
bin/bin_0.1.2.7z


BIN
bin/bin_0.1.3.7z


BIN
bin/bin_0.2.1.7z


BIN
bin/bin_0.3.1.7z


+ 0 - 0
bin/log.txt


BIN
bin/postproc-58.dll


BIN
bin/swresample-5.dll


BIN
bin/swscale-8.dll


+ 0 - 65
libs/AvRecorder/ui/av_recorder.cpp

@@ -112,37 +112,6 @@ AvRecorder::AvRecorder(QWidget* parent)
     centerLayout->addLayout(leftLayout, 2);
     centerLayout->addLayout(rightLayout, 3);
 
-    // 10. 功能键区域
-    m_openBtn = new QPushButton("打开");
-    m_saveBtn = new QPushButton("保存");
-    m_shareBtn = new QPushButton("共享屏幕");
-    m_loginBtn = new QPushButton("登录");
-    m_meetingBtn = new QPushButton("会议");
-    m_aiBtn = new QPushButton("AI助理");
-    m_appBtn = new QPushButton("应用");
-    m_conferenceBtn = new QPushButton("线程会议");
-    
-    // 设置功能键样式
-    QList<QPushButton*> functionBtns = {m_openBtn, m_saveBtn, m_shareBtn, m_loginBtn, 
-                                        m_meetingBtn, m_aiBtn, m_appBtn, m_conferenceBtn};
-    for (auto* btn : functionBtns) {
-        btn->setMinimumHeight(35);
-        btn->setMaximumHeight(35);
-        btn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-    }
-    
-    QHBoxLayout* functionLayout = new QHBoxLayout;
-    functionLayout->setSpacing(8);
-    functionLayout->setContentsMargins(10, 5, 10, 5);
-    functionLayout->addWidget(m_openBtn);
-    functionLayout->addWidget(m_saveBtn);
-    functionLayout->addWidget(m_shareBtn);
-    functionLayout->addWidget(m_loginBtn);
-    functionLayout->addWidget(m_meetingBtn);
-    functionLayout->addWidget(m_aiBtn);
-    functionLayout->addWidget(m_appBtn);
-    functionLayout->addWidget(m_conferenceBtn);
-
     // 11. 状态栏
     initStatusBarUi();
 
@@ -152,7 +121,6 @@ AvRecorder::AvRecorder(QWidget* parent)
     // 12. 总体布局
     QVBoxLayout* mainLayout = new QVBoxLayout(this);
     mainLayout->addWidget(m_glWidget, 100);
-    mainLayout->addLayout(functionLayout, 0);  // 添加功能键区域
     mainLayout->addLayout(centerLayout, 0);
     mainLayout->addWidget(m_statusBar, 0);
     setLayout(mainLayout);
@@ -332,39 +300,6 @@ void AvRecorder::initConnect()
             m_captureTimeLabel->setText("00:00:00");
         }
     });
-    
-    // 连接功能键信号槽
-    connect(m_openBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "打开功能");
-    });
-    
-    connect(m_saveBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "保存功能");
-    });
-    
-    connect(m_shareBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "共享屏幕功能");
-    });
-    
-    connect(m_loginBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "登录功能");
-    });
-    
-    connect(m_meetingBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "会议功能");
-    });
-    
-    connect(m_aiBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "AI助理功能");
-    });
-    
-    connect(m_appBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "应用功能");
-    });
-    
-    connect(m_conferenceBtn, &QPushButton::clicked, this, [this] {
-        QMessageBox::information(this, "提示", "线程会议功能");
-    });
 }
 
 AvRecorder::~AvRecorder()

+ 1 - 11
libs/AvRecorder/ui/av_recorder.h

@@ -72,17 +72,7 @@ private:
     QComboBox* m_captureComboBox = nullptr;
 
     QPushButton* m_updateListBtn = nullptr;
-    
-    // 功能键按钮
-    QPushButton* m_openBtn = nullptr;      // 打开
-    QPushButton* m_saveBtn = nullptr;      // 保存
-    QPushButton* m_shareBtn = nullptr;     // 共享屏幕
-    QPushButton* m_loginBtn = nullptr;     // 登录
-    QPushButton* m_meetingBtn = nullptr;   // 会议
-    QPushButton* m_aiBtn = nullptr;        // AI助理
-    QPushButton* m_appBtn = nullptr;       // 应用
-    QPushButton* m_conferenceBtn = nullptr; // 线程会议
-    
+
     bool m_isRecord = false;
     bool m_isLive = false;
     bool m_isSyncRecord = false; // 是否同步录像

+ 540 - 0
widgets/functionbutton.cpp

@@ -0,0 +1,540 @@
+#include "functionbutton.h"
+#include <QAbstractAnimation>
+#include <QApplication>
+#include <QGuiApplication>
+#include <QPropertyAnimation>
+#include <QScreen>
+#include <QDebug>
+#include <QResizeEvent>
+
+namespace Colors {
+// Element Plus 主题色
+const QColor Primary = QColor(64, 158, 255);       // #409EFF
+const QColor PrimaryLight = QColor(144, 202, 249); // #90CAF9
+const QColor PrimaryDark = QColor(25, 118, 210);   // #1976D2
+
+// 功能色
+const QColor Success = QColor(103, 194, 58); // #67C23A
+const QColor Warning = QColor(230, 162, 60); // #E6A23C
+const QColor Danger = QColor(245, 108, 108); // #F56C6C
+const QColor Info = QColor(144, 147, 153);   // #909399
+
+// 中性色
+const QColor TextPrimary = QColor(48, 49, 51);        // #303133
+const QColor TextRegular = QColor(96, 98, 102);       // #606266
+const QColor TextSecondary = QColor(144, 147, 153);   // #909399
+const QColor TextPlaceholder = QColor(192, 196, 204); // #C0C4CC
+
+// 边框色
+const QColor BorderBase = QColor(220, 223, 230);       // #DCDFE6
+const QColor BorderLight = QColor(228, 231, 237);      // #E4E7ED
+const QColor BorderLighter = QColor(235, 238, 245);    // #EBEEF5
+const QColor BorderExtraLight = QColor(244, 245, 247); // #F4F5F7
+
+// 背景色
+const QColor Background = Qt::white;                 // #FFFFFF
+const QColor BackgroundPage = QColor(245, 245, 245); // #F5F5F5
+const QColor BackgroundBase = QColor(248, 249, 250); // #F8F9FA
+
+// 兼容旧版本
+const QColor PrimaryHover = PrimaryLight;
+const QColor PrimaryPressed = PrimaryDark;
+const QColor Border = BorderBase;
+const QColor Text = TextPrimary;
+const QColor SecondaryText = TextSecondary;
+const QColor Shadow = QColor(0, 0, 0, 60);
+const QColor Indicator = Info;
+} // namespace Colors
+namespace Fonts {
+static QFont buttonFont()
+{
+    QFont font;
+    font.setPointSize(10);
+    font.setWeight(QFont::Medium);
+    return font;
+}
+} // namespace Fonts
+
+// PopoverTriggerButton 实现
+PopoverTriggerButton::PopoverTriggerButton(QWidget* parent)
+    : QPushButton(parent)
+{
+    setCursor(Qt::PointingHandCursor);
+    setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+    setFixedSize(20, 70);
+    setMinimumSize(20, 70);
+    setMaximumSize(20, 70);
+}
+
+QSize PopoverTriggerButton::sizeHint() const
+{
+    return QSize(20, 70); // 强制返回固定大小
+}
+
+QSize PopoverTriggerButton::minimumSizeHint() const
+{
+    return QSize(20, 70); // 强制返回固定最小大小
+}
+
+void PopoverTriggerButton::resizeEvent(QResizeEvent* event)
+{
+    // 强制保持固定大小,忽略任何外部尺寸变化
+    if (size() != QSize(20, 70)) {
+        setFixedSize(20, 70);
+    }
+    QPushButton::resizeEvent(event);
+}
+
+Popover::Popover(QWidget* parent)
+    : QWidget(parent)
+{
+    // 使用更安全的窗口标志组合,避免分层窗口问题
+    setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
+
+    // 移除透明背景属性,避免UpdateLayeredWindowIndirect错误
+    // setAttribute(Qt::WA_TranslucentBackground);
+
+    // 设置窗口属性
+    setAttribute(Qt::WA_DeleteOnClose, false);
+    setAttribute(Qt::WA_ShowWithoutActivating, true);
+    setAttribute(Qt::WA_X11NetWmWindowTypePopupMenu, true);
+
+    // 暂时移除阴影效果,避免分层窗口问题
+    // 我们将在paintEvent中手动绘制阴影
+    /*
+    auto shadow = new QGraphicsDropShadowEffect(this);
+    shadow->setBlurRadius(qMax(0.0, 20.0));
+    shadow->setColor(Colors::Shadow);
+    shadow->setXOffset(0);
+    shadow->setYOffset(qMax(0.0, 5.0));
+    setGraphicsEffect(shadow);
+    */
+
+    mainLayout = new QVBoxLayout(this);
+    mainLayout->setContentsMargins(25, 25, 25, 25);
+    mainLayout->setSpacing(10);
+
+    // 移除样式表,使用paintEvent手动绘制背景
+    // setStyleSheet("QWidget { background-color: white; border: 1px solid #e0e0e0; border-radius: 8px; }");
+
+    // 连接应用程序状态变化信号
+    connect(qApp, &QApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
+        if (state != Qt::ApplicationActive && isVisible()) {
+            hideAnimated();
+        }
+    });
+
+    // 安装事件过滤器以检测点击外部区域
+    qApp->installEventFilter(this);
+}
+
+Popover::~Popover()
+{
+    // 移除事件过滤器
+    qApp->removeEventFilter(this);
+}
+
+void Popover::setContentWidget(QWidget* widget)
+{
+    // 清除现有内容
+    QLayoutItem* child;
+    while ((child = mainLayout->takeAt(0)) != nullptr) {
+        delete child->widget();
+        delete child;
+    }
+
+    // 添加新内容
+    mainLayout->addWidget(widget);
+
+    // 动态计算最小尺寸,考虑阴影边距
+    if (widget->minimumSize().isValid()) {
+        QMargins margins = mainLayout->contentsMargins();
+        int extraWidth = margins.left() + margins.right();
+        int extraHeight = margins.top() + margins.bottom();
+        setMinimumSize(widget->minimumSize() + QSize(extraWidth, extraHeight));
+    }
+}
+
+void Popover::showAnimated(const QPoint& position)
+{
+    setOpacity(0.0);
+
+    // 确保窗口尺寸有效
+    if (width() <= 0 || height() <= 0) {
+        adjustSize();
+    }
+
+    // 确保位置在屏幕范围内
+    QScreen* screen = QGuiApplication::screenAt(position);
+    if (!screen) {
+        screen = QGuiApplication::primaryScreen();
+    }
+
+    if (screen) {
+        QRect screenGeometry = screen->availableGeometry();
+        QPoint adjustedPos = position;
+
+        // 计算阴影边距,确保为正值
+        QMargins shadowMargins = this->shadowMargins();
+        int leftMargin = qMax(0, shadowMargins.left());
+        int topMargin = qMax(0, shadowMargins.top());
+        int rightMargin = qMax(0, shadowMargins.right());
+        int bottomMargin = qMax(0, shadowMargins.bottom());
+
+        // 确保窗口完全在屏幕内
+        int minX = screenGeometry.left() + leftMargin;
+        int minY = screenGeometry.top() + topMargin;
+        int maxX = screenGeometry.right() - width() - rightMargin;
+        int maxY = screenGeometry.bottom() - height() - bottomMargin;
+
+        // 调整X坐标
+        adjustedPos.setX(qBound(minX, adjustedPos.x(), maxX));
+
+        // 调整Y坐标
+        adjustedPos.setY(qBound(minY, adjustedPos.y(), maxY));
+
+        // 最终验证位置的有效性
+        if (adjustedPos.x() >= 0 && adjustedPos.y() >= 0) {
+            move(adjustedPos);
+        } else {
+            // 如果计算出的位置仍然无效,使用屏幕中心
+            QPoint centerPos = screenGeometry.center() - QPoint(width() / 2, height() / 2);
+            move(centerPos);
+        }
+    } else {
+        // 如果无法获取屏幕信息,确保位置为正值
+        QPoint safePos = QPoint(qMax(0, position.x()), qMax(0, position.y()));
+        move(safePos);
+    }
+
+    show();
+
+    QPropertyAnimation* animation = new QPropertyAnimation(this, "opacity");
+    animation->setDuration(200);
+    animation->setStartValue(0.0);
+    animation->setEndValue(1.0);
+    animation->setEasingCurve(QEasingCurve::OutCubic);
+    animation->start(QAbstractAnimation::DeleteWhenStopped);
+}
+
+void Popover::hideAnimated()
+{
+    QPropertyAnimation* animation = new QPropertyAnimation(this, "opacity");
+    animation->setDuration(150);
+    animation->setStartValue(1.0);
+    animation->setEndValue(0.0);
+    animation->setEasingCurve(QEasingCurve::InCubic);
+    connect(animation, &QPropertyAnimation::finished, this, &Popover::hide);
+    animation->start(QAbstractAnimation::DeleteWhenStopped);
+}
+
+void Popover::setOpacity(qreal opacity)
+{
+    m_opacity = opacity;
+    setWindowOpacity(opacity);
+}
+
+QMargins Popover::shadowMargins() const
+{
+    // 返回固定的小边距,用于手动绘制的阴影
+    return QMargins(5, 5, 5, 5);
+}
+
+void Popover::closeEvent(QCloseEvent* event)
+{
+    emit popoverClosed();
+    QWidget::closeEvent(event);
+}
+
+void Popover::paintEvent(QPaintEvent* event)
+{
+    Q_UNUSED(event);
+
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    QRect widgetRect = rect();
+
+    // 手动绘制简单的阴影效果
+    QMargins shadow = shadowMargins();
+
+    // 绘制阴影 - 简单的偏移矩形
+    QRect shadowRect = widgetRect.adjusted(shadow.left(), shadow.top(), 0, 0);
+    if (shadowRect.width() > 0 && shadowRect.height() > 0) {
+        painter.setPen(Qt::NoPen);
+        painter.setBrush(QColor(0, 0, 0, 30)); // 半透明黑色阴影
+        painter.drawRoundedRect(shadowRect, 8, 8);
+    }
+
+    // 绘制主背景
+    QRect bgRect = widgetRect.adjusted(0, 0, -shadow.right(), -shadow.bottom());
+    if (bgRect.width() > 0 && bgRect.height() > 0) {
+        painter.setPen(Colors::Border);
+        painter.setBrush(Colors::Background);
+        painter.drawRoundedRect(bgRect, 8, 8);
+    }
+}
+
+QSize Popover::sizeHint() const
+{
+    if (mainLayout->count() > 0) {
+        QWidget* content = mainLayout->itemAt(0)->widget();
+        if (content) {
+            QMargins margins = mainLayout->contentsMargins();
+            return content->sizeHint()
+                   + QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
+        }
+    }
+    return QWidget::sizeHint();
+}
+
+bool Popover::eventFilter(QObject* obj, QEvent* event)
+{
+    if (event->type() == QEvent::MouseButtonPress
+        || event->type() == QEvent::NonClientAreaMouseButtonPress) {
+        QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
+        // 检查点击是否在弹窗外部,如果是则关闭弹窗
+        if (isVisible() && !geometry().contains(mouseEvent->globalPos())) {
+            // 如果点击的是父窗口或其他任何地方,都关闭弹窗
+            hideAnimated();
+            return false; // 不阻止事件传播,让父窗口能正常处理点击
+        }
+    }
+    // 检测窗口激活状态变化
+    else if (event->type() == QEvent::WindowActivate) {
+        // 如果其他窗口被激活,关闭弹窗
+        if (isVisible() && obj != this) {
+            hideAnimated();
+        }
+    }
+    return QWidget::eventFilter(obj, event);
+}
+
+///////////////////
+
+// FunctionButton 实现
+FunctionButton::FunctionButton(const QIcon& icon, const QString& text, QWidget* parent)
+    : QPushButton(parent)
+    , m_icon(icon)
+    , m_text(text)
+{
+    setFixedSize(100, 70); // 固定大小
+    setCursor(Qt::PointingHandCursor);
+}
+
+void FunctionButton::paintEvent(QPaintEvent* event)
+{
+    Q_UNUSED(event);
+
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    // 绘制按钮背景
+    QRect rect = this->rect();
+    QColor bgColor = Colors::Primary;
+
+    if (isDown()) {
+        bgColor = Colors::PrimaryPressed;
+    } else if (underMouse()) {
+        bgColor = Colors::PrimaryHover;
+    }
+
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(bgColor);
+    painter.drawRoundedRect(rect, 5, 5);
+
+    // 绘制图标
+    if (!m_icon.isNull()) {
+        int iconSize = 24;
+        int iconLeft = (width() - iconSize) / 2;
+        int iconTop = 10;
+        QRect iconRect(iconLeft, iconTop, iconSize, iconSize);
+        m_icon.paint(&painter, iconRect);
+    }
+
+    // 绘制文本
+    painter.setPen(Qt::white);
+    QFont font = Fonts::buttonFont();
+    painter.setFont(font);
+
+    QFontMetrics metrics(font);
+    int textWidth = metrics.horizontalAdvance(m_text);
+    int textLeft = (width() - textWidth) / 2;
+    int textTop = 45; // 文本在图标下方
+    painter.drawText(QRect(textLeft, textTop, textWidth, 20), Qt::AlignCenter, m_text);
+}
+
+void FunctionButton::enterEvent(QEvent* event)
+{
+    QPushButton::enterEvent(event);
+    update();
+}
+
+void FunctionButton::leaveEvent(QEvent* event)
+{
+    QPushButton::leaveEvent(event);
+    update();
+}
+
+// PopoverButtonGroup 实现
+PopoverButtonGroup::PopoverButtonGroup(Qt::Orientation orientation, QWidget* parent)
+    : QWidget(parent)
+{
+    if (orientation == Qt::Horizontal) {
+        QHBoxLayout* hLayout = new QHBoxLayout(this);
+        hLayout->setSpacing(15);
+        hLayout->setContentsMargins(0, 0, 0, 0);
+        hLayout->setAlignment(Qt::AlignLeft); // 左对齐,避免拉伸
+        layout = hLayout;
+    } else {
+        QVBoxLayout* vLayout = new QVBoxLayout(this);
+        vLayout->setSpacing(15);
+        vLayout->setContentsMargins(0, 0, 0, 0);
+        vLayout->setAlignment(Qt::AlignTop); // 顶部对齐,避免拉伸
+        layout = vLayout;
+    }
+}
+
+void PopoverButtonGroup::addButton(FunctionButton* button, Popover* popover)
+{
+    // 创建容器布局来组合功能按钮和Popover按钮
+    QWidget* container = new QWidget;
+    QHBoxLayout* containerLayout = new QHBoxLayout(container);
+    containerLayout->setContentsMargins(0, 0, 0, 0);
+    containerLayout->setSpacing(0);
+
+    // 添加功能按钮
+    containerLayout->addWidget(button);
+    buttons.append(button);
+
+    if (popover) {
+        // 创建Popover触发按钮
+        QPushButton* popoverBtn = createPopoverTriggerButton();
+        containerLayout->addWidget(popoverBtn);
+
+        // 存储关联关系
+        popoverMap[popoverBtn] = popover;
+        buttonPopoverMap[button] = popoverBtn;
+
+        // 连接Popover按钮点击事件
+        connect(popoverBtn, &QPushButton::clicked, this, [this, popoverBtn]() {
+            togglePopover(popoverBtn);
+        });
+    }
+
+    // 添加容器到主布局
+    layout->addWidget(container);
+}
+
+void PopoverButtonGroup::addPopoverToButton(FunctionButton* button, Popover* popover)
+{
+    if (buttons.contains(button) && !buttonPopoverMap.contains(button)) {
+        // 找到按钮所在的容器
+        QWidget* container = button->parentWidget();
+        if (!container)
+            return;
+
+        // 创建Popover触发按钮
+        QPushButton* popoverBtn = createPopoverTriggerButton();
+        QHBoxLayout* containerLayout = qobject_cast<QHBoxLayout*>(container->layout());
+        if (containerLayout) {
+            containerLayout->addWidget(popoverBtn);
+
+            // 存储关联关系
+            popoverMap[popoverBtn] = popover;
+            buttonPopoverMap[button] = popoverBtn;
+
+            // 连接Popover按钮点击事件
+            connect(popoverBtn, &QPushButton::clicked, this, [this, popoverBtn]() {
+                togglePopover(popoverBtn);
+            });
+        }
+    }
+}
+
+QPushButton* PopoverButtonGroup::createPopoverTriggerButton()
+{
+    auto btn = new PopoverTriggerButton(this);
+
+    // 绘制下拉箭头(垂直居中)
+    QPixmap pixmap(16, 16);
+    pixmap.fill(Qt::transparent);
+    QPainter painter(&pixmap);
+    painter.setRenderHint(QPainter::Antialiasing);
+    painter.setPen(QPen(Colors::Primary, 2));
+
+    QPolygon arrow;
+    arrow << QPoint(4, 6) << QPoint(8, 10) << QPoint(12, 6);
+    painter.drawPolyline(arrow);
+
+    btn->setIcon(QIcon(pixmap));
+    btn->setIconSize(QSize(16, 16));
+
+    return btn;
+}
+
+void PopoverButtonGroup::togglePopover(QPushButton* popoverBtn)
+{
+    if (!popoverMap.contains(popoverBtn))
+        return;
+
+    Popover* popover = popoverMap[popoverBtn];
+
+    if (popover->isVisible()) {
+        popover->hideAnimated();
+    } else {
+        // 关闭其他Popover
+        for (auto btn : popoverMap.keys()) {
+            if (btn != popoverBtn && popoverMap[btn]->isVisible()) {
+                popoverMap[btn]->hideAnimated();
+            }
+        }
+
+        // 显示当前Popover
+        QPoint pos = popoverBtn->mapToGlobal(QPoint(0, popoverBtn->height()));
+        popover->showAnimated(pos);
+    }
+}
+
+QSize PopoverButtonGroup::sizeHint() const
+{
+    if (buttons.isEmpty()) {
+        return QSize(0, 0);
+    }
+
+    // 每个FunctionButton的固定大小
+    const int buttonWidth = 100;
+    const int buttonHeight = 70;
+    const int spacing = 15; // 与构造函数中设置的spacing保持一致
+    
+    // 计算Popover触发按钮的宽度(如果有的话)
+    const int popoverBtnWidth = 20; // createPopoverTriggerButton中设置的宽度
+    
+    int totalWidth = 0;
+    int maxHeight = buttonHeight;
+    
+    if (qobject_cast<QHBoxLayout*>(layout)) {
+        // 水平布局:累加宽度
+        for (int i = 0; i < buttons.size(); ++i) {
+            totalWidth += buttonWidth;
+            
+            // 检查是否有对应的Popover按钮
+            FunctionButton* btn = buttons[i];
+            if (buttonPopoverMap.contains(btn)) {
+                totalWidth += popoverBtnWidth; // 添加Popover按钮宽度
+            }
+            
+            if (i > 0) totalWidth += spacing;
+        }
+    } else {
+        // 垂直布局:累加高度,取最大宽度
+        totalWidth = buttonWidth + popoverBtnWidth; // 最大可能宽度
+        maxHeight = 0;
+        for (int i = 0; i < buttons.size(); ++i) {
+            maxHeight += buttonHeight;
+            if (i > 0) maxHeight += spacing;
+        }
+    }
+
+    return QSize(totalWidth, maxHeight);
+}

+ 111 - 0
widgets/functionbutton.h

@@ -0,0 +1,111 @@
+#ifndef BUTTON_COMPONENTS_H
+#define BUTTON_COMPONENTS_H
+
+#include <QFontMetrics>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QLayout>
+#include <QList>
+#include <QMap>
+#include <QPaintEvent>
+#include <QPainter>
+#include <QPixmap>
+#include <QPolygon>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QWidget>
+
+class Popover : public QWidget
+{
+    Q_OBJECT
+    Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)
+public:
+    explicit Popover(QWidget* parent = nullptr);
+    ~Popover();
+
+    void setContentWidget(QWidget* widget);
+    void showAnimated(const QPoint& position);
+    void hideAnimated();
+
+    qreal opacity() const { return m_opacity; }
+    void setOpacity(qreal opacity);
+
+    // 计算阴影边距
+    QMargins shadowMargins() const;
+
+signals:
+    void popoverClosed();
+
+protected:
+    void closeEvent(QCloseEvent* event) override;
+    void paintEvent(QPaintEvent* event) override;
+    QSize sizeHint() const override;
+    bool eventFilter(QObject* obj, QEvent* event) override;
+
+private:
+    QVBoxLayout* mainLayout;
+    qreal m_opacity = 1.0;
+};
+
+// 自定义Popover触发按钮,确保固定大小不被全局QSS覆盖
+class PopoverTriggerButton : public QPushButton
+{
+    Q_OBJECT
+public:
+    explicit PopoverTriggerButton(QWidget* parent = nullptr);
+    
+protected:
+    QSize sizeHint() const override;
+    QSize minimumSizeHint() const override;
+    void resizeEvent(QResizeEvent* event) override;
+};
+
+// 自定义功能按钮(图标在上,文本在下)
+class FunctionButton : public QPushButton
+{
+    Q_OBJECT
+public:
+    FunctionButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr);
+
+protected:
+    void paintEvent(QPaintEvent* event) override;
+    void enterEvent(QEvent* event) override;
+    void leaveEvent(QEvent* event) override;
+
+private:
+    QIcon m_icon;
+    QString m_text;
+};
+
+// 按钮组容器(管理功能按钮和Popover按钮)
+class PopoverButtonGroup : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit PopoverButtonGroup(Qt::Orientation orientation = Qt::Horizontal,
+                                QWidget* parent = nullptr);
+
+    // 添加一个按钮(可选择是否关联Popover)
+    void addButton(FunctionButton* button, Popover* popover = nullptr);
+
+    // 为已有按钮添加Popover
+    void addPopoverToButton(FunctionButton* button, Popover* popover);
+
+protected:
+    QSize sizeHint() const override;
+
+private:
+    // 创建Popover触发按钮
+    QPushButton* createPopoverTriggerButton();
+
+    // 切换Popover显示状态
+    void togglePopover(QPushButton* popoverBtn);
+
+private:
+    QList<FunctionButton*> buttons;
+    QMap<QPushButton*, Popover*> popoverMap;              // Popover按钮到Popover的映射
+    QMap<FunctionButton*, QPushButton*> buttonPopoverMap; // 功能按钮到Popover按钮的映射
+    QLayout* layout;
+};
+
+#endif // FUNCTIONBUTTON_H