zhuizhu 6 months ago
parent
commit
ab44cb5493
6 changed files with 672 additions and 63 deletions
  1. 2 0
      LearningSmartClient.pro
  2. 92 61
      MainPanel.cpp
  3. 12 1
      MainPanel.h
  4. 1 1
      main.cpp
  5. 463 0
      widgets/onlineuserswidget.cpp
  6. 102 0
      widgets/onlineuserswidget.h

+ 2 - 0
LearningSmartClient.pro

@@ -45,6 +45,7 @@ SOURCES += \
     widgets/chatView/chatwindow.cpp \
     widgets/chatView/independentchatwindow.cpp \
     widgets/colorlistwidget.cpp \
+    widgets/onlineuserswidget.cpp \
     widgets/createmeetingdialog.cpp \
     widgets/framelessbase.cpp \
     widgets/functionbutton.cpp \
@@ -90,6 +91,7 @@ HEADERS += \
     widgets/chatView/chatwindow.h \
     widgets/chatView/independentchatwindow.h \
     widgets/colorlistwidget.h \
+    widgets/onlineuserswidget.h \
     widgets/createmeetingdialog.h \
     widgets/framelessbase.h \
     widgets/functionbutton.h \

+ 92 - 61
MainPanel.cpp

@@ -46,7 +46,9 @@
 
 MainPanel::MainPanel(QWidget *parent)
     : TWidget(parent)
-    , chatView(nullptr)
+    , m_chatTabWidget(nullptr)
+    , m_chatWindow(nullptr)
+    , m_onlineUsersWidget(nullptr)
 {
     setAttribute(Qt::WA_StyledBackground, true);
 
@@ -86,12 +88,35 @@ void MainPanel::initChatComponents()
         webSocketClient->setAutoReconnect(true);
     }
     
-    chatView = new ChatWindow(webSocketClient);
-    chatView->setMinimumWidth(400);
+    // 创建聊天窗口
+    m_chatWindow = new ChatWindow(webSocketClient);
+    m_chatWindow->setMinimumWidth(400);
     // 防御:明确不随关闭销毁,避免父窗口关闭时误删
-    chatView->setAttribute(Qt::WA_DeleteOnClose, false);
+    m_chatWindow->setAttribute(Qt::WA_DeleteOnClose, false);
     // 连接聊天窗口关闭请求信号
-    connect(chatView, &ChatWindow::windowCloseRequested, this, &MainPanel::onChatWindowCloseRequested);
+    connect(m_chatWindow, &ChatWindow::windowCloseRequested, this, &MainPanel::onChatWindowCloseRequested);
+    
+    // 创建在线用户管理widget
+    m_onlineUsersWidget = new OnlineUsersWidget(this);
+    
+    // 连接在线用户widget的信号
+    connect(m_onlineUsersWidget, &OnlineUsersWidget::userDoubleClicked,
+            [this](const QString &userId, const QString &username) {
+                qDebug() << "用户双击:" << username << "ID:" << userId;
+                // TODO: 实现私聊功能
+            });
+    
+    connect(m_onlineUsersWidget, &OnlineUsersWidget::privateMessageRequested,
+            [this](const QString &userId, const QString &username) {
+                qDebug() << "请求私信:" << username << "ID:" << userId;
+                // TODO: 实现私信功能
+            });
+    
+    // 创建Tab容器
+    m_chatTabWidget = new QTabWidget(this);
+    m_chatTabWidget->addTab(m_chatWindow, "聊天");
+    m_chatTabWidget->addTab(m_onlineUsersWidget, "在线用户");
+    m_chatTabWidget->setMinimumWidth(400);
 }
 
 void MainPanel::initLayoutComponents()
@@ -105,7 +130,7 @@ void MainPanel::initLayoutComponents()
     m_chatContainer = new QWidget(m_rightWidget);
     QVBoxLayout *chatLayout = new QVBoxLayout(m_chatContainer);
     chatLayout->setContentsMargins(0, 0, 0, 0);
-    chatLayout->addWidget(chatView);
+    chatLayout->addWidget(m_chatTabWidget);
     vbox->addWidget(m_chatContainer, 1);
 
     // 创建主分割器
@@ -247,8 +272,8 @@ void MainPanel::connectSignals()
         QJsonObject obj = doc.object();
         int liveStatus = obj.value("liveStatus").toInt(0); // 默认-1
         if (liveStatus == 1) {
-            qDebug() << "[MainPanel] liveStatus: 直播中" << chatView;
-            if (chatView) {
+            qDebug() << "[MainPanel] liveStatus: 直播中" << m_chatWindow;
+            if (m_chatWindow) {
                 const QString id = webSocketClient->roomId();
 
                 // 使用防抖机制处理频繁的请求
@@ -296,8 +321,8 @@ void MainPanel::setPushRoomId(const QString &id)
 
     // 初始化聊天室连接
     webSocketClient->connectToRoom(id);
-    if (chatView) {
-        chatView->initWebsocket(id);
+    if (m_chatWindow) {
+        m_chatWindow->initWebsocket(id);
     }
 
     // 若当前是播放器,使用防抖播放
@@ -347,7 +372,7 @@ void MainPanel::setPlayerWidget(QWidget *newPlayer)
                 tlw->setUpdatesEnabled(false);
             }
             rec->hidePreview();
-            if (chatView) chatView->hide();
+            if (m_chatTabWidget) m_chatTabWidget->hide();
             if (m_rightWidget) m_rightWidget->hide();
             if (splitter) splitter->hide();
 
@@ -537,7 +562,13 @@ void MainPanel::showPlayerStandalone()
 
 void MainPanel::showChatStandalone()
 {
-    if (!chatView) return;
+    // 显示独立聊天窗口 - 调用 showChatInFrame 实现
+    showChatInFrame();
+}
+
+void MainPanel::showChatInFrame()
+{
+    if (!m_chatTabWidget) return;
     if (m_isStreaming) return; // 推流时保持仅浮窗
 
     // -    if (m_chatFrame) {
@@ -546,14 +577,14 @@ void MainPanel::showChatStandalone()
     // -        return;
     // -    }
     if (m_chatFrame) {
-        // 确保 chatView 已正确作为中央部件挂载
-        if (m_chatFrame->centralWidget() != chatView && chatView) {
-            chatView->hide();
-            if (chatView->parent() != m_chatFrame) {
-                chatView->setParent(m_chatFrame);
+        // 确保 m_chatTabWidget 已正确作为中央部件挂载
+        if (m_chatFrame->centralWidget() != m_chatTabWidget && m_chatTabWidget) {
+            m_chatTabWidget->hide();
+            if (m_chatTabWidget->parent() != m_chatFrame) {
+                m_chatTabWidget->setParent(m_chatFrame);
             }
-            m_chatFrame->setCentralWidget(chatView);
-            chatView->show();
+            m_chatFrame->setCentralWidget(m_chatTabWidget);
+            m_chatTabWidget->show();
         }
         if (!m_chatFrame->isVisible())
             m_chatFrame->show();
@@ -565,7 +596,7 @@ void MainPanel::showChatStandalone()
     // 从嵌入容器移除
     if (m_chatContainer) {
         if (auto l = qobject_cast<QVBoxLayout*>(m_chatContainer->layout())) {
-            l->removeWidget(chatView);
+            l->removeWidget(m_chatTabWidget);
         }
     }
 
@@ -574,8 +605,8 @@ void MainPanel::showChatStandalone()
     m_chatFrame->setWindowTitle(tr("聊天"));
     m_chatFrame->installEventFilter(this);
 
-    chatView->setParent(m_chatFrame);
-    m_chatFrame->setCentralWidget(chatView);
+    m_chatTabWidget->setParent(m_chatFrame);
+    m_chatFrame->setCentralWidget(m_chatTabWidget);
     m_chatFrame->resize(380, 540);
     m_chatFrame->show();
 
@@ -588,50 +619,50 @@ void MainPanel::showChatStandalone()
 
 void MainPanel::showChatEmbedded()
 {
-    if (!chatView || !m_chatContainer) return;
+    if (!m_chatTabWidget || !m_chatContainer) return;
 
-    // 如存在独立窗口包装,先将 chatView 移回容器再关闭窗口,避免被父窗口销毁
+    // 如存在独立窗口包装,先将 m_chatTabWidget 移回容器再关闭窗口,避免被父窗口销毁
     if (m_chatFrame) {
         // 断开临时回调,避免 destroyed 中的副作用
         disconnect(m_chatFrame, nullptr, this, nullptr);
-        if (chatView->parent() == m_chatFrame) {
+        if (m_chatTabWidget->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());
+        m_chatTabWidget->hide();
+        m_chatTabWidget->setParent(m_chatContainer);
+        m_chatTabWidget->setWindowTitle(QString());
         if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
-            if (l->indexOf(chatView) < 0)
-                l->addWidget(chatView);
+            if (l->indexOf(m_chatTabWidget) < 0)
+                l->addWidget(m_chatTabWidget);
 
         } else {
             auto l2 = new QVBoxLayout(m_chatContainer);
             l2->setContentsMargins(0, 0, 0, 0);
-            l2->addWidget(chatView);
+            l2->addWidget(m_chatTabWidget);
         }
-        chatView->show();
+        m_chatTabWidget->show();
         // 现在安全地关闭独立窗口
         m_chatFrame->close();
         m_chatFrame = nullptr;
 
     } else {
         // 无独立窗口,仅确保嵌入
-        chatView->hide();
-        chatView->setParent(m_chatContainer);
-        chatView->setWindowTitle(QString());
+        m_chatTabWidget->hide();
+        m_chatTabWidget->setParent(m_chatContainer);
+        m_chatTabWidget->setWindowTitle(QString());
         if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
-            if (l->indexOf(chatView) < 0)
-                l->addWidget(chatView);
+            if (l->indexOf(m_chatTabWidget) < 0)
+                l->addWidget(m_chatTabWidget);
         } else {
             auto l2 = new QVBoxLayout(m_chatContainer);
             l2->setContentsMargins(0, 0, 0, 0);
-            l2->addWidget(chatView);
+            l2->addWidget(m_chatTabWidget);
         }
-        chatView->show();
+        m_chatTabWidget->show();
     }
 }
 
@@ -674,7 +705,7 @@ void MainPanel::onChatWindowCloseRequested()
 
 void MainPanel::onChatButtonClicked()
 {
-    if (!chatView) return;
+    if (!m_chatTabWidget) return;
 
     if (m_isStreaming) {
         // 推流时:只在独立聊天窗口上 显示/隐藏 切换,避免频繁 reparent
@@ -682,7 +713,7 @@ void MainPanel::onChatButtonClicked()
             // 如果当前在嵌入容器里,先从布局移除
             if (m_chatContainer) {
                 if (auto l = qobject_cast<QVBoxLayout*>(m_chatContainer->layout())) {
-                    l->removeWidget(chatView);
+                    l->removeWidget(m_chatTabWidget);
                 }
             }
             m_chatFrame = new TMainWindow();
@@ -690,9 +721,9 @@ void MainPanel::onChatButtonClicked()
             m_chatFrame->setWindowTitle(tr("聊天"));
             m_chatFrame->installEventFilter(this);
 
-            chatView->setParent(m_chatFrame);
-            m_chatFrame->setCentralWidget(chatView);
-            chatView->show();
+            m_chatTabWidget->setParent(m_chatFrame);
+            m_chatFrame->setCentralWidget(m_chatTabWidget);
+            m_chatTabWidget->show();
             m_chatFrame->resize(380, 540);
             m_chatFrame->show();
             m_chatFrame->raise();
@@ -702,12 +733,12 @@ void MainPanel::onChatButtonClicked()
             return; // 结束推流分支处理
         } else {
             // 已有独立窗口:切换显示/隐藏
-            if (chatView->parent() != m_chatFrame) {
-                chatView->setParent(m_chatFrame);
-                if (m_chatFrame->centralWidget() != chatView)
-                    m_chatFrame->setCentralWidget(chatView);
+            if (m_chatTabWidget->parent() != m_chatFrame) {
+                m_chatTabWidget->setParent(m_chatFrame);
+                if (m_chatFrame->centralWidget() != m_chatTabWidget)
+                    m_chatFrame->setCentralWidget(m_chatTabWidget);
             }
-            chatView->show();
+            m_chatTabWidget->show();
             if (m_chatFrame->isVisible()) {
                 m_chatFrame->hide();
                 if (m_chatButton) m_chatButton->setText(tr("显示聊天"));
@@ -730,7 +761,7 @@ void MainPanel::onChatButtonClicked()
         applyModeLayout();
     } else {
         // 目前为嵌入状态,弹出为独立窗口
-        showChatStandalone();
+        showChatInFrame();
         if (m_chatButton)
             m_chatButton->setText(tr("嵌入聊天"));
         applyModeLayout();
@@ -776,7 +807,7 @@ void MainPanel::applyModeLayout()
 
     if (isPlayer) {
         // 播放模式:根据聊天是否嵌入来决定右侧面板显示
-        const bool embeddedChat = (chatView && m_chatContainer && chatView->parent() == m_chatContainer);
+        const bool embeddedChat = (m_chatTabWidget && m_chatContainer && m_chatTabWidget->parent() == m_chatContainer);
         if (embeddedChat) {
             if (m_rightWidget) m_rightWidget->show();
             int chatW = panelW / 3;
@@ -796,7 +827,7 @@ void MainPanel::applyModeLayout()
     if (m_rightWidget) m_rightWidget->show();
     
     // 检查聊天当前是否为嵌入状态
-    const bool embeddedChat = (chatView && m_chatContainer && chatView->parent() == m_chatContainer);
+    const bool embeddedChat = (m_chatTabWidget && m_chatContainer && m_chatTabWidget->parent() == m_chatContainer);
     
     if (embeddedChat) {
         // 聊天为嵌入状态,设置分割布局
@@ -835,25 +866,25 @@ bool MainPanel::eventFilter(QObject *watched, QEvent *event)
             return true; // 事件已处理,不再继续关闭
         }
 
-        // 非推流:允许关闭,但先把 chatView 放回嵌入容器,避免被父窗口销毁
-        if (chatView && m_chatContainer && chatView->parent() == m_chatFrame) {
+        // 非推流:允许关闭,但先把 m_chatTabWidget 放回嵌入容器,避免被父窗口销毁
+        if (m_chatTabWidget && m_chatContainer && m_chatTabWidget->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());
+            m_chatTabWidget->hide();
+            m_chatTabWidget->setParent(m_chatContainer);
+            m_chatTabWidget->setWindowTitle(QString());
             if (auto l = qobject_cast<QVBoxLayout *>(m_chatContainer->layout())) {
-                if (l->indexOf(chatView) < 0)
-                    l->addWidget(chatView);
+                if (l->indexOf(m_chatTabWidget) < 0)
+                    l->addWidget(m_chatTabWidget);
             } else {
                 auto l2 = new QVBoxLayout(m_chatContainer);
                 l2->setContentsMargins(0, 0, 0, 0);
-                l2->addWidget(chatView);
+                l2->addWidget(m_chatTabWidget);
             }
-            chatView->show();
+            m_chatTabWidget->show();
             if (m_chatButton)
                 m_chatButton->setText(tr("聊天"));
             applyModeLayout();

+ 12 - 1
MainPanel.h

@@ -13,6 +13,10 @@
 // Qt Widgets
 #include <QPushButton>
 #include <QCheckBox>
+#include <QTabWidget>
+#include <QTableWidget>
+#include "widgets/chatView/chatwindow.h"
+#include "widgets/onlineuserswidget.h"
 
 // Project includes
 #include "libs/Recorder/export.h"
@@ -115,6 +119,11 @@ public slots:
      */
     void showChatEmbedded();
 
+    /**
+     * @brief 在浮动窗口中显示聊天
+     */
+    void showChatInFrame();
+
     // ========== 测试功能 ==========
     /**
      * @brief 开始推流拉流测试
@@ -157,7 +166,9 @@ private:
     QWidget *m_rightWidget = nullptr;          // 推流时整体隐藏的右侧面板(含聊天容器)
     
     // ========== 聊天相关组件 ==========
-    ChatWindow *chatView = nullptr;            // 统一的聊天窗口实例
+    QTabWidget *m_chatTabWidget = nullptr;     // Tab容器,包含聊天窗口和在线用户表格
+    ChatWindow *m_chatWindow = nullptr;        // 聊天窗口实例
+    OnlineUsersWidget *m_onlineUsersWidget = nullptr; // 在线用户管理widget
     QWidget *m_chatContainer = nullptr;        // 聊天窗口的容器(用于嵌入式显示)
     WebSocketClient *webSocketClient = nullptr;
     

+ 1 - 1
main.cpp

@@ -119,7 +119,7 @@ zlmediakit/zlmediakit:master
 
     // AVPlayerWidget avPlayerWidget;
     // avPlayerWidget.show();
-
+    // avPlayerWidget.playAsync("rtsp://127.0.0.1:554/stream/V1/0198da3f-5900-78e3-8160-2b7a149cc772");
     int ret = a.exec();
 
     return ret;

+ 463 - 0
widgets/onlineuserswidget.cpp

@@ -0,0 +1,463 @@
+#include "onlineuserswidget.h"
+#include <QApplication>
+#include <QDateTime>
+#include <QDebug>
+#include <QJsonDocument>
+
+OnlineUsersWidget::OnlineUsersWidget(QWidget *parent)
+    : QWidget(parent)
+    , m_mainLayout(nullptr)
+    , m_headerLayout(nullptr)
+    , m_titleLabel(nullptr)
+    , m_countLabel(nullptr)
+    , m_refreshButton(nullptr)
+    , m_searchEdit(nullptr)
+    , m_usersTable(nullptr)
+    , m_contextMenu(nullptr)
+    , m_privateMessageAction(nullptr)
+    , m_viewProfileAction(nullptr)
+    , m_kickUserAction(nullptr)
+{
+    setupUI();
+    setupTable();
+    setupContextMenu();
+    applyStyles();
+    
+    // 添加一些示例数据
+    addUser(OnlineUser("1", "管理员", "在线"));
+    addUser(OnlineUser("2", "用户A", "在线"));
+    addUser(OnlineUser("3", "用户B", "离开"));
+    addUser(OnlineUser("4", "用户C", "忙碌"));
+}
+
+OnlineUsersWidget::~OnlineUsersWidget()
+{
+}
+
+void OnlineUsersWidget::setupUI()
+{
+    m_mainLayout = new QVBoxLayout(this);
+    m_mainLayout->setContentsMargins(8, 8, 8, 8);
+    m_mainLayout->setSpacing(6);
+    
+    // 头部布局
+    m_headerLayout = new QHBoxLayout();
+    
+    m_titleLabel = new QLabel("在线用户", this);
+    m_titleLabel->setObjectName("titleLabel");
+    
+    m_countLabel = new QLabel("(0)", this);
+    m_countLabel->setObjectName("countLabel");
+    
+    m_refreshButton = new QPushButton("刷新", this);
+    m_refreshButton->setObjectName("refreshButton");
+    m_refreshButton->setMaximumWidth(60);
+    
+    m_headerLayout->addWidget(m_titleLabel);
+    m_headerLayout->addWidget(m_countLabel);
+    m_headerLayout->addStretch();
+    m_headerLayout->addWidget(m_refreshButton);
+    
+    // 搜索框
+    m_searchEdit = new QLineEdit(this);
+    m_searchEdit->setPlaceholderText("搜索用户...");
+    m_searchEdit->setObjectName("searchEdit");
+    
+    // 用户表格
+    m_usersTable = new QTableWidget(this);
+    m_usersTable->setObjectName("usersTable");
+    
+    // 添加到主布局
+    m_mainLayout->addLayout(m_headerLayout);
+    m_mainLayout->addWidget(m_searchEdit);
+    m_mainLayout->addWidget(m_usersTable);
+    
+    // 连接信号
+    connect(m_refreshButton, &QPushButton::clicked, this, &OnlineUsersWidget::onRefreshClicked);
+    connect(m_searchEdit, &QLineEdit::textChanged, this, &OnlineUsersWidget::onSearchTextChanged);
+}
+
+void OnlineUsersWidget::setupTable()
+{
+    // 设置表格列
+    m_usersTable->setColumnCount(3);
+    QStringList headers;
+    headers << "状态" << "用户名" << "最后活动";
+    m_usersTable->setHorizontalHeaderLabels(headers);
+    
+    // 设置表格属性
+    m_usersTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+    m_usersTable->setSelectionMode(QAbstractItemView::SingleSelection);
+    m_usersTable->setAlternatingRowColors(true);
+    m_usersTable->setShowGrid(false);
+    m_usersTable->verticalHeader()->setVisible(false);
+    
+    // 设置列宽
+    QHeaderView *header = m_usersTable->horizontalHeader();
+    header->setStretchLastSection(true);
+    header->resizeSection(0, 60);  // 状态列
+    header->resizeSection(1, 120); // 用户名列
+    
+    // 连接信号
+    connect(m_usersTable, &QTableWidget::itemDoubleClicked, 
+            [this](QTableWidgetItem *item) {
+                if (item) {
+                    onTableDoubleClicked(item->row(), item->column());
+                }
+            });
+    
+    m_usersTable->setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(m_usersTable, &QTableWidget::customContextMenuRequested,
+            this, &OnlineUsersWidget::onTableContextMenu);
+}
+
+void OnlineUsersWidget::setupContextMenu()
+{
+    m_contextMenu = new QMenu(this);
+    
+    m_privateMessageAction = new QAction("发送私信", this);
+    m_viewProfileAction = new QAction("查看资料", this);
+    m_kickUserAction = new QAction("踢出用户", this);
+    
+    m_contextMenu->addAction(m_privateMessageAction);
+    m_contextMenu->addAction(m_viewProfileAction);
+    m_contextMenu->addSeparator();
+    m_contextMenu->addAction(m_kickUserAction);
+    
+    // 连接信号
+    connect(m_privateMessageAction, &QAction::triggered, [this]() {
+        if (!m_selectedUserId.isEmpty()) {
+            emit privateMessageRequested(m_selectedUserId, m_selectedUsername);
+        }
+    });
+    
+    connect(m_viewProfileAction, &QAction::triggered, [this]() {
+        if (!m_selectedUserId.isEmpty()) {
+            emit userProfileRequested(m_selectedUserId, m_selectedUsername);
+        }
+    });
+    
+    connect(m_kickUserAction, &QAction::triggered, [this]() {
+        // TODO: 实现踢出用户功能
+        qDebug() << "踢出用户:" << m_selectedUsername;
+    });
+}
+
+void OnlineUsersWidget::applyStyles()
+{
+    setStyleSheet(
+        "#titleLabel {"
+        "    font-size: 14px;"
+        "    font-weight: bold;"
+        "    color: #2c3e50;"
+        "}"
+        
+        "#countLabel {"
+        "    font-size: 12px;"
+        "    color: #7f8c8d;"
+        "    margin-left: 5px;"
+        "}"
+        
+        "#refreshButton {"
+        "    padding: 4px 8px;"
+        "    border: 1px solid #bdc3c7;"
+        "    border-radius: 3px;"
+        "    background-color: #ecf0f1;"
+        "}"
+        
+        "#refreshButton:hover {"
+        "    background-color: #d5dbdb;"
+        "}"
+        
+        "#refreshButton:pressed {"
+        "    background-color: #bdc3c7;"
+        "}"
+        
+        "#searchEdit {"
+        "    padding: 6px;"
+        "    border: 1px solid #bdc3c7;"
+        "    border-radius: 3px;"
+        "    font-size: 12px;"
+        "}"
+        
+        "#usersTable {"
+        "    border: 1px solid #bdc3c7;"
+        "    border-radius: 3px;"
+        "    background-color: white;"
+        "    gridline-color: #ecf0f1;"
+        "}"
+        
+        "#usersTable::item {"
+        "    padding: 8px;"
+        "    border-bottom: 1px solid #ecf0f1;"
+        "}"
+        
+        "#usersTable::item:selected {"
+        "    background-color: #3498db;"
+        "    color: white;"
+        "}"
+        
+        "#usersTable::item:hover {"
+        "    background-color: #ecf0f1;"
+        "}"
+    );
+}
+
+void OnlineUsersWidget::addUser(const OnlineUser &user)
+{
+    // 检查用户是否已存在
+    int existingRow = findUserRow(user.userId);
+    if (existingRow >= 0) {
+        // 更新现有用户
+        m_users[existingRow] = user;
+        updateTableRow(existingRow, user);
+        return;
+    }
+    
+    // 添加新用户
+    m_users.append(user);
+    
+    // 如果当前有过滤条件,检查是否匹配
+    if (!m_currentFilter.isEmpty()) {
+        if (!user.username.contains(m_currentFilter, Qt::CaseInsensitive)) {
+            updateUserCountLabel();
+            return;
+        }
+    }
+    
+    // 添加到表格
+    int row = m_usersTable->rowCount();
+    m_usersTable->insertRow(row);
+    updateTableRow(row, user);
+    
+    updateUserCountLabel();
+}
+
+void OnlineUsersWidget::removeUser(const QString &userId)
+{
+    int row = findUserRow(userId);
+    if (row >= 0) {
+        m_users.removeAt(row);
+        m_usersTable->removeRow(row);
+        updateUserCountLabel();
+    }
+}
+
+void OnlineUsersWidget::updateUserStatus(const QString &userId, const QString &status)
+{
+    int userIndex = -1;
+    for (int i = 0; i < m_users.size(); ++i) {
+        if (m_users[i].userId == userId) {
+            userIndex = i;
+            break;
+        }
+    }
+    
+    if (userIndex >= 0) {
+        m_users[userIndex].status = status;
+        m_users[userIndex].lastSeen = QDateTime::currentDateTime();
+        
+        // 更新表格中对应的行
+        int row = findUserRow(userId);
+        if (row >= 0) {
+            updateTableRow(row, m_users[userIndex]);
+        }
+    }
+}
+
+void OnlineUsersWidget::updateUserList(const QJsonArray &users)
+{
+    clearUsers();
+    
+    for (const auto &value : users) {
+        if (value.isObject()) {
+            QJsonObject userObj = value.toObject();
+            OnlineUser user;
+            user.userId = userObj["id"].toString();
+            user.username = userObj["username"].toString();
+            user.status = userObj["status"].toString("在线");
+            user.isAdmin = userObj["isAdmin"].toBool(false);
+            
+            if (userObj.contains("lastSeen")) {
+                user.lastSeen = QDateTime::fromString(userObj["lastSeen"].toString(), Qt::ISODate);
+            } else {
+                user.lastSeen = QDateTime::currentDateTime();
+            }
+            
+            addUser(user);
+        }
+    }
+}
+
+void OnlineUsersWidget::clearUsers()
+{
+    m_users.clear();
+    m_usersTable->setRowCount(0);
+    updateUserCountLabel();
+}
+
+QList<OnlineUser> OnlineUsersWidget::getAllUsers() const
+{
+    return m_users;
+}
+
+OnlineUser OnlineUsersWidget::getUser(const QString &userId) const
+{
+    for (const auto &user : m_users) {
+        if (user.userId == userId) {
+            return user;
+        }
+    }
+    return OnlineUser();
+}
+
+int OnlineUsersWidget::getUserCount() const
+{
+    return m_users.size();
+}
+
+void OnlineUsersWidget::setSearchVisible(bool visible)
+{
+    m_searchEdit->setVisible(visible);
+}
+
+void OnlineUsersWidget::filterUsers(const QString &keyword)
+{
+    m_currentFilter = keyword;
+    
+    // 清空表格
+    m_usersTable->setRowCount(0);
+    
+    // 重新添加匹配的用户
+    for (const auto &user : m_users) {
+        if (keyword.isEmpty() || user.username.contains(keyword, Qt::CaseInsensitive)) {
+            int row = m_usersTable->rowCount();
+            m_usersTable->insertRow(row);
+            updateTableRow(row, user);
+        }
+    }
+    
+    updateUserCountLabel();
+}
+
+void OnlineUsersWidget::onTableDoubleClicked(int row, int column)
+{
+    Q_UNUSED(column);
+    
+    if (row >= 0 && row < m_usersTable->rowCount()) {
+        QTableWidgetItem *userItem = m_usersTable->item(row, 1); // 用户名列
+        if (userItem) {
+            QString username = userItem->text();
+            // 从用户列表中找到对应的userId
+            for (const auto &user : m_users) {
+                if (user.username == username) {
+                    emit userDoubleClicked(user.userId, username);
+                    break;
+                }
+            }
+        }
+    }
+}
+
+void OnlineUsersWidget::onTableContextMenu(const QPoint &position)
+{
+    QTableWidgetItem *item = m_usersTable->itemAt(position);
+    if (!item) return;
+    
+    int row = item->row();
+    QTableWidgetItem *userItem = m_usersTable->item(row, 1);
+    if (!userItem) return;
+    
+    QString username = userItem->text();
+    // 从用户列表中找到对应的userId
+    for (const auto &user : m_users) {
+        if (user.username == username) {
+            m_selectedUserId = user.userId;
+            m_selectedUsername = username;
+            
+            // 根据用户权限显示/隐藏菜单项
+            m_kickUserAction->setVisible(user.isAdmin); // 示例:只有管理员能踢人
+            
+            QPoint globalPos = m_usersTable->mapToGlobal(position);
+            emit userRightClicked(user.userId, username, globalPos);
+            m_contextMenu->exec(globalPos);
+            break;
+        }
+    }
+}
+
+void OnlineUsersWidget::onSearchTextChanged(const QString &text)
+{
+    filterUsers(text);
+}
+
+void OnlineUsersWidget::onRefreshClicked()
+{
+    // TODO: 发送刷新用户列表的请求
+    qDebug() << "刷新用户列表";
+}
+
+void OnlineUsersWidget::updateUserCountLabel()
+{
+    int totalUsers = m_users.size();
+    int displayedUsers = m_usersTable->rowCount();
+    
+    if (m_currentFilter.isEmpty()) {
+        m_countLabel->setText(QString("(%1)").arg(totalUsers));
+    } else {
+        m_countLabel->setText(QString("(%1/%2)").arg(displayedUsers).arg(totalUsers));
+    }
+}
+
+void OnlineUsersWidget::updateTableRow(int row, const OnlineUser &user)
+{
+    // 状态列
+    QTableWidgetItem *statusItem = new QTableWidgetItem(getStatusIcon(user.status) + " " + user.status);
+    statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable);
+    m_usersTable->setItem(row, 0, statusItem);
+    
+    // 用户名列
+    QTableWidgetItem *nameItem = new QTableWidgetItem(user.username);
+    nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
+    if (user.isAdmin) {
+        nameItem->setText(user.username + " (管理员)");
+        nameItem->setForeground(QColor("#e74c3c"));
+    }
+    m_usersTable->setItem(row, 1, nameItem);
+    
+    // 最后活动时间列
+    QString timeText = user.lastSeen.toString("hh:mm");
+    QTableWidgetItem *timeItem = new QTableWidgetItem(timeText);
+    timeItem->setFlags(timeItem->flags() & ~Qt::ItemIsEditable);
+    m_usersTable->setItem(row, 2, timeItem);
+}
+
+int OnlineUsersWidget::findUserRow(const QString &userId) const
+{
+    for (int i = 0; i < m_users.size(); ++i) {
+        if (m_users[i].userId == userId) {
+            // 在表格中查找对应行
+            for (int row = 0; row < m_usersTable->rowCount(); ++row) {
+                QTableWidgetItem *nameItem = m_usersTable->item(row, 1);
+                if (nameItem && nameItem->text().contains(m_users[i].username)) {
+                    return row;
+                }
+            }
+            break;
+        }
+    }
+    return -1;
+}
+
+QString OnlineUsersWidget::getStatusIcon(const QString &status) const
+{
+    if (status == "在线") {
+        return "🟢";
+    } else if (status == "离开") {
+        return "🟡";
+    } else if (status == "忙碌") {
+        return "🔴";
+    } else {
+        return "⚪";
+    }
+}

+ 102 - 0
widgets/onlineuserswidget.h

@@ -0,0 +1,102 @@
+#ifndef ONLINEUSERSWIDGET_H
+#define ONLINEUSERSWIDGET_H
+
+#include <QWidget>
+#include <QTableWidget>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <QLineEdit>
+#include <QHeaderView>
+#include <QMenu>
+#include <QAction>
+#include <QTimer>
+#include <QJsonObject>
+#include <QJsonArray>
+
+struct OnlineUser {
+    QString userId;
+    QString username;
+    QString status;        // "在线", "离开", "忙碌"
+    QDateTime lastSeen;
+    QString avatar;        // 头像URL或路径
+    bool isAdmin;
+    
+    OnlineUser() : isAdmin(false) {}
+    OnlineUser(const QString &id, const QString &name, const QString &stat = "在线")
+        : userId(id), username(name), status(stat), isAdmin(false), lastSeen(QDateTime::currentDateTime()) {}
+};
+
+class OnlineUsersWidget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit OnlineUsersWidget(QWidget *parent = nullptr);
+    ~OnlineUsersWidget();
+
+    // 用户管理
+    void addUser(const OnlineUser &user);
+    void removeUser(const QString &userId);
+    void updateUserStatus(const QString &userId, const QString &status);
+    void updateUserList(const QJsonArray &users);
+    void clearUsers();
+    
+    // 获取用户信息
+    QList<OnlineUser> getAllUsers() const;
+    OnlineUser getUser(const QString &userId) const;
+    int getUserCount() const;
+    
+    // 搜索功能
+    void setSearchVisible(bool visible);
+    void filterUsers(const QString &keyword);
+
+signals:
+    // 用户交互信号
+    void userDoubleClicked(const QString &userId, const QString &username);
+    void userRightClicked(const QString &userId, const QString &username, const QPoint &position);
+    void privateMessageRequested(const QString &userId, const QString &username);
+    void userProfileRequested(const QString &userId, const QString &username);
+
+private slots:
+    void onTableDoubleClicked(int row, int column);
+    void onTableContextMenu(const QPoint &position);
+    void onSearchTextChanged(const QString &text);
+    void onRefreshClicked();
+    void updateUserCountLabel();
+
+private:
+    void setupUI();
+    void setupTable();
+    void setupContextMenu();
+    void applyStyles();
+    void updateTableRow(int row, const OnlineUser &user);
+    int findUserRow(const QString &userId) const;
+    QString getStatusIcon(const QString &status) const;
+    
+    // UI组件
+    QVBoxLayout *m_mainLayout;
+    QHBoxLayout *m_headerLayout;
+    QLabel *m_titleLabel;
+    QLabel *m_countLabel;
+    QPushButton *m_refreshButton;
+    QLineEdit *m_searchEdit;
+    QTableWidget *m_usersTable;
+    
+    // 右键菜单
+    QMenu *m_contextMenu;
+    QAction *m_privateMessageAction;
+    QAction *m_viewProfileAction;
+    QAction *m_kickUserAction;  // 仅管理员可见
+    
+    // 数据
+    QList<OnlineUser> m_users;
+    QString m_currentFilter;
+    
+    // 当前选中的用户
+    QString m_selectedUserId;
+    QString m_selectedUsername;
+};
+
+#endif // ONLINEUSERSWIDGET_H