zhuizhu 7 mēneši atpakaļ
vecāks
revīzija
9fa8bda139

+ 581 - 586
MainPanel.cpp

@@ -1,8 +1,18 @@
 #include "MainPanel.h"
 #include <QSplitter>
 #include <QVBoxLayout>
-
-
+#include <QIcon>
+#include <QPixmap>
+#include <QPainter>
+#include <QPolygon>
+#include <QSizePolicy>
+#include <QTimer>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QPen>
+#include <QResizeEvent>
+#include "network/websocketclient.h"
 #include <util/jsonmapper.h>
 
 #include <QDebug>
@@ -16,167 +26,141 @@
 #include "widgets/bubbletip.h"
 #include "widgets/chatView/chatwindow.h"
 
-#include "widgets/functionbutton.h"
-#include "widgets/maskoverlay.h"
-#include "widgets/statswidget.h"
-#include "widgets/userprofilewidget.h"
-
+#include <QComboBox>
+#include <QFrame>
+#include <QHBoxLayout>
+#include <QLabel>
 #include "AVPlayer/avplayerwidget.h"
 #include "api/roomapi.h"
 #include "appevent.h"
+#include "widgets/framelessbase.h"
+#include "widgets/functionbutton.h"
+#include "widgets/maskoverlay.h"
 #include "widgets/recorderwidget.h"
-#include <QComboBox>
-#include <QLabel>
-#include <QHBoxLayout>
+#include "widgets/statswidget.h"
+// 新增:设置面板
+#include "themesettingswidget.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);
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(60,60,60), 2));
+    p.drawEllipse(QPointF(12,12), 8, 8);
+    p.drawLine(QPointF(12,2), QPointF(12,6));
+    p.drawLine(QPointF(12,22), QPointF(12,18));
+    p.end();
+    return QIcon(pix);
 }
 
 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);
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(60,60,60), 2));
+    p.drawEllipse(QPointF(10,10), 6, 6);
+    p.drawLine(QPointF(14,14), QPointF(22,22));
+    p.end();
+    return QIcon(pix);
 }
 
 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);
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(60,60,60), 2));
+    p.drawEllipse(QPointF(12,9), 5, 5);
+    p.drawRoundedRect(QRectF(6,14,12,8), 4, 4);
+    p.end();
+    return QIcon(pix);
 }
 
 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);
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(60,60,60), 2));
+    p.drawRoundedRect(QRectF(5,6,14,12), 3, 3);
+    p.drawLine(QPointF(12,18), QPointF(12,22));
+    p.drawLine(QPointF(8,22), QPointF(16,22));
+    p.end();
+    return QIcon(pix);
 }
 
 QIcon createStreamIcon()
 {
-    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);
-    
-    // 播放按钮(三角形)
-    QPolygon triangle;
-    triangle << QPoint(6, 4) << QPoint(6, 20) << QPoint(18, 12);
-    painter.drawPolygon(triangle);
-    
-    // 信号波纹
-    painter.setBrush(Qt::NoBrush);
-    painter.drawArc(16, 8, 4, 8, 0, 180 * 16);
-    painter.drawArc(18, 6, 4, 12, 0, 180 * 16);
-    painter.drawArc(20, 4, 4, 16, 0, 180 * 16);
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(250,80,60), 2));
+    p.setBrush(QColor(250,80,60));
+    QPolygon tri; tri << QPoint(8,6) << QPoint(20,12) << QPoint(8,18);
+    p.drawPolygon(tri);
+    p.end();
+    return QIcon(pix);
+}
 
-    return QIcon(icon);
+QIcon createChatIcon()
+{
+    QPixmap pix(24, 24);
+    pix.fill(Qt::transparent);
+    QPainter p(&pix);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setPen(QPen(QColor(60,60,60), 2));
+    p.setBrush(Qt::NoBrush);
+    p.drawRoundedRect(QRectF(4,5,16,12), 3, 3);
+    QPolygon tail; tail << QPoint(12,17) << QPoint(10,21) << QPoint(15,18);
+    p.drawPolygon(tail);
+    p.end();
+    return QIcon(pix);
 }
 } // namespace IconUtils
 
 MainPanel::MainPanel(QWidget *parent)
     : QWidget(parent)
-    , userProfile(nullptr)
     , chatView(nullptr)
 {
-    // 初始化防抖定时器
+    setAttribute(Qt::WA_StyledBackground, true);
+    setStyleSheet("background: #f7f7f7;");
+
     m_debounceTimer = new QTimer(this);
+    m_debounceTimer->setInterval(500);
     m_debounceTimer->setSingleShot(true);
-    m_debounceTimer->setInterval(500); // 500ms防抖延迟
     connect(m_debounceTimer, &QTimer::timeout, this, &MainPanel::handleDebouncedPlay);
-    // setupUI
-    userProfile = new UserProfileWidget(this);
+
     webSocketClient = new WebSocketClient(this);
+    
+    // 启用WebSocket自动重连,提升聊天室稳定性
+    if (webSocketClient) {
+        webSocketClient->setAutoReconnect(true);
+    }
+    
     chatView = new ChatWindow(webSocketClient);
     chatView->setMinimumWidth(400);
+    // 防御:明确不随关闭销毁,避免父窗口关闭时误删
+    chatView->setAttribute(Qt::WA_DeleteOnClose, false);
     // 连接聊天窗口关闭请求信号
     connect(chatView, &ChatWindow::windowCloseRequested, this, &MainPanel::onChatWindowCloseRequested);
-    statsWidget = new StatsWidget(this);
+    // statsWidget = new StatsWidget(this); // 暂时移除统计在此处的展示
 
-    QWidget *rightWidget = new QWidget;
-    QVBoxLayout *vbox = new QVBoxLayout(rightWidget);
+    m_rightWidget = new QWidget;
+    QVBoxLayout *vbox = new QVBoxLayout(m_rightWidget);
     vbox->setContentsMargins(0, 0, 0, 0);
-    vbox->addWidget(userProfile, 0);
-    vbox->addWidget(statsWidget, 0);
+    // vbox->addWidget(userProfile, 0);
+    // vbox->addWidget(statsWidget, 0); // 暂时移除
     
     // 创建聊天窗口容器
-    m_chatContainer = new QWidget(rightWidget);
+    m_chatContainer = new QWidget(m_rightWidget);
     QVBoxLayout *chatLayout = new QVBoxLayout(m_chatContainer);
     chatLayout->setContentsMargins(0, 0, 0, 0);
     chatLayout->addWidget(chatView);
@@ -185,7 +169,7 @@ MainPanel::MainPanel(QWidget *parent)
     splitter = new QSplitter(Qt::Horizontal, this);
     playerContainer = new QWidget(this);
     splitter->addWidget(playerContainer);
-    splitter->addWidget(rightWidget);
+    splitter->addWidget(m_rightWidget);
     splitter->setStretchFactor(0, 60);
     splitter->setStretchFactor(1, 30);
     QVBoxLayout *mainLayout = new QVBoxLayout(this);
@@ -202,18 +186,33 @@ MainPanel::MainPanel(QWidget *parent)
 
     // 添加功能按钮
     FunctionButton *settingsBtn = new FunctionButton(IconUtils::createSettingsIcon(), "设置", this);
-    Popover *settingsPopover = new Popover(this);
-    // settingsPopover->setContentWidget(settingsContent);
-    buttonGroup->addButton(settingsBtn, settingsPopover);
+    // 移除 Popover 箭头,改为直接打开设置窗口
+    buttonGroup->addButton(settingsBtn, nullptr);
+    connect(settingsBtn, &QPushButton::clicked, this, &MainPanel::onSettingsButtonClicked);
 
-    FunctionButton *searchBtn = new FunctionButton(IconUtils::createSearchIcon(), "搜索", this);
-    Popover *searchPopover = new Popover(this);
-    // searchPopover->setContentWidget(searchContent);
-    buttonGroup->addButton(searchBtn, searchPopover);
+    // 移除:搜索按钮(暂不使用)
+    // FunctionButton *searchBtn = new FunctionButton(IconUtils::createSearchIcon(), "搜索", this);
+    // Popover *searchPopover = new Popover(this);
+    // buttonGroup->addButton(searchBtn, searchPopover);
 
     FunctionButton *userBtn = new FunctionButton(IconUtils::createUserIcon(), "用户", this);
     Popover *userPopover = new Popover(this);
-    // userPopover->setContentWidget(userContent);
+    // 构建用户Popover内容:提供“退出”操作
+    {
+        QWidget *userContent = new QWidget(userPopover);
+        QVBoxLayout *userLayout = new QVBoxLayout(userContent);
+        userLayout->setContentsMargins(8, 8, 8, 8);
+        userLayout->setSpacing(8);
+
+        QPushButton *logoutBtn = new QPushButton(tr("退出"), userContent);
+        logoutBtn->setMinimumWidth(120);
+        connect(logoutBtn, &QPushButton::clicked, this, [this]() {
+            emit logoutClicked();
+        });
+        userLayout->addWidget(logoutBtn);
+
+        userPopover->setContentWidget(userContent);
+    }
     buttonGroup->addButton(userBtn, userPopover);
 
     // 添加音频设备选择按钮
@@ -239,222 +238,37 @@ MainPanel::MainPanel(QWidget *parent)
     audioLayout->addWidget(m_micWidget);
 
     // 扬声器区域
-    QLabel *spkTitle = new QLabel(tr("扬声器"), audioContent);
-    spkTitle->setStyleSheet("font-weight:600; margin-top:4px;");
+    QLabel *speakerTitle = new QLabel(tr("扬声器"), audioContent);
+    speakerTitle->setStyleSheet("font-weight:600;");
     m_speakerWidget = new QComboBox(audioContent);
     m_speakerWidget->setEditable(false);
-    audioLayout->addWidget(spkTitle);
+    audioLayout->addWidget(speakerTitle);
     audioLayout->addWidget(m_speakerWidget);
 
-    // 新增:编码器区域
-    QLabel *encTitle = new QLabel(tr("视频编码器"), audioContent);
-    encTitle->setStyleSheet("font-weight:600; margin-top:4px;");
+    // 视频编码器区域
+    QLabel *encoderTitle = new QLabel(tr("视频编码器"), audioContent);
+    encoderTitle->setStyleSheet("font-weight:600;");
     m_encoderWidget = new QComboBox(audioContent);
     m_encoderWidget->setEditable(false);
-    audioLayout->addWidget(encTitle);
+    audioLayout->addWidget(encoderTitle);
     audioLayout->addWidget(m_encoderWidget);
 
-    // 新增:录制控制区域
-    QLabel *controlTitle = new QLabel(tr("录制控制"), audioContent);
-    controlTitle->setStyleSheet("font-weight:600; margin-top:8px;");
-    audioLayout->addWidget(controlTitle);
-
-    // 选项区域
-    m_drawCursorCheckBox = new QCheckBox(tr("绘制鼠标指针"), audioContent);
-    m_drawCursorCheckBox->setChecked(true);
-    m_syncRecordCheckBox = new QCheckBox(tr("推流时同步录制"), audioContent);
-    audioLayout->addWidget(m_drawCursorCheckBox);
-    audioLayout->addWidget(m_syncRecordCheckBox);
-
-    // 按钮区域
-    QHBoxLayout *buttonLayout = new QHBoxLayout();
-    m_recordButton = new QPushButton(tr("开始录制"), audioContent);
-    m_settingsButton = new QPushButton(tr("设置"), audioContent);
-    
-    buttonLayout->addWidget(m_recordButton);
-    buttonLayout->addWidget(m_settingsButton);
-    audioLayout->addLayout(buttonLayout);
-
-    // 创建独立的推流按钮
-    m_streamButton = new FunctionButton(IconUtils::createStreamIcon(), "推流", this);
-
     audioDevicePopover->setContentWidget(audioContent);
-
-    // 将按钮与 Popover 关联
     buttonGroup->addButton(audioDeviceBtn, audioDevicePopover);
 
-    // 添加独立的推流按钮到buttonGroup
+    // 新增:独立的推流按钮(无弹层)
+    m_streamButton = new FunctionButton(IconUtils::createStreamIcon(), tr("推流"), this);
     buttonGroup->addButton(m_streamButton);
-    // 设备枚举与填充逻辑
-    auto populateAudioDevices = [this]() {
-        // 清空缓存
-        m_micDevices.clear();
-        m_speakerDevices.clear();
-
-        // 获取麦克风列表
-        AMRECORDER_DEVICE *micArray = nullptr;
-        int micCount = recorder_get_mics(&micArray);
-        QStringList micNames;
-        int defaultMicIndex = -1;
-        for (int i = 0; i < micCount; ++i) {
-            m_micDevices.push_back(micArray[i]);
-            micNames << QString::fromUtf8(micArray[i].name);
-            if (micArray[i].is_default && defaultMicIndex < 0) defaultMicIndex = i;
-        }
-        if (micArray) recorder_free_array(micArray);
-
-        // 获取扬声器列表
-        AMRECORDER_DEVICE *spkArray = nullptr;
-        int spkCount = recorder_get_speakers(&spkArray);
-        QStringList spkNames;
-        int defaultSpkIndex = -1;
-        for (int i = 0; i < spkCount; ++i) {
-            m_speakerDevices.push_back(spkArray[i]);
-            spkNames << QString::fromUtf8(spkArray[i].name);
-            if (spkArray[i].is_default && defaultSpkIndex < 0) defaultSpkIndex = i;
-        }
-        if (spkArray) recorder_free_array(spkArray);
 
-        // 更新 UI 列表
-        if (m_micWidget) {
-            m_micWidget->clear();
-            m_micWidget->addItems(micNames);
-        }
-        if (m_speakerWidget) {
-            m_speakerWidget->clear();
-            m_speakerWidget->addItems(spkNames);
-        }
-
-        // 恢复或设置默认选择
-        if (m_micWidget) {
-            if (defaultMicIndex >= 0 && defaultMicIndex < m_micDevices.size()) {
-                m_micWidget->setCurrentText(QString::fromUtf8(m_micDevices[defaultMicIndex].name));
-            } else if (!micNames.isEmpty()) {
-                m_micWidget->setCurrentIndex(0);
-            }
-        }
-        if (m_speakerWidget) {
-            if (defaultSpkIndex >= 0 && defaultSpkIndex < m_speakerDevices.size()) {
-                m_speakerWidget->setCurrentText(
-                    QString::fromUtf8(m_speakerDevices[defaultSpkIndex].name));
-            } else if (!spkNames.isEmpty()) {
-                m_speakerWidget->setCurrentIndex(0);
-            }
-        }
+    // 新增:聊天按钮(推流:显示/隐藏;非推流:弹出/嵌入)
+    m_chatButton = new FunctionButton(IconUtils::createChatIcon(), tr("聊天"), this);
+    buttonGroup->addButton(m_chatButton);
 
-        // 将当前选择传递给 RecorderWidget(若存在)
-        if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-            // 选中麦克风
-            if (m_micWidget) {
-                const QString sel = m_micWidget->currentText();
-                for (const auto &d : m_micDevices) {
-                    if (sel == QString::fromUtf8(d.name)) { rec->setMicDevice(d); break; }
-                }
-            }
-            // 选中扬声器
-            if (m_speakerWidget) {
-                const QString sel = m_speakerWidget->currentText();
-                for (const auto &d : m_speakerDevices) {
-                    if (sel == QString::fromUtf8(d.name)) { rec->setSpeakerDevice(d); break; }
-                }
-            }
-        }
-    };
-
-    // 新增:编码器枚举与填充逻辑
-    auto populateEncoders = [this]() {
-        m_encoderList.clear();
-        m_selectedEncoderId = -1;
-        AMRECORDER_ENCODERS *encArray = nullptr;
-        int encCount = recorder_get_vencoders(&encArray);
-        QStringList encNames;
-        for (int i = 0; i < encCount; ++i) {
-            m_encoderList.push_back(encArray[i]);
-            encNames << QString::fromUtf8(encArray[i].name);
-        }
-        if (encArray) recorder_free_array(encArray);
-
-        if (m_encoderWidget) {
-            m_encoderWidget->clear();
-            m_encoderWidget->addItems(encNames);
-            if (!m_encoderList.isEmpty()) {
-                m_encoderWidget->setCurrentIndex(0);
-                m_selectedEncoderId = m_encoderList[0].id;
-            }
-        }
-
-        if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-            if (m_selectedEncoderId >= 0) rec->setVideoEncoderId(m_selectedEncoderId);
-        }
-    };
-
-    // 首次填充设备和编码器
-    populateAudioDevices();
-    populateEncoders();
-
-    // 连接设备选择变化 -> 传给 RecorderWidget
-
-    connect(m_micWidget, &QComboBox::currentTextChanged, this, [this](const QString &deviceName) {
-        if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-            for (const auto &d : m_micDevices) {
-                if (deviceName == QString::fromUtf8(d.name)) {
-                    rec->setMicDevice(d);
-                    break;
-                }
-            }
-        }
-    });
-
-    connect(m_speakerWidget, &QComboBox::currentTextChanged, this, [this](const QString &deviceName) {
-        if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-            for (const auto &d : m_speakerDevices) {
-                if (deviceName == QString::fromUtf8(d.name)) {
-                    rec->setSpeakerDevice(d);
-                    break;
-                }
-            }
-        }
-    });
-
-    // 新增:编码器选择变化 -> 传给 RecorderWidget
-    connect(m_encoderWidget, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
-        if (index >= 0 && index < m_encoderList.size()) {
-            m_selectedEncoderId = m_encoderList[index].id;
-            if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-                rec->setVideoEncoderId(m_selectedEncoderId);
-            }
-        }
-    });
-
-    // 新增:录制控制按钮信号连接
-    connect(m_recordButton, &QPushButton::clicked, this, &MainPanel::onRecordButtonClicked);
-    connect(m_streamButton, &QPushButton::clicked, this, &MainPanel::onStreamButtonClicked);
-    connect(m_settingsButton, &QPushButton::clicked, this, &MainPanel::onSettingsButtonClicked);
-
-    // 维持原有的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);
+    // 移除:执行操作按钮(暂不使用)
+    // FunctionButton *actionButton = new FunctionButton(IconUtils::createSettingsIcon(),
+    //                                                   "执行操作",
+    //                                                   this);
+    // buttonGroup->addButton(actionButton, nullptr);
     
     // 将buttonGroup添加到playerContainer的布局中
     playerLayout = qobject_cast<QVBoxLayout*>(playerContainer->layout());
@@ -467,13 +281,8 @@ MainPanel::MainPanel(QWidget *parent)
     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);
+    // 已移除与 UserProfileWidget 相关的在线状态更新
+    // connect(webSocketClient, &WebSocketClient::statsUpdate, statsWidget, &StatsWidget::updateStats); // 暂时移除统计在主面板的更新
     connect(webSocketClient, &WebSocketClient::liveStatus, this, [this](const QString &msg) {
         // 这里可以处理 liveStatus 相关逻辑
         QJsonParseError err;
@@ -493,351 +302,537 @@ MainPanel::MainPanel(QWidget *parent)
                 m_pendingRoomId = id;
                 m_debounceTimer->start(); // 重新开始计时,如果在500ms内再次收到请求,会重置定时器
             }
-
-            // 你的处理逻辑
-        } else if (liveStatus == 2) {
-            qDebug() << "[MainPanel] liveStatus: 未开播";
-            // 你的处理逻辑
-        } else {
-            qDebug() << "[MainPanel] liveStatus: 未知状态" << liveStatus;
         }
     });
+
+    // 连接按钮事件
+    connect(m_streamButton, &QPushButton::clicked, this, &MainPanel::onStreamButtonClicked);
+    connect(m_chatButton, &QPushButton::clicked, this, &MainPanel::onChatButtonClicked);
+
+    // 初始化音频设备列表
+    initAudioDeviceSelectors();
 }
 
 MainPanel::~MainPanel()
 {
-    if (userProfile) {
-        delete userProfile;
-        userProfile = nullptr;
+    if (m_avPlayerStandalone) {
+        m_avPlayerStandalone->deleteLater();
+        m_avPlayerStandalone = nullptr;
     }
     if (m_recorderStandalone) {
         m_recorderStandalone->deleteLater();
         m_recorderStandalone = nullptr;
     }
-    if (m_avPlayerStandalone) {
-        m_avPlayerStandalone->deleteLater();
-        m_avPlayerStandalone = nullptr;
-    }
 }
 void MainPanel::setRole(const QStringList &roleList)
 {
-    QWidget *newPlayer = nullptr;
-    if (roleList.contains("role.admin")) {
-        newPlayer = new RecorderWidget(this);
+    bool isRec = roleList.contains("role.admin") || roleList.contains("role.recorder") || roleList.contains("录制");
+    if (isRec) {
+        auto rec = new RecorderWidget(this);
+        setPlayerWidget(rec);
     } else {
-        newPlayer = new AVPlayerWidget(this);
+        auto av = new AVPlayerWidget(this);
+        setPlayerWidget(av);
     }
-    setPlayerWidget(newPlayer);
 
-    // 设置初始化信息
-    const QString &name = AppEvent::instance()->userName();
-    userProfile->setUsername(name);
+    if (m_streamButton) {
+        m_streamButton->setVisible(isRec);
+        m_streamButton->setText(tr("推流"));
+    }
 }
 
 void MainPanel::setPushRoomId(const QString &id)
 {
-    // 推流配置
-    if (RecorderWidget *recorderWidget = qobject_cast<RecorderWidget *>(playerWidget)) {
-        RecorderWidget::Settings settings;
-        settings.liveUrl = "rtmp://106.55.186.74:1935/stream/V1";
-        settings.liveName = id.toStdString();
-        recorderWidget->setSettings(settings);
-    }
-
-    // 重新进入房间
-    chatView->initWebsocket(id);
+    if (!webSocketClient) return;
 
-    if (AVPlayerWidget *playWidget = qobject_cast<AVPlayerWidget *>(playerWidget)) {
-        MaskOverlay::instance()->show(nullptr, 0, MaskOverlay::ActiveWindow);
-
-        QFuture<HttpResponse> getRoomFuture = getRoomApi(id);
-        QtPromise::QPromise<HttpResponse> roomListPromise = QtPromise::resolve(getRoomFuture);
-
-        roomListPromise
-            .then([this, playWidget, id](const HttpResponse &response) {
-                qDebug() << response.code << response.data << response.message;
-                if (response.code != 0) {
-                    BubbleTip::showTip(this, response.message, BubbleTip::Top, 3000);
-                    return;
-                }
-                RoomInfo roomInfo = JsonMapper::formJsonEx<RoomInfo>(response.data.toObject());
-
-                qDebug() << "roomInfo.liveStatus.has_value()" << roomInfo.liveStatus.has_value();
-
-                int status = roomInfo.liveStatus.value_or(0);
-                if (status == 1) {
-                    qDebug() << "open" << ("rtmp://106.55.186.74:1935/stream/V1/" + id);
-
-                    playWidget->play("rtmp://106.55.186.74:1935/stream/V1/" + id);
-                }
-            })
-            .finally([]() { MaskOverlay::instance()->hide(); });
+    // 初始化聊天室连接
+    webSocketClient->connectToRoom(id);
+    if (chatView) {
+        chatView->initWebsocket(id);
     }
+
+    // 若当前是播放器,使用防抖播放
+    m_pendingRoomId = id;
+    m_debounceTimer->start();
 }
+
 void MainPanel::setPlayerWidget(QWidget *newPlayer)
 {
+    if (!newPlayer) return;
+    if (!playerContainer) return;
+
     if (playerWidget) {
-        playerWidget->setParent(nullptr);
         playerWidget->deleteLater();
+        playerWidget = nullptr;
     }
+
     playerWidget = newPlayer;
-    playerWidget->setParent(playerContainer);
-    
-    // 获取现有布局并清理(保留buttonGroup)
-    QVBoxLayout *vbox = qobject_cast<QVBoxLayout*>(playerContainer->layout());
-    if (vbox) {
-        // 清理除了buttonGroup之外的所有项目
-        QLayoutItem *item;
-        while (vbox->count() > 0) {
-            item = vbox->takeAt(0);
-            if (item->widget() && item->widget() != buttonGroup) {
-                item->widget()->setParent(nullptr);
-            }
-            if (item->spacerItem()) {
-                delete item;  // 删除spacer
-            } else if (item->widget() != buttonGroup) {
-                delete item;
-            }
-        }
+
+    // 将新播放器添加到容器布局
+    if (auto layout = qobject_cast<QVBoxLayout*>(playerContainer->layout())) {
+        layout->insertWidget(0, playerWidget, 1);
     } else {
-        // 如果没有布局或布局类型不对,创建新的
-        if (playerContainer->layout()) {
-            delete playerContainer->layout();
-        }
-        vbox = new QVBoxLayout(playerContainer);
-        vbox->setContentsMargins(0, 0, 0, 0);
+        auto layout2 = new QVBoxLayout(playerContainer);
+        layout2->setContentsMargins(0, 0, 0, 0);
+        layout2->setSpacing(0);
+        layout2->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);  // 使用固定大小策略
+    // 更新推流按钮可见性
+    if (m_streamButton) {
+        const bool isRec = qobject_cast<RecorderWidget*>(playerWidget) != nullptr;
+        m_streamButton->setVisible(isRec);
+        if (!isRec) {
+            m_isStreaming = false;
+            m_streamButton->setText(tr("推流"));
+        }
+    }
 
-    // 如果新的 player 是 RecorderWidget,则同步当前选择的音频设备
-    if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-        if (m_micWidget) {
-            const QString sel = m_micWidget->currentText();
-            for (const auto &d : m_micDevices) {
-                if (sel == QString::fromUtf8(d.name)) { rec->setMicDevice(d); break; }
+    // 连接 RecorderWidget 的推流信号,保持 UI 状态同步
+    if (auto rec = qobject_cast<RecorderWidget*>(playerWidget)) {
+        connect(rec, &RecorderWidget::streamingStarted, this, [this, rec]() {
+            m_isStreaming = true;
+            if (m_streamButton) m_streamButton->setText(tr("停止推流"));
+            // 进入极简模式
+            if (QWidget *tlw = window()) {
+                tlw->setUpdatesEnabled(false);
             }
-        }
-        if (m_speakerWidget) {
-            const QString sel = m_speakerWidget->currentText();
-            for (const auto &d : m_speakerDevices) {
-                if (sel == QString::fromUtf8(d.name)) { rec->setSpeakerDevice(d); break; }
+            rec->hidePreview();
+            if (chatView) chatView->hide();
+            if (m_rightWidget) m_rightWidget->hide();
+            if (splitter) splitter->hide();
+
+            // 使用自定义标题的紧凑浮窗承载工具栏
+            if (!m_compactFrame) {
+                m_compactFrame = new TMainWindow();
+                m_compactFrame->setWindowTitle(tr("共享控制"));
+                m_compactFrame->setWindowFlag(Qt::Tool, true);
+                // m_compactFrame->setAttribute(Qt::WA_DeleteOnClose);
+
+                if (buttonGroup) {
+                    if (auto layout = qobject_cast<QVBoxLayout*>(playerContainer->layout())) {
+                        layout->removeWidget(buttonGroup);
+                    }
+                    auto container = new QWidget(m_compactFrame);
+                    auto lay = new QHBoxLayout(container);
+                    lay->setContentsMargins(12, 8, 12, 8);
+                    lay->setSpacing(10);
+                    buttonGroup->setParent(container);
+                    lay->addWidget(buttonGroup);
+                    m_compactFrame->setCentralWidget(container);
+                    // 使用 sizeHint 直接设置初始尺寸,避免反复 adjustSize 带来的重算
+                    const QSize hint = buttonGroup->sizeHint();
+                    int w = qMax(360, hint.width() + 24);
+                    int h = qMax(80, hint.height() + 16);
+                    int titleH = 0;
+                    if (auto mw = m_compactFrame->menuWidget()) {
+                        mw->adjustSize();
+                        titleH = mw->sizeHint().height();
+                    }
+                    m_compactFrame->setMinimumSize(w, h + titleH);
+                    m_compactFrame->resize(w, h + titleH);
+                }
             }
-        }
-        // 同步编码器
-        if (m_selectedEncoderId >= 0) {
-            rec->setVideoEncoderId(m_selectedEncoderId);
-        } else if (m_encoderWidget && m_encoderWidget->count() > 0 && !m_encoderList.isEmpty()) {
-            int idx = m_encoderWidget->currentIndex();
-            if (idx >= 0 && idx < m_encoderList.size()) {
-                rec->setVideoEncoderId(m_encoderList[idx].id);
+            m_compactFrame->show();
+            m_compactFrame->raise();
+            m_compactMode = true;
+
+            // 主窗口保持原几何但隐藏
+            if (QWidget *tlw = window()) {
+                m_savedWindowGeometry = tlw->geometry();
+                tlw->hide();
             }
-        }
+        });
+        connect(rec, &RecorderWidget::streamingStopped, this, [this, rec]() {
+            m_isStreaming = false;
+            if (m_streamButton) m_streamButton->setText(tr("推流"));
+            // 退出极简模式
+            if (QWidget *tlw = window()) {
+                tlw->setUpdatesEnabled(false);
+            }
+            rec->showPreview();
+            if (splitter) splitter->show();
+            if (m_rightWidget) m_rightWidget->show();
+
+            // 销毁紧凑浮窗并将 buttonGroup 归还
+            if (m_compactFrame) {
+                if (buttonGroup) {
+                    buttonGroup->hide();
+                    buttonGroup->setParent(playerContainer);
+                    if (auto layout = qobject_cast<QVBoxLayout*>(playerContainer->layout())) {
+                        layout->addWidget(buttonGroup, 0);
+                    }
+                    buttonGroup->show();
+                }
+                m_compactFrame->close();
+                m_compactFrame->deleteLater();
+                m_compactFrame = nullptr;
+            }
+            m_compactMode = false;
+
+            // 恢复主窗口显示
+            if (QWidget *tlw = window()) {
+                tlw->setGeometry(m_savedWindowGeometry);
+                tlw->show();
+                tlw->raise();
+                tlw->setUpdatesEnabled(true);
+            }
+
+            showChatEmbedded();
+            if (m_chatButton) m_chatButton->setText(tr("聊天"));
+        });
     }
 }
 
-
-
 void MainPanel::handleDebouncedPlay()
 {
-    // 防抖处理后的播放逻辑
-    if (m_pendingRoomId.isEmpty() || !chatView) {
+    // 在定时器触发时执行播放逻辑
+    const QString id = m_pendingRoomId;
+    if (id.isEmpty()) return;
+    qDebug() << "[MainPanel] Debounced startPlay for room" << id;
+
+    AVPlayerWidget *av = qobject_cast<AVPlayerWidget*>(playerWidget);
+    if (av) {
+        av->stopPlay();
+        av->setPlayRoomId(id);
+        av->startPlay();
         return;
     }
 
-    if (AVPlayerWidget *playWidget = qobject_cast<AVPlayerWidget *>(playerWidget)) {
-        if (!m_isStartingPlay) {
-            m_isStartingPlay = true;
-            qDebug() << "[MainPanel] 防抖处理后开始播放:" << m_pendingRoomId;
-
-            playWidget->play("rtmp://106.55.186.74:1935/stream/V1/" + m_pendingRoomId);
-            m_isStartingPlay = false; // 如果 startToPlay 是同步的
-        }
+    RecorderWidget *rec = qobject_cast<RecorderWidget*>(playerWidget);
+    if (rec) {
+        // rec->startLive();
+        // -        RecorderWidget::Settings s = rec->m_settings; // 需要通过公共接口设置,避免直接访问成员
     }
-
-    // 清空待处理的房间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] 音频设备选择器初始化完成";
+    // TODO: 枚举音频设备、填充 m_micWidget 和 m_speakerWidget;枚举编码器填充 m_encoderWidget
 }
 
 void MainPanel::showRecorderStandalone()
 {
-    if (!m_recorderStandalone) {
-        m_recorderStandalone = new RecorderWidget(nullptr);
-        m_recorderStandalone->setAttribute(Qt::WA_DeleteOnClose, false);
-        connect(m_recorderStandalone, &QObject::destroyed, this, [this]() {
-            m_recorderStandalone = nullptr;
-        });
-        m_recorderStandalone->setWindowTitle(tr("RecorderWidget"));
+    if (m_recorderFrame) {
+        m_recorderFrame->raise();
+        m_recorderFrame->activateWindow();
+        return;
     }
-    m_recorderStandalone->show();
-    m_recorderStandalone->raise();
-    m_recorderStandalone->activateWindow();
+    m_recorderFrame = new TMainWindow();
+    m_recorderFrame->setAttribute(Qt::WA_DeleteOnClose);
+    m_recorderFrame->setWindowTitle(tr("录制器"));
+
+    m_recorderStandalone = new RecorderWidget(m_recorderFrame);
+    m_recorderFrame->setCentralWidget(m_recorderStandalone);
+    m_recorderFrame->resize(960, 600);
+    m_recorderFrame->show();
+
+    connect(m_recorderFrame, &QObject::destroyed, this, [this]() {
+        m_recorderFrame = nullptr;
+        m_recorderStandalone = nullptr;
+    });
 }
 
 void MainPanel::showPlayerStandalone()
 {
-    if (!m_avPlayerStandalone) {
-        m_avPlayerStandalone = new AVPlayerWidget(nullptr);
-        m_avPlayerStandalone->setAttribute(Qt::WA_DeleteOnClose, false);
-        connect(m_avPlayerStandalone, &QObject::destroyed, this, [this]() {
-            m_avPlayerStandalone = nullptr;
-        });
-        m_avPlayerStandalone->setWindowTitle(tr("AVPlayerWidget"));
+    if (m_playerFrame) {
+        m_playerFrame->raise();
+        m_playerFrame->activateWindow();
+        return;
     }
-    m_avPlayerStandalone->show();
-    m_avPlayerStandalone->raise();
-    m_avPlayerStandalone->activateWindow();
+    m_playerFrame = new TMainWindow();
+    m_playerFrame->setAttribute(Qt::WA_DeleteOnClose);
+    m_playerFrame->setWindowTitle(tr("播放器"));
+
+    m_avPlayerStandalone = new AVPlayerWidget(m_playerFrame);
+    m_playerFrame->setCentralWidget(m_avPlayerStandalone);
+    m_playerFrame->resize(960, 600);
+    m_playerFrame->show();
+
+    connect(m_playerFrame, &QObject::destroyed, this, [this]() {
+        m_playerFrame = nullptr;
+        m_avPlayerStandalone = nullptr;
+    });
 }
 
 void MainPanel::showChatStandalone()
 {
-    if (!chatView) {
-        return; // 如果聊天窗口不存在,直接返回
+    if (!chatView) return;
+    if (m_isStreaming) return; // 推流时保持仅浮窗
+
+    // -    if (m_chatFrame) {
+    // -        m_chatFrame->raise();
+    // -        m_chatFrame->activateWindow();
+    // -        return;
+    // -    }
+    if (m_chatFrame) {
+        // 确保 chatView 已正确作为中央部件挂载
+        if (m_chatFrame->centralWidget() != chatView && chatView) {
+            chatView->hide();
+            if (chatView->parent() != m_chatFrame) {
+                chatView->setParent(m_chatFrame);
+            }
+            m_chatFrame->setCentralWidget(chatView);
+            chatView->show();
+        }
+        if (!m_chatFrame->isVisible())
+            m_chatFrame->show();
+        m_chatFrame->raise();
+        m_chatFrame->activateWindow();
+        return;
     }
-    
-    // 从容器中移除聊天窗口(如果在容器中)
-    if (m_chatContainer && chatView->parent() == m_chatContainer) {
-        chatView->setParent(nullptr);
+
+    // 从嵌入容器移除
+    if (m_chatContainer) {
+        if (auto l = qobject_cast<QVBoxLayout*>(m_chatContainer->layout())) {
+            l->removeWidget(chatView);
+        }
     }
-    
-    // 设置为独立窗口
-    chatView->setWindowFlags(Qt::Window);
-    chatView->setAttribute(Qt::WA_DeleteOnClose, false);
-    chatView->setMinimumWidth(400);
-    chatView->setWindowTitle(tr("ChatWindow"));
-    chatView->show();
-    chatView->raise();
-    chatView->activateWindow();
+
+    m_chatFrame = new TMainWindow();
+    m_chatFrame->setAttribute(Qt::WA_DeleteOnClose);
+    m_chatFrame->setWindowTitle(tr("聊天"));
+    m_chatFrame->installEventFilter(this);
+
+    chatView->setParent(m_chatFrame);
+    m_chatFrame->setCentralWidget(chatView);
+    m_chatFrame->resize(380, 540);
+    m_chatFrame->show();
+
+    // -    connect(m_chatFrame, &QObject::destroyed, this, [this]() {
+    // -        m_chatFrame = nullptr;
+    // -        onChatWindowCloseRequested();
+    // -    });
+    connect(m_chatFrame, &QObject::destroyed, this, [this]() { m_chatFrame = nullptr; });
 }
 
 void MainPanel::showChatEmbedded()
 {
-    if (!chatView) {
-        return; // 如果聊天窗口不存在,直接返回
-    }
-    
-    // 如果当前是独立窗口模式,先隐藏
-    if (chatView->isWindow()) {
+    if (!chatView || !m_chatContainer) return;
+
+    // 如存在独立窗口包装,先将 chatView 移回容器再关闭窗口,避免被父窗口销毁
+    if (m_chatFrame) {
+        // 断开临时回调,避免 destroyed 中的副作用
+        disconnect(m_chatFrame, nullptr, this, nullptr);
+        if (chatView->parent() == m_chatFrame) {
+            // 使用 takeCentralWidget 而非 setCentralWidget(nullptr) 避免误删中央部件
+            if (m_chatFrame->centralWidget()) {
+                QWidget *w = m_chatFrame->takeCentralWidget();
+                Q_UNUSED(w);
+            }
+        }
+        chatView->hide();
+        chatView->setParent(m_chatContainer);
+        chatView->setWindowTitle(QString());
+        if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
+            if (l->indexOf(chatView) < 0)
+                l->addWidget(chatView);
+
+        } else {
+            auto l2 = new QVBoxLayout(m_chatContainer);
+            l2->setContentsMargins(0, 0, 0, 0);
+            l2->addWidget(chatView);
+        }
+        chatView->show();
+        // 现在安全地关闭独立窗口
+        m_chatFrame->close();
+        m_chatFrame = nullptr;
+
+    } else {
+        // 无独立窗口,仅确保嵌入
         chatView->hide();
-    }
-    
-    // 重置窗口标志为普通widget
-    chatView->setWindowFlags(Qt::Widget);
-    
-    // 将聊天窗口重新添加到容器中
-    if (m_chatContainer) {
         chatView->setParent(m_chatContainer);
-        // 如果容器有布局,将聊天窗口添加到布局中
-        if (m_chatContainer->layout()) {
-            m_chatContainer->layout()->addWidget(chatView);
+        chatView->setWindowTitle(QString());
+        if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
+            if (l->indexOf(chatView) < 0)
+                l->addWidget(chatView);
+        } else {
+            auto l2 = new QVBoxLayout(m_chatContainer);
+            l2->setContentsMargins(0, 0, 0, 0);
+            l2->addWidget(chatView);
         }
         chatView->show();
-        chatView->raise();
     }
 }
 
-// 新增:录制控制按钮的槽函数实现
 void MainPanel::onRecordButtonClicked()
 {
-    if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-        // 同步选项设置到RecorderWidget
-        // 注意:这里需要RecorderWidget提供设置选项的接口
-        
-        // 调用RecorderWidget的录制方法
-        rec->onRecordButtonClicked();
-        
-        // 更新按钮文本
-        if (m_recordButton) {
-            if (m_recordButton->text() == tr("开始录制")) {
-                m_recordButton->setText(tr("停止录制"));
-            } else {
-                m_recordButton->setText(tr("开始录制"));
-            }
+    // TODO: 录制控制逻辑
+}
+
+void MainPanel::onStreamButtonClicked()
+{
+    RecorderWidget *rec = qobject_cast<RecorderWidget*>(playerWidget);
+    if (!rec) {
+        qDebug() << "[MainPanel] 当前不处于录制器模式,无法推流";
+        return;
+    }
+    if (!m_isStreaming) {
+        rec->startStreaming(); // 成功与否由信号驱动 UI
+    } else {
+        rec->stopStreaming();
+    }
+}
+
+void MainPanel::onSettingsButtonClicked()
+{
+    // 使用 TMainWindow 包裹设置页面,保持无边框一致风格
+    if (m_settingsFrame) {
+        if (m_settingsFrame->isVisible()) {
+            m_settingsFrame->raise();
+            m_settingsFrame->activateWindow();
+        } else {
+            m_settingsFrame->show();
+            m_settingsFrame->raise();
+            m_settingsFrame->activateWindow();
         }
+        return;
     }
+
+    m_settingsFrame = new TMainWindow();
+    m_settingsFrame->setAttribute(Qt::WA_DeleteOnClose);
+    m_settingsFrame->setWindowTitle(tr("设置"));
+    // 当设置窗口关闭时,清空指针
+    connect(m_settingsFrame, &QObject::destroyed, this, [this]() { m_settingsFrame = nullptr; });
+
+    auto *settingsWidget = new ThemeSettingsWidget(m_settingsFrame);
+    m_settingsFrame->setCentralWidget(settingsWidget);
+    m_settingsFrame->resize(560, 460);
+    m_settingsFrame->show();
 }
 
-void MainPanel::onStreamButtonClicked()
+void MainPanel::onChatWindowCloseRequested()
 {
-    if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-        // 同步选项设置到RecorderWidget
-
-        rec->onStreamButtonClicked();
-
-        // 更新按钮文本和状态
-        if (m_streamButton) {
-            if (!m_isStreaming) {
-                // 开始推流
-                m_streamButton->setText(tr("停止推流"));
-                m_isStreaming = true;
-                
-                // 隐藏预览
-                rec->hidePreview();
-                
-                // 显示独立聊天窗口
-                showChatStandalone();
+    // 如果处在推流状态,认为是“隐藏聊天”而不是关闭程序
+    if (m_isStreaming) {
+        if (m_chatButton) m_chatButton->setText(tr("显示聊天"));
+        return; // ChatWindow本身会隐藏
+    }
+
+    // 非推流:回归嵌入显示
+    showChatEmbedded();
+    if (m_chatButton) m_chatButton->setText(tr("聊天"));
+}
 
+void MainPanel::onChatButtonClicked()
+{
+    if (!chatView) return;
+
+    if (m_isStreaming) {
+        // 推流时:只在独立聊天窗口上 显示/隐藏 切换,避免频繁 reparent
+        if (!m_chatFrame) {
+            // 如果当前在嵌入容器里,先从布局移除
+            if (m_chatContainer) {
+                if (auto l = qobject_cast<QVBoxLayout*>(m_chatContainer->layout())) {
+                    l->removeWidget(chatView);
+                }
+            }
+            m_chatFrame = new TMainWindow();
+            m_chatFrame->setAttribute(Qt::WA_DeleteOnClose);
+            m_chatFrame->setWindowTitle(tr("聊天"));
+            m_chatFrame->installEventFilter(this);
+
+            chatView->setParent(m_chatFrame);
+            m_chatFrame->setCentralWidget(chatView);
+            m_chatFrame->resize(380, 540);
+            m_chatFrame->show();
+            connect(m_chatFrame, &QObject::destroyed, this, [this]() { m_chatFrame = nullptr; });
+            if (m_chatButton) m_chatButton->setText(tr("隐藏聊天"));
+            return; // 结束推流分支处理
+        } else {
+            // 已有独立窗口:切换显示/隐藏
+            if (chatView->parent() != m_chatFrame) {
+                chatView->setParent(m_chatFrame);
+                if (m_chatFrame->centralWidget() != chatView)
+                    m_chatFrame->setCentralWidget(chatView);
+            }
+            if (m_chatFrame->isVisible()) {
+                m_chatFrame->hide();
+                if (m_chatButton) m_chatButton->setText(tr("显示聊天"));
             } else {
-                // 停止推流
-                m_streamButton->setText(tr("推流"));
-                m_isStreaming = false;
-                
-                // 恢复预览显示
-                rec->showPreview();
+                m_chatFrame->show();
+                m_chatFrame->raise();
+                m_chatFrame->activateWindow();
+                if (m_chatButton) m_chatButton->setText(tr("隐藏聊天"));
             }
+            return; // 结束推流分支处理
         }
     }
+
+    // 非推流:在弹出与嵌入间切换
+    if (m_chatFrame) {
+        // 已经是弹出状态,切回嵌入
+        showChatEmbedded();
+        if (m_chatButton)
+            m_chatButton->setText(tr("聊天"));
+    } else {
+        // 目前为嵌入状态,弹出为独立窗口
+        showChatStandalone();
+        if (m_chatButton)
+            m_chatButton->setText(tr("嵌入聊天"));
+    }
 }
 
+// ===== 浮动工具栏 =====
+void MainPanel::showFloatingToolbar()
+{
+    // 改为使用 m_compactFrame 承载,无需再在主窗口中悬浮
+    if (!m_compactFrame)
+        return;
+    m_compactFrame->show();
+    m_compactFrame->raise();
+}
 
+void MainPanel::hideFloatingToolbar()
+{
+    if (m_compactFrame)
+        m_compactFrame->hide();
+}
 
-void MainPanel::onSettingsButtonClicked()
+void MainPanel::resizeEvent(QResizeEvent* event)
 {
-    if (RecorderWidget *rec = qobject_cast<RecorderWidget *>(playerWidget)) {
-        // 调用RecorderWidget的设置方法
-        rec->onSettingsButtonClicked();
-    }
+    QWidget::resizeEvent(event);
+    // 主窗口 Resize 不再需要移动悬浮条
 }
 
-void MainPanel::onChatWindowCloseRequested()
+bool MainPanel::eventFilter(QObject *watched, QEvent *event)
 {
-    if (!chatView) {
-        return;
-    }
-    
-    if (m_isStreaming) {
-        // 推流状态下,只隐藏窗口
-        chatView->hide();
-    } else {
-        // 非推流状态下,切换回嵌入式显示
-        showChatEmbedded();
+    // 拦截聊天独立窗口的关闭事件
+    if (watched == m_chatFrame && event->type() == QEvent::Close) {
+        // 推流期间:关闭操作改为隐藏窗口,避免销毁与频繁 reparent 导致异常
+        if (m_isStreaming) {
+            event->ignore();
+            if (m_chatFrame) {
+                m_chatFrame->hide();
+            }
+            if (m_chatButton)
+                m_chatButton->setText(tr("显示聊天"));
+            return true; // 事件已处理,不再继续关闭
+        }
+
+        // 非推流:允许关闭,但先把 chatView 放回嵌入容器,避免被父窗口销毁
+        if (chatView && m_chatContainer && chatView->parent() == m_chatFrame) {
+            // 取走中央部件,避免 setCentralWidget(nullptr) 触发潜在删除
+            if (m_chatFrame->centralWidget()) {
+                QWidget *w = m_chatFrame->takeCentralWidget();
+                Q_UNUSED(w);
+            }
+            chatView->hide();
+            chatView->setParent(m_chatContainer);
+            chatView->setWindowTitle(QString());
+            if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
+                if (l->indexOf(chatView) < 0)
+                    l->addWidget(chatView);
+            } else {
+                auto l2 = new QVBoxLayout(m_chatContainer);
+                l2->setContentsMargins(0, 0, 0, 0);
+                l2->addWidget(chatView);
+            }
+            chatView->show();
+            if (m_chatButton)
+                m_chatButton->setText(tr("聊天"));
+        }
+        return false; // 继续关闭流程
     }
+    return QWidget::eventFilter(watched, event);
 }

+ 35 - 4
MainPanel.h

@@ -4,6 +4,7 @@
 #include <QTimer>
 #include <QWaitCondition>
 #include <QWidget>
+#include <QRect>
 #include "qobjectdefs.h"
 
 #include <QVector>
@@ -13,10 +14,13 @@
 
 class QSplitter;
 
-class UserProfileWidget;
+// 移除:UserProfileWidget 前向声明(不再使用)
 class ChatWindow;
 class RecorderWidget; // forward declaration for standalone recorder
 class AVPlayerWidget; // forward declaration for standalone player
+class PopoverButtonGroup; // 前向声明:在本类中以指针形式使用
+class FunctionButton; // 新增:用于指针成员 m_streamButton 的前向声明
+class TMainWindow;    // 新增:用于无边框自定义标题窗口承载弹窗/悬浮工具栏
 // Removed RecorderAudioWidget forward declaration; use QComboBox instead
 class QComboBox;
 
@@ -57,17 +61,32 @@ private:
 
     void handleDebouncedPlay(); // 防抖处理函数
 
+    // 新增:推流时浮动工具栏控制
+    void showFloatingToolbar();
+    void hideFloatingToolbar();
+
+protected:
+    void resizeEvent(QResizeEvent* event) override;
+    bool eventFilter(QObject *watched, QEvent *event) override;
+
 private:
     QSplitter *splitter = nullptr;
     QWidget *playerContainer = nullptr;
     QWidget *playerWidget = nullptr;
-    UserProfileWidget *userProfile = nullptr;
-    ChatWindow *chatView = nullptr;           // 统一的聊天窗口实例
+    QWidget *m_rightWidget = nullptr;          // 推流时整体隐藏的右侧面板(含聊天容器)
+    // 移除:UserProfileWidget *userProfile = nullptr;
+    ChatWindow *chatView = nullptr;            // 统一的聊天窗口实例
     // 新增:独立显示的窗口
     RecorderWidget *m_recorderStandalone = nullptr;
     AVPlayerWidget *m_avPlayerStandalone = nullptr;
     QWidget *m_chatContainer = nullptr;       // 聊天窗口的容器(用于嵌入式显示)
 
+    // 使用 TMainWindow 承载的独立窗口框架
+    class TMainWindow *m_recorderFrame = nullptr;
+    class TMainWindow *m_playerFrame = nullptr;
+    class TMainWindow *m_chatFrame = nullptr;
+    // 新增:设置窗口的无边框框架
+    class TMainWindow *m_settingsFrame = nullptr;
     WebSocketClient *webSocketClient = nullptr;
     StatsWidget *statsWidget = nullptr;
     bool m_isStartingPlay = false;
@@ -94,15 +113,26 @@ private:
     // 新增:录制控制按钮(从RecorderWidget移动过来)
     QPushButton *m_recordButton = nullptr;        // 开始录制按钮
     class FunctionButton *m_streamButton = nullptr;        // 开始推流按钮(独立FunctionButton)
+    class FunctionButton *m_chatButton = nullptr;          // 聊天显示/隐藏/弹出/嵌入切换按钮
     QPushButton *m_settingsButton = nullptr;      // 设置按钮
     QCheckBox *m_drawCursorCheckBox = nullptr;    // 绘制鼠标指针选项
     QCheckBox *m_syncRecordCheckBox = nullptr;    // 推流时同步录制选项
 
-    // DockManager 相关
+    // DockManager 附近
     ADS::DockManager *m_dockManager = nullptr;
     
     // 推流状态跟踪
     bool m_isStreaming = false;
+
+    // 极简模式与窗口几何缓存
+    bool m_compactMode = false;
+    QRect m_savedWindowGeometry;
+
+    // -    // 标记 buttonGroup 是否处于悬浮状态
+    // -    bool m_buttonGroupFloating = false;
+    // -
+    // 新增:用于承载“悬浮工具栏”的自定义标题窗口
+    TMainWindow *m_compactFrame = nullptr;
     
 private slots:
     void initAudioDeviceSelectors();
@@ -111,6 +141,7 @@ private slots:
     void onRecordButtonClicked();
     void onStreamButtonClicked();
     void onSettingsButtonClicked();
+    void onChatButtonClicked();                 // 聊天弹出/嵌入/显示/隐藏按钮槽
     
     // 聊天窗口关闭处理
     void onChatWindowCloseRequested();

+ 27 - 0
libs/AVPlayer/avplayerwidget.cpp

@@ -211,3 +211,30 @@ void AVPlayerWidget::terminateSlot()
 {
     stop();
 }
+
+// 兼容旧接口实现
+void AVPlayerWidget::stopPlay()
+{
+    stop();
+}
+
+void AVPlayerWidget::setPlayRoomId(const QString &id)
+{
+    // 以当前URL为基准,去掉最后一节后拼接房间ID,避免硬编码服务器地址
+    QString base = m_urlEdit->text().trimmed();
+    if (base.isEmpty()) {
+        base = "rtmp://127.0.0.1:1935/stream/V1/stream";
+    }
+    int lastSlash = base.lastIndexOf('/');
+    QString streamBase = lastSlash > 0 ? base.left(lastSlash) : base;
+    QString url = streamBase + "/" + id;
+    m_urlEdit->setText(url);
+}
+
+void AVPlayerWidget::startPlay()
+{
+    const QString url = m_urlEdit->text().trimmed();
+    if (!url.isEmpty()) {
+        play(url);
+    }
+}

+ 5 - 0
libs/AVPlayer/avplayerwidget.h

@@ -26,6 +26,11 @@ public:
     void pause();
     void resume();
     
+    // 兼容旧接口:基于房间ID控制播放
+    void stopPlay();
+    void setPlayRoomId(const QString &id);
+    void startPlay();
+    
 private Q_SLOTS:
     void ptsChangedSlot(unsigned int duration);
     void durationChangedSlot(unsigned int pts);

+ 86 - 115
mainwindow.cpp

@@ -25,13 +25,14 @@
 #include "widgets/chatView/chatwindow.h"
 #include "widgets/framelessbase.h"
 #include "widgets/maskoverlay.h"
-#include "widgets/userprofilewidget.h"
+// #include "widgets/userprofilewidget.h" // 未使用,移除
 #include "widgets/meetingselectionwidget.h"
 #include "widgets/joinmeetingdialog.h"
 #include "widgets/createmeetingdialog.h"
 
 #include <QDebug>
 #include <QFutureWatcher>
+#include <QJsonObject>
 
 #include <api/loginapi.h>
 
@@ -75,7 +76,7 @@ MainWindow::MainWindow(QWidget *parent)
     connect(meetingSelectionWidget, &MeetingSelectionWidget::createMeetingRequested, this, &MainWindow::onCreateMeetingRequested);
     connect(meetingSelectionWidget, &MeetingSelectionWidget::logoutRequested, this, &MainWindow::onMeetingSelectionLogout);
     
-    resize(1280, 600);
+    resize(1024, 576);
 
     // 显示登录页面
     stackedWidget->setCurrentWidget(loginWidget);
@@ -121,73 +122,73 @@ void MainWindow::onLoginSuccess(const QString &username, const QString &password
 
     loginApiPromise
         .then([this](const HttpResponse &response) {
-            qDebug() << response.code << response.data << response.message;
-            if (response.code != 0) {
-                BubbleTip::showTip(this, response.message, BubbleTip::Top, 3000);
-                return;
-            }
-            QString token;
-            if (response.data.isObject()) {
-                QJsonObject dataObj = response.data.toObject();
-                qDebug() << dataObj;
-                token = dataObj["token"].toString();
-                AppEvent::instance()->setJwtToken(token);
-                authLogin(); // 获取登录信息
-            }
-
-            if (!token.isEmpty()) {
-                qDebug() << "登录成功,获取到的令牌:" << token;
-                // 存储令牌
+            MaskOverlay::instance()->hide();
+            if (response.code == 0) {
+                // 登录成功后解析并保存JWT,确保后续请求携带Authorization
+                QString token;
+                if (response.data.isString()) {
+                    token = response.data.toString();
+                } else if (response.data.isObject()) {
+                    const QJsonObject obj = response.data.toObject();
+                    token = obj.value("accessToken").toString();
+                    if (token.isEmpty()) token = obj.value("token").toString();
+                    if (token.isEmpty()) token = obj.value("jwt").toString();
+                }
+                if (!token.isEmpty()) {
+                    AppEvent::instance()->setJwtToken(token);
+                }
+                // 显示会议选择页面(内部会调用获取用户信息)
+                authLogin();
+            } else {
+                BubbleTip::showTip(this,
+                                   QString("登录失败: %1").arg(response.message),
+                                   BubbleTip::Top,
+                                   3000,
+                                   BubbleTip::Type::Error);
             }
         })
-        .finally([]() { MaskOverlay::instance()->hide(); });
+        .fail([this](const std::exception &e) {
+            MaskOverlay::instance()->hide();
+            BubbleTip::showTip(this,
+                               QString("登录失败: %1").arg(e.what()),
+                               BubbleTip::Top,
+                               3000,
+                               BubbleTip::Type::Error);
+        });
 }
+
 void MainWindow::authLogin()
 {
-    // 创建两个异步操作的Promise
-    QFuture<HttpResponse> userInfoFuture = getUserInfoApi();
-    QFuture<HttpResponse> accessCodesFuture = AuthApi::getAccessCodesApi();
+    QFuture<HttpResponse> getUserInfoFuture = getUserInfoApi();
 
     // 使用fromQFuture将QFuture转换为QPromise
-    QtPromise::QPromise<HttpResponse> userInfoPromise = QtPromise::resolve(userInfoFuture);
-    QtPromise::QPromise<HttpResponse> accessCodesPromise = QtPromise::resolve(accessCodesFuture);
+    QtPromise::QPromise<HttpResponse> userInfoPromise = QtPromise::resolve(getUserInfoFuture);
 
-    // 使用QtPromise::all等待所有Promise完成
-    QtPromise::all(QVector<QtPromise::QPromise<HttpResponse>>{userInfoPromise, accessCodesPromise})
-        .then([this](const QVector<HttpResponse> &results) {
-            // 处理用户信息
-
-            // 用户信息: 200 QJsonValue(object, QJsonObject({"id":"e52e6b47-f4d3-43a6-b481-570ffcc1c9db","role":[],"username":"admin"})) "success"
-            // 访问代码: 203 QJsonValue(null) "Error transferring http://127.0.0.1:8888/api/auth/codes - server replied: Not Found"
-            const HttpResponse &userInfo = results[0];
-            qDebug() << "用户信息:" << userInfo.code << userInfo.data << userInfo.message;
-            UserInfo user = JsonMapper::fromJson<UserInfo>(userInfo.data);
-
-            // 处理访问代码
-            const HttpResponse &accessCodes = results[1];
-            qDebug() << "访问代码:" << accessCodes.code << accessCodes.data << accessCodes.message;
-
-            // 处理成功的逻辑
-            if (userInfo.code == 0 && accessCodes.code == 0) {
-                qDebug() << "所有请求成功完成";
-                // 这里可以添加后续处理逻辑
+    MaskOverlay::instance()->show(stackedWidget, 0, MaskOverlay::ActiveWindow);
+
+    userInfoPromise
+        .then([this](const HttpResponse &response) {
+            MaskOverlay::instance()->hide();
+            if (response.code == 0) {
+                UserInfo userInfo = JsonMapper::fromJson<UserInfo>(response.data);
+                AppEvent *appEvent = AppEvent::instance();
+                appEvent->setUserId(userInfo.id);
+                appEvent->setUserName(userInfo.username);
+                appEvent->setRoles(userInfo.roleName);
+                showMeetingSelection();
+            } else {
+                qDebug() << QString("获取用户信息失败: %1").arg(response.message);
+                BubbleTip::showTip(this,
+                                   QString("获取用户信息失败: %1").arg(response.message),
+                                   BubbleTip::Top,
+                                   3000,
+                                   BubbleTip::Type::Error);
             }
-            qDebug() << "当前角色:" << user.roleName;
-            qDebug() << "当前ID:" << user.id;
-            AppEvent::instance()->setRoles(user.roleName);
-            AppEvent::instance()->setUserId(user.id);
-            AppEvent::instance()->setUserName(user.username);
-
-            // 显示会议选择页面
-            showMeetingSelection();
         })
         .fail([this](const std::exception &e) {
-            // 处理错误
-            AppEvent::instance()->setRoles({});
-            AppEvent::instance()->setUserId({});
-            qWarning() << "登录后处理失败:" << e.what();
+            MaskOverlay::instance()->hide();
             BubbleTip::showTip(this,
-                               QString("请求失败: %1").arg(e.what()),
+                               QString("获取用户信息失败: %1").arg(e.what()),
                                BubbleTip::Top,
                                3000,
                                BubbleTip::Type::Error);
@@ -196,7 +197,6 @@ void MainWindow::authLogin()
 
 void MainWindow::showMeetingSelection()
 {
-    // 设置会议选择页面的用户信息
     AppEvent *appEvent = AppEvent::instance();
     meetingSelectionWidget->setUserRoles(appEvent->roles());
     meetingSelectionWidget->setUserInfo(appEvent->userName(), appEvent->userId());
@@ -251,18 +251,15 @@ void MainWindow::onJoinMeetingRequested()
             if (joinDialog->exec() == QDialog::Accepted) {
                 QString meetingId = joinDialog->getSelectedMeetingId();
                 
-                // 创建主界面部件(用于加入会议)
-                createMainWindow();
-
-                // 加入会议时,强制使用PlayWidget(观看者模式),无论用户角色如何
-                QStringList joinRoles = {"role.viewer"}; // 强制设置为观看者角色
-                mainWidget->setRole(joinRoles);
-                
-                // 显示主界面
-                stackedWidget->setCurrentWidget(mainWidget);
+                // 独立窗口加入会议(观看者模式)
+                MainPanel *panel = new MainPanel(nullptr);
+                panel->setAttribute(Qt::WA_DeleteOnClose);
+                panel->setRole({"role.viewer"});
+                connect(panel, &MainPanel::logoutClicked, panel, &QWidget::close);
+                panel->show();
                 
                 // 检查房间状态并验证会议ID是否存在
-                checkRoom(meetingId);
+                checkRoom(panel, meetingId);
             }
             
             joinDialog->deleteLater();
@@ -279,18 +276,15 @@ void MainWindow::onJoinMeetingRequested()
             if (joinDialog->exec() == QDialog::Accepted) {
                 QString meetingId = joinDialog->getSelectedMeetingId();
                 
-                // 创建主界面部件(用于加入会议)
-                createMainWindow();
-
-                // 加入会议时,强制使用PlayWidget(观看者模式),无论用户角色如何
-                QStringList joinRoles = {"role.viewer"}; // 强制设置为观看者角色
-                mainWidget->setRole(joinRoles);
-                
-                // 显示主界面
-                stackedWidget->setCurrentWidget(mainWidget);
+                // 独立窗口加入会议(观看者模式)
+                MainPanel *panel = new MainPanel(nullptr);
+                panel->setAttribute(Qt::WA_DeleteOnClose);
+                panel->setRole({"role.viewer"});
+                connect(panel, &MainPanel::logoutClicked, panel, &QWidget::close);
+                panel->show();
                 
                 // 检查房间状态并验证会议ID是否存在
-                checkRoom(meetingId);
+                checkRoom(panel, meetingId);
             }
             
             joinDialog->deleteLater();
@@ -312,11 +306,11 @@ void MainWindow::onCreateMeetingRequested()
         .then([this](const HttpResponse &response) {
             qDebug() << "获取会议列表:" << response.data << response.rawData << response.message;
 
+            QStringList meetingIds;
             if (response.code == 0) {
                 RoomListData roomListData(response.data.toObject());
                 QList<RoomInfo> roomInfos = roomListData.getRooms();
 
-                QStringList meetingIds;
                 QStringList meetingNames;
 
                 // 提取会议ID和名称
@@ -326,10 +320,6 @@ void MainWindow::onCreateMeetingRequested()
                         meetingNames.append(room.name.value());
                     }
                 }
-                if (meetingIds.size() > 0) {
-                    checkRoom(meetingIds[0]);
-                }
-
             } else {
                 qWarning() << "获取会议列表失败:" << response.message;
                 BubbleTip::showTip(this,
@@ -339,14 +329,16 @@ void MainWindow::onCreateMeetingRequested()
                                    BubbleTip::Type::Error);
             }
 
-            // 创建主界面部件(用于加入会议)
-            createMainWindow();
-
-            // 加入会议时,强制使用PlayWidget(观看者模式),无论用户角色如何
-            mainWidget->setRole({"role.admin"});
+            // 独立窗口创建/开启会议(管理员模式)
+            MainPanel *panel = new MainPanel(nullptr);
+            panel->setAttribute(Qt::WA_DeleteOnClose);
+            panel->setRole({"role.admin"});
+            connect(panel, &MainPanel::logoutClicked, panel, &QWidget::close);
+            panel->show();
 
-            // 显示主界面
-            stackedWidget->setCurrentWidget(mainWidget);
+            if (meetingIds.size() > 0) {
+                checkRoom(panel, meetingIds[0]);
+            }
         })
         .fail([this](const std::exception &e) {
             qWarning() << "获取会议列表异常:" << e.what();
@@ -372,7 +364,7 @@ void MainWindow::onMeetingSelectionLogout()
     stackedWidget->setCurrentWidget(loginWidget);
 }
 
-void MainWindow::checkRoom(const QString &meetingId)
+void MainWindow::checkRoom(MainPanel *target, const QString &meetingId)
 {
     // 创建两个异步操作的Promise
     RoomInfo roomInfo;
@@ -384,7 +376,7 @@ void MainWindow::checkRoom(const QString &meetingId)
 
     // 使用QtPromise::all等待所有Promise完成
     QtPromise::all(QVector<QtPromise::QPromise<HttpResponse>>{userInfoPromise})
-        .then([this, meetingId](const QVector<HttpResponse> &results) {
+        .then([this, target, meetingId](const QVector<HttpResponse> &results) {
             const HttpResponse &getRoomList = results[0];
             qDebug() << "访问房间:" << getRoomList.data << getRoomList.rawData
                      << getRoomList.message;
@@ -394,17 +386,6 @@ void MainWindow::checkRoom(const QString &meetingId)
             QList<RoomInfo> RoomInfos = roomListData.getRooms();
             qDebug() << RoomInfos.size();
 
-            // 填充房间列表
-            // for (int var = 0; var < RoomInfos.size(); ++var) {
-            //     const RoomInfo room = RoomInfos.at(var);
-            //     if (room.name) {
-            //         QListWidgetItem *item = new QListWidgetItem(room.name.value_or(""),
-            //                                                     mainWidget->roomListWidget());
-            //         item->setData(Qt::UserRole + 100, room.id.value_or(""));
-            //         mainWidget->roomListWidget()->addItem(item);
-            //     }
-            // }
-
             // 如果有待验证的会议ID,检查是否存在
             if (!meetingId.isEmpty()) {
                 bool meetingFound = false;
@@ -421,8 +402,7 @@ void MainWindow::checkRoom(const QString &meetingId)
                 
                 if (meetingFound) {
                     // 会议ID存在,设置为当前房间
-                    // mainWidget->roomListWidget()->setCurrentRow(foundIndex);
-                    mainWidget->setPushRoomId(meetingId);
+                    if (target) target->setPushRoomId(meetingId);
                     qDebug() << "成功加入会议:" << meetingId;
 
                     BubbleTip::showTip(this,
@@ -445,16 +425,7 @@ void MainWindow::checkRoom(const QString &meetingId)
                 }
             } else if (RoomInfos.size() > 0) {
                 // 没有待验证的会议ID,使用默认的第一个房间
-                // mainWidget->roomListWidget()->setCurrentRow(0);
-                mainWidget->setPushRoomId(RoomInfos[0].id.value_or(""));
-                // mainWidget->chatView->initWebsocket(RoomInfos[0].id.value());
-
-                // if (AvRecorder *avRecorder = qobject_cast<AvRecorder *>(mainWidget->playerWidget)) {
-                //     SettingsPage::Param param;
-                //     param.liveUrl = "rtmp://192.168.3.76:1935/stream/V1";
-                //     param.liveName = RoomInfos[0].id.value_or("").toStdString();
-                //     avRecorder->setSettings(param);
-                // }
+                if (target) target->setPushRoomId(RoomInfos[0].id.value_or(""));
             }
         })
         .fail([this](const std::exception &e) {

+ 1 - 1
mainwindow.h

@@ -24,7 +24,7 @@ public:
 private:
     void onLoginSuccess(const QString &username, const QString &password);
     void authLogin();
-    void checkRoom(const QString &meetingId = QString());
+    void checkRoom(MainPanel *target, const QString &meetingId = QString());
     void showMeetingSelection();
     void onJoinMeetingRequested();
     void onCreateMeetingRequested();

+ 1 - 1
widgets/functionbutton.h

@@ -97,7 +97,7 @@ public:
     // 移除按钮
     void removeButton(FunctionButton* button);
 
-protected:
+    // 公开 sizeHint,供外部布局/尺寸计算使用
     QSize sizeHint() const override;
 
 private:

+ 45 - 3
widgets/meetingselectionwidget.cpp

@@ -1,5 +1,8 @@
 #include "meetingselectionwidget.h"
+#include "userprofilewidget.h"
+#include "statswidget.h"
 #include "../appevent.h"
+#include "network/websocketclient.h"
 #include <QApplication>
 #include <QScreen>
 
@@ -39,7 +42,13 @@ void MeetingSelectionWidget::setupUI()
     m_welcomeLabel = new QLabel("欢迎使用!请选择您要进行的操作:", this);
     m_welcomeLabel->setAlignment(Qt::AlignCenter);
     m_welcomeLabel->setObjectName("welcomeLabel");
-    
+
+    // 用户资料卡片(复用现有组件)
+    m_userProfile = new UserProfileWidget(this);
+
+    // 消息统计(临时迁移到此)
+    m_statsWidget = new StatsWidget(this);
+
     // 按钮容器
     m_buttonFrame = new QFrame(this);
     m_buttonFrame->setObjectName("buttonFrame");
@@ -75,6 +84,8 @@ void MeetingSelectionWidget::setupUI()
     m_mainLayout->addStretch();
     m_mainLayout->addWidget(m_titleLabel);
     m_mainLayout->addWidget(m_welcomeLabel);
+    m_mainLayout->addWidget(m_userProfile, 0, Qt::AlignCenter);
+    m_mainLayout->addWidget(m_statsWidget, 0, Qt::AlignCenter); // 新增:消息统计
     m_mainLayout->addStretch();
     m_mainLayout->addWidget(m_buttonFrame, 0, Qt::AlignCenter);
     m_mainLayout->addStretch();
@@ -86,7 +97,15 @@ void MeetingSelectionWidget::setupUI()
 void MeetingSelectionWidget::setUserRoles(const QStringList &roles)
 {
     m_userRoles = roles;
-    m_isAdmin = roles.contains("role.admin");
+    m_isAdmin = m_userRoles.contains("role.admin");
+    updateButtonsVisibility();
+}
+
+void MeetingSelectionWidget::setUserRoles(const QString &roles)
+{
+    m_userRoles = roles.split(',', Qt::SkipEmptyParts);
+    for (QString &r : m_userRoles) r = r.trimmed();
+    m_isAdmin = m_userRoles.contains("role.admin");
     updateButtonsVisibility();
 }
 
@@ -99,6 +118,29 @@ void MeetingSelectionWidget::setUserInfo(const QString &username, const QString
     if (!username.isEmpty()) {
         m_welcomeLabel->setText(QString("欢迎,%1!请选择您要进行的操作:").arg(username));
     }
+    // 同步到用户资料组件
+    if (m_userProfile) {
+        m_userProfile->setUsername(m_username);
+        // 若后续有头像与状态,可在此设置:m_userProfile->setAvatar(...); m_userProfile->setStatus(...);
+    }
+}
+
+void MeetingSelectionWidget::setWebSocketClient(WebSocketClient *client)
+{
+    if (m_wsClient == client) return;
+
+    // 断开旧连接
+    if (m_wsClient && m_statsWidget) {
+        QObject::disconnect(m_wsClient, nullptr, m_statsWidget, nullptr);
+    }
+
+    m_wsClient = client;
+
+    // 建立新连接
+    if (m_wsClient && m_statsWidget) {
+        connect(m_wsClient, &WebSocketClient::statsUpdate,
+                m_statsWidget, &StatsWidget::updateStats);
+    }
 }
 
 void MeetingSelectionWidget::updateButtonsVisibility()
@@ -127,7 +169,7 @@ void MeetingSelectionWidget::applyStyles()
                   "#welcomeLabel {"
                   "    font-size: 16px;"
                   "    color: #7f8c8d;"
-                  "    margin-bottom: 20px;"
+                  "    margin-bottom: 10px;"
                   "}"
 
                   "#buttonFrame {"

+ 14 - 1
widgets/meetingselectionwidget.h

@@ -9,6 +9,10 @@
 #include <QFrame>
 #include <QStringList>
 
+class UserProfileWidget; // 前置声明:复用已有的用户资料组件
+class StatsWidget;       // 前置声明:消息统计组件
+class WebSocketClient;   // 前置声明:消息源
+
 class MeetingSelectionWidget : public QWidget
 {
     Q_OBJECT
@@ -17,12 +21,16 @@ public:
     explicit MeetingSelectionWidget(QWidget *parent = nullptr);
     ~MeetingSelectionWidget();
 
-    // 设置用户角色
+    // 设置用户角色(兼容两种输入)
     void setUserRoles(const QStringList &roles);
+    void setUserRoles(const QString &roles);
     
     // 设置用户信息
     void setUserInfo(const QString &username, const QString &userId);
 
+    // 临时:设置统计信息来源(来自 WebSocketClient 的 statsUpdate 信号)
+    void setWebSocketClient(WebSocketClient *client);
+
 signals:
     // 用户选择加入会议
     void joinMeetingRequested();
@@ -47,6 +55,8 @@ private:
     QVBoxLayout *m_mainLayout;
     QLabel *m_titleLabel;
     QLabel *m_welcomeLabel;
+    UserProfileWidget *m_userProfile; // 使用已有的用户资料组件
+    StatsWidget *m_statsWidget;       // 新增:消息统计组件
     QFrame *m_buttonFrame;
     QVBoxLayout *m_buttonLayout;
     QPushButton *m_joinMeetingBtn;
@@ -60,6 +70,9 @@ private:
     
     // 是否为管理员
     bool m_isAdmin;
+
+    // 统计信号来源
+    WebSocketClient *m_wsClient = nullptr;
 };
 
 #endif // MEETINGSELECTIONWIDGET_H

+ 51 - 46
widgets/recorderwidget.cpp

@@ -52,9 +52,9 @@ RecorderWidget::RecorderWidget(QWidget *parent)
     initUI();
     initRecorder();
     
-    // 创建定时器
-    m_previewTimer = new QTimer(this);
-    connect(m_previewTimer, &QTimer::timeout, this, &RecorderWidget::updatePreview);
+    // 创建定时器(预览已移除,不再创建预览定时器)
+    // m_previewTimer = new QTimer(this);
+    // connect(m_previewTimer, &QTimer::timeout, this, &RecorderWidget::updatePreview);
     
     m_statusTimer = new QTimer(this);
     connect(m_statusTimer, &QTimer::timeout, this, &RecorderWidget::updateStatus);
@@ -72,10 +72,10 @@ void RecorderWidget::initUI()
     m_mainLayout = new QVBoxLayout(this);
     m_mainLayout->setContentsMargins(0, 0, 0, 0);
     
-    // 预览区域
-    m_previewWidget = new QOpenGLWidget(this);
-    m_previewWidget->setMinimumSize(640, 480);
-    m_previewWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    // 预览区域已移除
+    // m_previewWidget = new QOpenGLWidget(this);
+    // m_previewWidget->setMinimumSize(640, 480);
+    // m_previewWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
     
     // 控制区域
     QWidget* controlWidget = new QWidget(this);
@@ -98,8 +98,8 @@ void RecorderWidget::initUI()
     // 状态栏
     initStatusBar();
     
-    // 添加到主布局
-    m_mainLayout->addWidget(m_previewWidget, 1);
+    // 添加到主布局(预览区域已移除)
+    // m_mainLayout->addWidget(m_previewWidget, 1);
     m_mainLayout->addWidget(controlWidget, 0);
     m_mainLayout->addWidget(m_statusBar, 0);
     
@@ -165,8 +165,9 @@ void RecorderWidget::setupCallbacks()
     m_callbacks.func_duration = onDurationCallback;
     m_callbacks.func_error = onErrorCallback;
     m_callbacks.func_device_change = onDeviceChangeCallback;
-    m_callbacks.func_preview_yuv = onPreviewYUVCallback;
-    m_callbacks.func_preview_audio = onPreviewAudioCallback;
+    // 预览已移除,不再设置视频/音频预览回调
+    m_callbacks.func_preview_yuv = nullptr;
+    m_callbacks.func_preview_audio = nullptr;
 }
 
 void RecorderWidget::refreshAudioDevices()
@@ -356,8 +357,8 @@ bool RecorderWidget::startRecording()
         
         m_isInitialized = true;
         
-        // 启用预览
-        recorder_set_preview_enabled(1);
+        // 关闭预览
+        recorder_set_preview_enabled(0);
         
         // 开始录制
         result = recorder_start();
@@ -379,8 +380,8 @@ bool RecorderWidget::startRecording()
             m_isRecording = true;
             m_recordStartTime = QTime::currentTime();
             
-            // 开始预览定时器
-            m_previewTimer->start(33); // 约30fps
+            // 预览已移除,不再启动预览定时器
+            // if (m_previewTimer) m_previewTimer->start(33);
             
             emit recordingStarted();
         }, Qt::QueuedConnection);
@@ -407,8 +408,8 @@ void RecorderWidget::stopRecording()
     
     m_isRecording = false;
     
-    // 停止预览定时器
-    m_previewTimer->stop();
+    // 预览已移除,不再停止预览定时器
+    // if (m_previewTimer) m_previewTimer->stop();
     
     emit recordingStopped();
 }
@@ -468,8 +469,8 @@ bool RecorderWidget::startStreaming()
         
         m_isInitialized = true;
         
-        // 启用预览
-        recorder_set_preview_enabled(1);
+        // 关闭预览
+        recorder_set_preview_enabled(0);
         
         // 开始推流
         result = recorder_start();
@@ -491,8 +492,8 @@ bool RecorderWidget::startStreaming()
             m_isStreaming = true;
             m_recordStartTime = QTime::currentTime();
             
-            // 开始预览定时器
-            m_previewTimer->start(33); // 约30fps
+            // 预览已移除,不再启动预览定时器
+            // if (m_previewTimer) m_previewTimer->start(33);
             
             emit streamingStarted();
         }, Qt::QueuedConnection);
@@ -506,39 +507,43 @@ bool RecorderWidget::startStreaming()
 
 void RecorderWidget::stopStreaming()
 {
-    if (!m_isStreaming) {
+    if (!m_isStreaming && !m_isInitialized) {
         return;
     }
-    
-    recorder_stop();
-    
-    if (m_isInitialized) {
-        recorder_release();
-        m_isInitialized = false;
+
+    if (m_statusLabel) {
+        m_statusLabel->setText("状态: 正在停止...");
     }
-    
-    m_isStreaming = false;
-    
-    // 停止预览定时器
-    m_previewTimer->stop();
-    
-    emit streamingStopped();
+
+    QtConcurrent::run([this]() {
+        // 停止推流并释放资源(与 startStreaming 对称)
+        recorder_stop();
+        if (m_isInitialized) {
+            recorder_release();
+        }
+
+        QMetaObject::invokeMethod(this, [this]() {
+            m_isStreaming = false;
+            m_isInitialized = false;
+            if (m_statusLabel)
+                m_statusLabel->setText("状态: 就绪");
+            emit streamingStopped();
+        }, Qt::QueuedConnection);
+    });
+}
+
+void RecorderWidget::setLiveName(const QString& name)
+{
+    m_settings.liveName = name.toStdString();
 }
 
-// 供MainPanel调用的按钮功能实现
+// 新增:供MainPanel调用的按钮功能
 void RecorderWidget::onRecordButtonClicked()
 {
-    if (!m_isRecording) {
-        if (startRecording()) {
-            if (m_statusLabel) {
-                m_statusLabel->setText("状态: 录制中");
-            }
-        }
-    } else {
+    if (m_isRecording) {
         stopRecording();
-        if (m_statusLabel) {
-            m_statusLabel->setText("状态: 就绪");
-        }
+    } else {
+        startRecording();
     }
 }
 

+ 4 - 1
widgets/recorderwidget.h

@@ -67,7 +67,10 @@ public:
     
     // 停止推流
     void stopStreaming();
-    
+
+    // 便捷方法:设置推流流名(用于以房间ID作为流名)
+    void setLiveName(const QString& name);
+
     // 新增:供MainPanel调用的按钮功能
     void onRecordButtonClicked();
     void onStreamButtonClicked();