Junjie
2026-04-07 d9debca3da85bd2b66d0b9999cf3afab98a75214
#websocket连接管理页面
1个文件已添加
2个文件已修改
314 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/TvDeviceController.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/websocket/TvWebSocketServer.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/tvWebSocket/tvWebSocket.html 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/TvDeviceController.java
@@ -11,6 +11,8 @@
import com.zy.asrs.entity.TvDevice;
import com.zy.asrs.service.ApkBuildTaskService;
import com.zy.asrs.service.TvDeviceService;
import com.zy.asrs.websocket.TvWebSocketServer;
import jakarta.websocket.Session;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -369,4 +371,40 @@
            return R.error("重启失败: " + e.getMessage());
        }
    }
    /**
     * 查询电视机WebSocket连接状态
     */
    @RequestMapping(value = "/tvDevice/tvWebSocket/status/auth")
    @ManagerAuth
    public R tvWebSocketStatus() {
        Map<String, Session> sessions = TvWebSocketServer.getSessions();
        // 查询所有设备信息,用于匹配IP对应的设备名称
        List<TvDevice> allDevices = tvDeviceService.list();
        Map<String, String> ipToDeviceName = new HashMap<>();
        for (TvDevice device : allDevices) {
            if (device.getIp() != null) {
                ipToDeviceName.put(device.getIp(), device.getName());
            }
        }
        List<Map<String, Object>> connections = new ArrayList<>();
        for (Map.Entry<String, Session> entry : sessions.entrySet()) {
            Map<String, Object> info = new HashMap<>();
            String ip = entry.getKey();
            Session session = entry.getValue();
            info.put("ip", ip);
            info.put("deviceName", ipToDeviceName.getOrDefault(ip, "-"));
            info.put("sessionId", session.getId());
            info.put("open", session.isOpen());
            connections.add(info);
        }
        Map<String, Object> result = new HashMap<>();
        result.put("total", sessions.size());
        result.put("connections", connections);
        return R.ok(result);
    }
}
src/main/java/com/zy/asrs/websocket/TvWebSocketServer.java
@@ -179,6 +179,10 @@
        return SESSIONS.keySet();
    }
    public static Map<String, Session> getSessions() {
        return Collections.unmodifiableMap(SESSIONS);
    }
    private String getIp(Session session) {
        Object ip = session.getUserProperties().get("ip");
        return ip != null ? ip.toString() : "unknown";
src/main/webapp/views/tvWebSocket/tvWebSocket.html
New file
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>WebSocket连接监控</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="../../static/vue/element/element.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
            background: #f5f7fa;
            padding: 15px;
        }
        .app-container {
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .stats-cards {
            display: flex;
            gap: 15px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        .stat-card {
            background: linear-gradient(135deg, #409eff, #66b1ff);
            border-radius: 8px;
            padding: 20px 30px;
            color: #fff;
            min-width: 200px;
            flex: 1;
            max-width: 300px;
        }
        .stat-card .label {
            font-size: 14px;
            opacity: 0.9;
            margin-bottom: 8px;
        }
        .stat-card .value {
            font-size: 36px;
            font-weight: bold;
        }
        .header-section {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }
        .header-section .title {
            font-size: 16px;
            font-weight: bold;
            color: #303133;
        }
        .status-dot {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 6px;
        }
        .status-online {
            background: #67c23a;
        }
        .pagination-section {
            margin-top: 20px;
            display: flex;
            justify-content: flex-end;
        }
        .auto-refresh-bar {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 15px;
            font-size: 13px;
            color: #909399;
        }
    </style>
</head>
<body>
    <div id="app">
        <div class="app-container">
            <!-- 统计卡片 -->
            <div class="stats-cards">
                <div class="stat-card">
                    <div class="label">WebSocket连接总数</div>
                    <div class="value">{{ total }}</div>
                </div>
            </div>
            <!-- 自动刷新控制 -->
            <div class="auto-refresh-bar">
                <el-switch v-model="autoRefresh" active-text="自动刷新" inactive-text="关闭"></el-switch>
                <span>每5秒刷新一次</span>
                <el-button size="mini" type="primary" icon="el-icon-refresh" @click="loadData"
                    :loading="loading">手动刷新</el-button>
            </div>
            <!-- 表格区域 -->
            <div class="header-section">
                <span class="title">连接列表</span>
                <span style="color: #909399; font-size: 13px;">最后刷新: {{ lastRefreshTime || '-' }}</span>
            </div>
            <el-table :data="pagedData" border stripe v-loading="loading"
                :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%;">
                <el-table-column type="index" label="序号" width="60" align="center"
                    :index="indexMethod"></el-table-column>
                <el-table-column prop="ip" label="客户端IP" min-width="140" align="center"></el-table-column>
                <el-table-column prop="deviceName" label="设备名称" min-width="180">
                    <template slot-scope="scope">
                        {{ scope.row.deviceName || '-' }}
                    </template>
                </el-table-column>
                <el-table-column prop="sessionId" label="会话ID" min-width="200" show-overflow-tooltip>
                    <template slot-scope="scope">
                        {{ scope.row.sessionId || '-' }}
                    </template>
                </el-table-column>
                <el-table-column prop="open" label="状态" width="100" align="center">
                    <template slot-scope="scope">
                        <span>
                            <span class="status-dot" :class="scope.row.open ? 'status-online' : ''"></span>
                            {{ scope.row.open ? '活跃' : '已关闭' }}
                        </span>
                    </template>
                </el-table-column>
            </el-table>
            <!-- 分页 -->
            <div class="pagination-section" v-if="connections.length > pageSize">
                <el-pagination background layout="total, prev, pager, next" :total="connections.length"
                    :page-size="pageSize" :current-page="currentPage" @current-change="handlePageChange">
                </el-pagination>
            </div>
            <!-- 空状态 -->
            <div v-if="!loading && connections.length === 0"
                style="text-align: center; padding: 60px; color: #909399;">
                <i class="el-icon-warning-outline" style="font-size: 48px; margin-bottom: 15px;"></i>
                <div>暂无WebSocket连接</div>
            </div>
        </div>
    </div>
    <script src="../../static/vue/js/vue.min.js"></script>
    <script src="../../static/vue/element/element.js"></script>
    <script src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
    <script src="../../static/js/common.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                connections: [],
                total: 0,
                loading: false,
                autoRefresh: true,
                refreshTimer: null,
                lastRefreshTime: '',
                currentPage: 1,
                pageSize: 10
            },
            computed: {
                pagedData() {
                    var start = (this.currentPage - 1) * this.pageSize;
                    return this.connections.slice(start, start + this.pageSize);
                }
            },
            created() {
                this.loadData();
                this.startAutoRefresh();
            },
            watch: {
                autoRefresh: function (val) {
                    if (val) {
                        this.startAutoRefresh();
                    } else {
                        this.stopAutoRefresh();
                    }
                }
            },
            methods: {
                getHeaders() {
                    return { 'token': localStorage.getItem('token') };
                },
                loadData() {
                    this.loading = true;
                    var self = this;
                    $.ajax({
                        url: baseUrl + '/tvDevice/tvWebSocket/status/auth',
                        headers: this.getHeaders(),
                        success: function (res) {
                            self.loading = false;
                            if (res.code === 200) {
                                self.connections = res.data.connections || [];
                                self.total = res.data.total || 0;
                                self.lastRefreshTime = self.formatTime(new Date());
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                self.$message.error(res.msg || '加载失败');
                            }
                        },
                        error: function () {
                            self.loading = false;
                            self.$message.error('请求失败');
                        }
                    });
                },
                startAutoRefresh() {
                    this.stopAutoRefresh();
                    this.refreshTimer = setInterval(this.loadData, 5000);
                },
                stopAutoRefresh() {
                    if (this.refreshTimer) {
                        clearInterval(this.refreshTimer);
                        this.refreshTimer = null;
                    }
                },
                handlePageChange(page) {
                    this.currentPage = page;
                },
                indexMethod(index) {
                    return (this.currentPage - 1) * this.pageSize + index + 1;
                },
                formatTime(date) {
                    var h = String(date.getHours()).padStart(2, '0');
                    var m = String(date.getMinutes()).padStart(2, '0');
                    var s = String(date.getSeconds()).padStart(2, '0');
                    return h + ':' + m + ':' + s;
                }
            },
            beforeDestroy() {
                this.stopAutoRefresh();
            }
        });
    </script>
</body>
</html>