Merge branch 'dev' of https://gitea.wangzhiwen.top/0214/ALOps_sys1 into dev
commit
a26e383f74
@ -0,0 +1,292 @@ |
||||
import { Message, Notification } from 'element-ui' |
||||
import { getToken } from '@/utils/auth' |
||||
|
||||
class WebSocketClient { |
||||
constructor() { |
||||
this.ws = null |
||||
this.url = '' |
||||
this.userId = '' |
||||
this.reconnectAttempts = 0 |
||||
this.maxReconnectAttempts = 5 |
||||
this.reconnectInterval = 3000 |
||||
this.connected = false |
||||
// token传递方式: 'url'(默认,URL参数), 'header'(请求头), 'message'(首次消息)
|
||||
this.tokenMode = 'url' |
||||
} |
||||
|
||||
// 设置token传递方式
|
||||
setTokenMode(mode) { |
||||
const validModes = ['url', 'header', 'message'] |
||||
if (validModes.includes(mode)) { |
||||
this.tokenMode = mode |
||||
console.log(`WebSocket token传递方式已设置为: ${mode}`) |
||||
|
||||
// 特别说明header模式的限制
|
||||
if (mode === 'header') { |
||||
console.warn('注意: header模式使用WebSocket子协议传递token,受浏览器API限制,实际token将通过Sec-WebSocket-Protocol头传递') |
||||
console.warn('后端需要从Sec-WebSocket-Protocol请求头中提取token信息') |
||||
} |
||||
return true |
||||
} |
||||
console.error('无效的token传递方式,可选值: url, header, message') |
||||
return false |
||||
} |
||||
|
||||
// 初始化WebSocket连接
|
||||
init(userId) { |
||||
// 重置重连计数
|
||||
this.reconnectAttempts = 0 |
||||
|
||||
// 检查必要参数
|
||||
if (!userId) { |
||||
console.error('WebSocket初始化失败: 用户ID不能为空') |
||||
return false |
||||
} |
||||
|
||||
// 获取token并检查登录状态
|
||||
const token = getToken() |
||||
if (!token) { |
||||
console.warn('WebSocket初始化失败: 未获取到token,请先登录') |
||||
return false |
||||
} |
||||
|
||||
this.userId = userId |
||||
// 确保baseURL不包含多余的引号或反引号
|
||||
let baseURL = process.env.VUE_APP_BASE_API |
||||
if (!baseURL) { |
||||
console.error('WebSocket初始化失败: 未配置VUE_APP_BASE_API环境变量') |
||||
return false |
||||
} |
||||
|
||||
// 清理baseURL格式
|
||||
baseURL = baseURL.replace(/[`'"\s]/g, '') |
||||
console.log('清理后的baseURL:', baseURL) |
||||
|
||||
// 构建WebSocket URL,将http替换为ws,https替换为wss
|
||||
let wsProtocol = baseURL.includes('https') ? 'wss' : 'ws' |
||||
let wsBaseURL = baseURL.replace(/^https?:\/\//, '') |
||||
|
||||
// 根据token传递方式构建URL
|
||||
if (this.tokenMode === 'url') { |
||||
// 在token前面加上'token-'前缀
|
||||
this.url = `${wsProtocol}://${wsBaseURL}/ws/onlineMessage?token=token-${token}` |
||||
} else { |
||||
// header或message模式下,URL不包含token
|
||||
this.url = `${wsProtocol}://${wsBaseURL}/ws/onlineMessage` |
||||
} |
||||
|
||||
// 输出环境变量和配置信息以调试
|
||||
console.log('WebSocket配置:', { |
||||
baseURL, |
||||
userId, |
||||
wsProtocol, |
||||
wsBaseURL, |
||||
hasToken: !!token, |
||||
tokenMode: this.tokenMode |
||||
}) |
||||
console.log('尝试连接WebSocket URL:', this.url) |
||||
|
||||
// 初始化连接
|
||||
this.connect() |
||||
return true |
||||
} |
||||
|
||||
// 手动重新初始化连接(用于登录后重新连接)
|
||||
reinitialize() { |
||||
if (this.userId && getToken()) { |
||||
console.log('尝试重新初始化WebSocket连接...') |
||||
return this.init(this.userId) |
||||
} |
||||
console.warn('无法重新初始化WebSocket连接: 用户ID或token缺失') |
||||
return false |
||||
} |
||||
|
||||
// 建立连接
|
||||
connect() { |
||||
// 检查URL是否已初始化
|
||||
if (!this.url) { |
||||
console.error('WebSocket连接失败: URL未初始化') |
||||
return |
||||
} |
||||
|
||||
// 确保之前的连接已关闭
|
||||
if (this.ws) { |
||||
try { |
||||
this.ws.close() |
||||
} catch (e) { |
||||
console.warn('关闭旧WebSocket连接时出错:', e) |
||||
} |
||||
this.ws = null |
||||
} |
||||
|
||||
try { |
||||
console.log(`正在建立WebSocket连接${this.tokenMode} (尝试 ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`) |
||||
|
||||
// 根据token模式创建WebSocket连接
|
||||
if (this.tokenMode === 'header') { |
||||
// header模式: 使用WebSocket子协议传递token
|
||||
// 注意:JavaScript WebSocket API要求子协议名必须是有效的字符串,不能包含特殊字符
|
||||
// 后端应从Sec-WebSocket-Protocol请求头中获取token信息
|
||||
const token = getToken() |
||||
if (token) { |
||||
// 在token前面加上'token-'前缀
|
||||
// 使用简洁的子协议名称,让后端从请求头中解析完整token
|
||||
this.ws = new WebSocket(this.url, ['auth-token']) |
||||
console.log('WebSocket连接将通过Sec-WebSocket-Protocol头传递token') |
||||
} else { |
||||
console.error('无法获取token,使用普通连接') |
||||
this.ws = new WebSocket(this.url) |
||||
} |
||||
} else { |
||||
// url或message模式: 直接创建连接
|
||||
this.ws = new WebSocket(this.url) |
||||
} |
||||
|
||||
// 设置事件监听
|
||||
this.ws.onopen = this.onOpen.bind(this) |
||||
this.ws.onmessage = this.onMessage.bind(this) |
||||
this.ws.onerror = this.onError.bind(this) |
||||
this.ws.onclose = this.onClose.bind(this) |
||||
} catch (error) { |
||||
console.error('WebSocket连接异常:', error) |
||||
// 直接触发重连
|
||||
setTimeout(() => this.reconnect(), this.reconnectInterval) |
||||
} |
||||
} |
||||
|
||||
// 连接打开回调
|
||||
onOpen(event) { |
||||
console.log('WebSocket连接已建立') |
||||
this.connected = true |
||||
this.reconnectAttempts = 0 |
||||
|
||||
// message模式: 连接成功后发送token
|
||||
if (this.tokenMode === 'message') { |
||||
const token = getToken() |
||||
if (token) { |
||||
const authMessage = { |
||||
type: 'auth', |
||||
token: `token-${token}`, // 在token前面加上'token-'前缀
|
||||
userId: this.userId |
||||
} |
||||
this.send(authMessage) |
||||
console.log('已发送认证消息:', authMessage) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 接收消息回调
|
||||
onMessage(event) { |
||||
try { |
||||
const message = JSON.parse(event.data) |
||||
this.handleMessage(message) |
||||
} catch (error) { |
||||
console.error('解析消息失败:', error) |
||||
} |
||||
} |
||||
|
||||
// 处理接收到的消息
|
||||
handleMessage(message) { |
||||
console.log('收到站内信:', message) |
||||
|
||||
// 使用Element UI的Notification组件显示消息
|
||||
Notification({ |
||||
title: message.title || '系统通知', |
||||
message: message.content, |
||||
type: 'info', |
||||
duration: 1000000, // 10秒后自动关闭
|
||||
showClose: true, |
||||
onClick: () => { |
||||
// 可以在这里添加点击通知后的处理逻辑
|
||||
console.log('用户点击了通知:', message.messageId) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 错误处理回调
|
||||
onError(error) { |
||||
console.error('WebSocket错误:', error) |
||||
// 添加更详细的错误信息
|
||||
if (this.ws) { |
||||
console.error('WebSocket状态:', this.ws.readyState) |
||||
console.error('WebSocket URL:', this.ws.url) |
||||
} |
||||
// 可能的原因分析
|
||||
console.error('可能的连接失败原因:') |
||||
console.error('1. 服务器WebSocket服务未运行或路径错误') |
||||
console.error('2. 网络问题或防火墙阻止') |
||||
console.error('3. CORS配置问题') |
||||
console.error('4. token无效或过期') |
||||
} |
||||
|
||||
// 连接关闭回调
|
||||
onClose(event) { |
||||
console.log('WebSocket连接已关闭', event) |
||||
// 输出关闭原因和代码
|
||||
console.log('关闭代码:', event.code) |
||||
console.log('关闭原因:', event.reason) |
||||
console.log('是否正常关闭:', event.wasClean) |
||||
|
||||
this.connected = false |
||||
this.reconnect() |
||||
} |
||||
|
||||
// 重连逻辑
|
||||
reconnect() { |
||||
// 检查token是否仍然有效
|
||||
const token = getToken() |
||||
if (!token) { |
||||
console.warn('WebSocket重连取消: token已失效或不存在') |
||||
return |
||||
} |
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) { |
||||
this.reconnectAttempts++ |
||||
// 指数退避策略 - 每次重连间隔增加
|
||||
const currentInterval = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1) |
||||
console.log(`WebSocket将在 ${Math.round(currentInterval/1000)} 秒后进行第${this.reconnectAttempts}次重连...`) |
||||
|
||||
setTimeout(() => { |
||||
// 根据不同token模式更新认证信息
|
||||
if (this.tokenMode === 'url' && this.url) { |
||||
// URL模式: 更新URL中的token参数
|
||||
// 在token前面加上'token-'前缀
|
||||
this.url = this.url.replace(/token=[^&]*/, `token=token-${token}`) |
||||
} else if (this.tokenMode === 'header') { |
||||
// Header模式: 重连时会自动从getToken()获取最新token
|
||||
console.log('Header模式下重连,将使用最新token') |
||||
} else if (this.tokenMode === 'message') { |
||||
// Message模式: 重连成功后会自动发送认证消息
|
||||
console.log('Message模式下重连,将在连接建立后发送认证消息') |
||||
} |
||||
this.connect() |
||||
}, currentInterval) |
||||
} else { |
||||
console.error('WebSocket重连失败,已达到最大重试次数') |
||||
// 不显示错误消息,避免干扰用户体验
|
||||
// Message.error('消息服务连接失败,请刷新页面重试')
|
||||
} |
||||
} |
||||
|
||||
// 发送消息
|
||||
send(data) { |
||||
if (this.connected && this.ws.readyState === WebSocket.OPEN) { |
||||
this.ws.send(JSON.stringify(data)) |
||||
} else { |
||||
console.error('WebSocket连接未建立') |
||||
} |
||||
} |
||||
|
||||
// 关闭连接
|
||||
close() { |
||||
if (this.ws) { |
||||
this.ws.close() |
||||
this.ws = null |
||||
} |
||||
this.connected = false |
||||
} |
||||
} |
||||
|
||||
// 导出单例实例
|
||||
const websocketClient = new WebSocketClient() |
||||
export default websocketClient |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,571 @@ |
||||
<template> |
||||
<div class="body"> |
||||
<div class="container"> |
||||
<!-- 管理员专属欢迎信息 --> |
||||
<div class="admin-welcome"> |
||||
<h1>管理员控制台</h1> |
||||
<p>欢迎回来,管理员!您可以在这里查看所有系统信息和管理用户。</p> |
||||
</div> |
||||
<!-- 上层:三个模块并排 --> |
||||
<div class="top-section"> |
||||
<!-- 预警信息统计 --> |
||||
<div class="panel"> |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<h2 class="card-title"><i class="fas fa-exclamation-triangle"></i> 预警信息统计</h2> |
||||
</div> |
||||
<div class="stats-container"> |
||||
<div class="stat-card" :class="alertMessageData.yearCount == 0?'normal':''"> |
||||
<div class="stat-label">年预警总数</div> |
||||
<div class="stat-value">{{ alertMessageData.yearCount }}</div> |
||||
</div> |
||||
<div class="stat-card" :class="alertMessageData.monthCount == 0?'normal':''"> |
||||
<div class="stat-label">月预警总数</div> |
||||
<div class="stat-value">{{ alertMessageData.monthCount }}</div> |
||||
</div> |
||||
<!-- 等级预警 --> |
||||
<div class="stat-card" :class="i.count == 0?'normal':i.count>10?'warning':''" v-for="i in alertMessageData.levelCount" :key="i.id"> |
||||
<div class="stat-label">{{ `等级${i.level}预警` }}</div> |
||||
<div class="stat-value">{{ i.count }}</div> |
||||
</div> |
||||
<!-- 设备预警 --> |
||||
<div class="stat-card" :class="i.count == 0?'normal':i.count>10?'warning':''" v-for="i in alertMessageData.typeCount" :key="i.id"> |
||||
<div class="stat-label">{{ `${i.name}预警` }}</div> |
||||
<div class="stat-value">{{ i.count }}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 检测设备信息 --> |
||||
<div class="panel"> |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<h2 class="card-title"><i class="fas fa-microchip"></i> 检测设备信息</h2> |
||||
</div> |
||||
<ul class="device-list"> |
||||
<PieChart height="250px" :chartData="byEquTypeData"/> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 设备预警信息 --> |
||||
<div class="panel"> |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<h2 class="card-title"><i class="fas fa-bell"></i> 设备预警信息</h2> |
||||
</div> |
||||
<el-empty description="暂无预警" v-if="alerts.length == 0"></el-empty> |
||||
<ul class="device-list" v-infinite-scroll="loadMore" :infinite-scroll-disabled="isLoading || noMore" infinite-scroll-distance="50" infinite-scroll-delay="300"> |
||||
<li class="device-item" v-for="alert in alerts" :key="alert.id" :class="alert.status === 'UNHANDLED' ? 'warning' : 'normal'" > |
||||
<div class="device-icon"> |
||||
<i :class="alert.status === 'UNHANDLED' ? 'fas fa-exclamation-circle' : 'fas fa-check-circle'"></i> |
||||
</div> |
||||
<div class="device-info"> |
||||
<div class="device-name">{{ alert.name }}</div> |
||||
<div class="device-details"> |
||||
<div>预警时间: {{ parseTime(alert.occurTime) }}</div> |
||||
<div>设备位置: {{ alert.location }}</div> |
||||
<div>预警信息: {{ alert.message }}</div> |
||||
</div> |
||||
</div> |
||||
<span :class="alert.status === 'UNHANDLED' ? 'alert-badge' : 'normal-badge'"> |
||||
{{ '等级'+alert.level }} |
||||
</span> |
||||
</li> |
||||
<p v-if="isLoading">加载中...</p> |
||||
<p v-if="noMore">没有更多了</p> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 下层:设备状态监控独立一行 --> |
||||
<div class="bottom-section"> |
||||
<div class="card full-width"> |
||||
<div class="card-header"> |
||||
<h2 class="card-title"><i class="fas fa-desktop"></i> 设备状态监控</h2> |
||||
<div class="last-update">最后更新: {{ lastUpdate }}</div> |
||||
</div> |
||||
<div class="table-container"> |
||||
|
||||
<el-empty description="暂无预警" v-if="tableHeaders.length == 0"></el-empty> |
||||
<table v-else> |
||||
<thead> |
||||
<tr> |
||||
<th v-for="header in tableHeaders" :key="header">{{ header }}</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr v-for="device in deviceStatus" :key="device.id"> |
||||
<td>{{ device.name }}</td> |
||||
<td>{{ device.location }}</td> |
||||
<td> |
||||
<span :class="getCpuStatus(device.cpuUtilization || 0)"></span> |
||||
{{ device.cpuUtilization || 0 }}% |
||||
</td> |
||||
<td> |
||||
<span :class="getMemoryStatus(device.memoryUtilization || 0)"></span> |
||||
{{ device.memoryUtilization || 0 }}% |
||||
</td> |
||||
<td>{{ device.temperature || 0 }}°C</td> |
||||
<td> |
||||
<el-tag :hit="false" :type="device.health == '1'?'success':device.health == '2'?'':device.health == '3'?'danger':device.health == '4'?'warning':'info'"> {{ device.health == '1'?'健康':device.health == '2'?'良好':device.health == '3'?'一般':device.health == '4'?'需维护':'未获取' }} </el-tag> |
||||
</td> |
||||
<td>{{ formatDuration(device.contWorkPeriod) }}</td> |
||||
<td>{{ parseTime(device.gatherTime) || '--' }}</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import PanelGroup from './dashboard/PanelGroup' |
||||
import LineChart from './dashboard/LineChart' |
||||
import RaddarChart from './dashboard/RaddarChart' |
||||
import PieChart from './dashboard/PieChart' |
||||
import BarChart from './dashboard/BarChart' |
||||
import digital from './dashboard/digital' |
||||
import { statistic, byEquType, listMonitor, alertMessageTimes } from "@/api/index/index" |
||||
import { getToken } from "@/utils/auth" |
||||
import Treeselect from "@riophae/vue-treeselect" |
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css" |
||||
import { Splitpanes, Pane } from "splitpanes" |
||||
import "splitpanes/dist/splitpanes.css" |
||||
|
||||
export default { |
||||
name: 'Index', |
||||
components: { |
||||
PanelGroup, |
||||
LineChart, |
||||
RaddarChart, |
||||
PieChart, |
||||
BarChart, |
||||
digital |
||||
}, |
||||
data() { |
||||
return { |
||||
totalAlerts: 111, |
||||
activeAlerts: 42, |
||||
resolvedAlerts: 69, |
||||
lastUpdate: new Date().toLocaleString(), |
||||
|
||||
isLoading: false, // 加载状态 |
||||
noMore: false, // 是否无更多数据 |
||||
page: 1, // 当前页码 |
||||
devices: [ |
||||
{ id: 1, name: 'Forecasts', description: '预测分析系统', status: 'normal', icon: 'fas fa-chart-line' }, |
||||
{ id: 2, name: 'Gold', description: '核心处理单元', status: 'warning', icon: 'fas fa-server' }, |
||||
{ id: 3, name: 'Industries', description: '工业控制系统', status: 'normal', icon: 'fas fa-industry' } |
||||
], |
||||
// 设备预警信息 |
||||
alerts: [], |
||||
// 统计预警信息次数 |
||||
alertMessageData: [], |
||||
|
||||
byEquTypeData: [], |
||||
|
||||
tableHeaders: ['设备名称', '设备位置', 'CPU使用率', '内存使用率', '温度', '健康度', '设备运行时长', '采集时间'], |
||||
deviceStatus: [] |
||||
} |
||||
}, |
||||
methods: { |
||||
formatDuration(input) {// 类型安全校验 |
||||
if (typeof input !== 'string') return "0天0小时"; |
||||
|
||||
const match = input.match(/(\d+)\s*days?,\s*(\d+):/i); |
||||
return match ? `${match[1]}天${match[2]}小时` : "0天0小时"; |
||||
}, |
||||
|
||||
getCpuStatus(usage) { |
||||
if (usage < 50) return 'status-indicator status-good'; |
||||
if (usage < 80) return 'status-indicator status-warning'; |
||||
return 'status-indicator status-critical'; |
||||
}, |
||||
getMemoryStatus(usage) { |
||||
if (usage < 60) return 'status-indicator status-good'; |
||||
if (usage < 85) return 'status-indicator status-warning'; |
||||
return 'status-indicator status-critical'; |
||||
}, |
||||
getHealthStatus(health) { |
||||
if (health >= 80) return 'health-good'; |
||||
if (health >= 60) return 'health-warning'; |
||||
return 'health-critical'; |
||||
}, |
||||
getbyEquType() { |
||||
this.loading = true |
||||
byEquType().then(response => { |
||||
this.loading = false |
||||
this.byEquTypeData = response.data |
||||
} |
||||
) |
||||
}, |
||||
getlistMonitor() { |
||||
this.loading = true |
||||
listMonitor({pageSize: 100}).then(response => { |
||||
this.loading = false |
||||
this.deviceStatus = response.rows |
||||
} |
||||
) |
||||
}, |
||||
getalertMessageTimes() { |
||||
this.loading = true |
||||
alertMessageTimes().then(response => { |
||||
this.alertMessageData = response.data |
||||
this.loading = false |
||||
} |
||||
) |
||||
}, |
||||
async loadMore() { |
||||
if (this.isLoading || this.noMore) return; |
||||
this.isLoading = true; |
||||
try { |
||||
const res = await statistic({page:this.page,status:'UNHANDLED'}); // 模拟API请求 |
||||
if (res.rows.length < 10) { // 假设每页10条 |
||||
this.noMore = true; |
||||
} else { |
||||
this.alerts = [...this.alerts, ...res.rows]; |
||||
this.page++; |
||||
} |
||||
} catch (error) { |
||||
console.error('加载失败:', error); |
||||
} finally { |
||||
this.isLoading = false; |
||||
} |
||||
} |
||||
}, |
||||
created() { |
||||
this.getbyEquType() |
||||
this.getlistMonitor() |
||||
this.getalertMessageTimes() |
||||
|
||||
|
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.admin-welcome { |
||||
background-color: #f5f7fa; |
||||
border-radius: 8px; |
||||
padding: 20px; |
||||
margin-bottom: 20px; |
||||
border-left: 4px solid #409eff; |
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.admin-welcome h1 { |
||||
color: #409eff; |
||||
margin-bottom: 10px; |
||||
font-size: 24px; |
||||
} |
||||
|
||||
.admin-welcome p { |
||||
color: #606266; |
||||
margin: 0; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.body { |
||||
background-color: #f5f7fa; |
||||
color: #333; |
||||
line-height: 1.6; |
||||
padding: 20px; |
||||
} |
||||
|
||||
.header { |
||||
text-align: center; |
||||
margin-bottom: 30px; |
||||
} |
||||
|
||||
.header h1 { |
||||
color: #2c3e50; |
||||
margin-bottom: 10px; |
||||
font-size: 28px; |
||||
} |
||||
|
||||
.header p { |
||||
color: #7f8c8d; |
||||
font-size: 16px; |
||||
} |
||||
|
||||
.container { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.top-section { |
||||
display: flex; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.bottom-section { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.panel { |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.card { |
||||
background: white; |
||||
border-radius: 12px; |
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
||||
padding: 20px; |
||||
transition: transform 0.3s ease, box-shadow 0.3s ease; |
||||
height: 100%; |
||||
} |
||||
|
||||
.card:hover { |
||||
transform: translateY(-5px); |
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); |
||||
} |
||||
|
||||
.card-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 15px; |
||||
padding-bottom: 10px; |
||||
border-bottom: 1px solid #eaeaea; |
||||
} |
||||
|
||||
.card-title { |
||||
font-size: 18px; |
||||
font-weight: 600; |
||||
color: #2c3e50; |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.card-title i { |
||||
color: #3498db; |
||||
} |
||||
|
||||
.stats-container { |
||||
display: grid; |
||||
justify-content: space-between; |
||||
grid-template-columns: repeat(3, 1fr); |
||||
gap: 15px; |
||||
overflow-y: auto; |
||||
overflow-x: hidden; |
||||
height: 255px; |
||||
} |
||||
|
||||
.stat-card { |
||||
flex: 1; |
||||
background: linear-gradient(135deg, #3498db, #2c3e50); |
||||
color: white; |
||||
border-radius: 10px; |
||||
padding: 12px; |
||||
text-align: center; |
||||
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.3); |
||||
} |
||||
|
||||
.stat-card.warning { |
||||
background: linear-gradient(135deg, #e74c3c, #c0392b); |
||||
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.3); |
||||
} |
||||
|
||||
.stat-card.normal { |
||||
background: linear-gradient(135deg, #2ecc71, #27ae60); |
||||
box-shadow: 0 4px 8px rgba(46, 204, 113, 0.3); |
||||
} |
||||
|
||||
.stat-value { |
||||
font-size: 32px; |
||||
font-weight: 700; |
||||
margin: 10px 0; |
||||
} |
||||
|
||||
.stat-label { |
||||
font-size: 14px; |
||||
opacity: 0.9; |
||||
} |
||||
|
||||
.device-list { |
||||
list-style: none; |
||||
overflow-y: auto; |
||||
overflow-x: hidden; |
||||
max-height: 255px; |
||||
padding-left: 0; |
||||
} |
||||
|
||||
.device-item { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 12px 15px; |
||||
margin-bottom: 10px; |
||||
background: #f8f9fa; |
||||
border-radius: 8px; |
||||
border-left: 4px solid #3498db; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.device-item:hover { |
||||
background: #edf2f7; |
||||
transform: translateX(5px); |
||||
} |
||||
|
||||
.device-item.warning { |
||||
border-left-color: #e74c3c; |
||||
} |
||||
|
||||
.device-item.normal { |
||||
border-left-color: #2ecc71; |
||||
} |
||||
|
||||
.device-icon { |
||||
width: 40px; |
||||
height: 40px; |
||||
background: #3498db; |
||||
border-radius: 50%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
margin-right: 15px; |
||||
color: white; |
||||
} |
||||
|
||||
.device-item.warning .device-icon { |
||||
background: #e74c3c; |
||||
} |
||||
|
||||
.device-item.normal .device-icon { |
||||
background: #2ecc71; |
||||
} |
||||
|
||||
.device-info { |
||||
flex: 1; |
||||
} |
||||
|
||||
.device-name { |
||||
font-weight: 600; |
||||
margin-bottom: 5px; |
||||
} |
||||
|
||||
.device-details { |
||||
font-size: 13px; |
||||
color: #666; |
||||
} |
||||
|
||||
.alert-badge { |
||||
background: #e74c3c; |
||||
color: white; |
||||
padding: 3px 8px; |
||||
border-radius: 12px; |
||||
font-size: 12px; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
.normal-badge { |
||||
background: #2ecc71; |
||||
color: white; |
||||
padding: 3px 8px; |
||||
border-radius: 12px; |
||||
font-size: 12px; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
.table-container { |
||||
overflow-x: auto; |
||||
border-radius: 10px; |
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
||||
} |
||||
|
||||
table { |
||||
width: 100%; |
||||
border-collapse: collapse; |
||||
background: white; |
||||
} |
||||
|
||||
th { |
||||
background: #2c3e50; |
||||
color: white; |
||||
padding: 15px 12px; |
||||
text-align: left; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
td { |
||||
padding: 12px; |
||||
border-bottom: 1px solid #eaeaea; |
||||
} |
||||
|
||||
tr:hover { |
||||
background-color: #f5f7fa; |
||||
} |
||||
|
||||
.status-indicator { |
||||
display: inline-block; |
||||
width: 10px; |
||||
height: 10px; |
||||
border-radius: 50%; |
||||
margin-right: 5px; |
||||
} |
||||
|
||||
.status-good { |
||||
background-color: #2ecc71; |
||||
} |
||||
|
||||
.status-warning { |
||||
background-color: #f39c12; |
||||
} |
||||
|
||||
.status-critical { |
||||
background-color: #e74c3c; |
||||
} |
||||
|
||||
.health-bar { |
||||
height: 6px; |
||||
background: #ecf0f1; |
||||
border-radius: 3px; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.health-fill { |
||||
height: 100%; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
.health-good { |
||||
background: #2ecc71; |
||||
width: 85%; |
||||
} |
||||
|
||||
.health-warning { |
||||
background: #f39c12; |
||||
width: 65%; |
||||
} |
||||
|
||||
.health-critical { |
||||
background: #e74c3c; |
||||
width: 40%; |
||||
} |
||||
|
||||
.full-width { |
||||
width: 100%; |
||||
} |
||||
|
||||
@media (max-width: 1024px) { |
||||
.top-section { |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.stats-container { |
||||
flex-direction: column; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,197 @@ |
||||
<template> |
||||
<div class="notification-test-container"> |
||||
<el-card shadow="never"> |
||||
<template #header> |
||||
<div class="card-header"> |
||||
<span>站内信功能测试</span> |
||||
</div> |
||||
</template> |
||||
|
||||
<div class="test-content"> |
||||
<el-alert |
||||
title="WebSocket连接状态" |
||||
:type="connectionStatus === 'connected' ? 'success' : 'warning'" |
||||
:closable="false" |
||||
show-icon |
||||
> |
||||
<template #description> |
||||
当前状态: {{ connectionStatusText }} |
||||
</template> |
||||
</el-alert> |
||||
|
||||
<div class="test-actions"> |
||||
<el-button type="primary" @click="testNotification">发送测试通知</el-button> |
||||
<el-button @click="reconnectWebSocket">重新连接WebSocket</el-button> |
||||
<el-button type="danger" @click="disconnectWebSocket">断开WebSocket连接</el-button> |
||||
</div> |
||||
|
||||
<div class="message-log"> |
||||
<h3>消息日志</h3> |
||||
<el-scrollbar height="300px" class="log-scrollbar"> |
||||
<div v-for="(log, index) in messageLogs" :key="index" class="log-item"> |
||||
<span class="log-time">{{ formatTime(log.time) }}</span> |
||||
<span class="log-content">{{ log.content }}</span> |
||||
</div> |
||||
</el-scrollbar> |
||||
</div> |
||||
</div> |
||||
</el-card> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import websocketClient from '@/utils/websocket.js' |
||||
import { Notification } from 'element-ui' |
||||
import { mapState } from 'vuex' |
||||
|
||||
export default { |
||||
name: 'NotificationTest', |
||||
data() { |
||||
return { |
||||
connectionStatus: 'disconnected', |
||||
connectionStatusText: '未连接', |
||||
messageLogs: [] |
||||
} |
||||
}, |
||||
computed: { |
||||
...mapState({ |
||||
userId: state => state.user.id |
||||
}) |
||||
}, |
||||
mounted() { |
||||
this.checkConnectionStatus() |
||||
this.logMessage('页面已加载,WebSocket状态检查完成') |
||||
}, |
||||
methods: { |
||||
// 检查WebSocket连接状态 |
||||
checkConnectionStatus() { |
||||
if (websocketClient.connected) { |
||||
this.connectionStatus = 'connected' |
||||
this.connectionStatusText = '已连接' |
||||
} else { |
||||
this.connectionStatus = 'disconnected' |
||||
this.connectionStatusText = '未连接' |
||||
} |
||||
}, |
||||
|
||||
// 发送测试通知 |
||||
testNotification() { |
||||
// 模拟接收到的WebSocket消息格式 |
||||
const mockMessage = { |
||||
messageId: Date.now(), |
||||
title: '测试通知', |
||||
content: '这是一条测试通知,用于验证站内信功能是否正常工作。', |
||||
timestamp: new Date().toISOString() |
||||
} |
||||
|
||||
// 使用Notification组件显示消息 |
||||
Notification({ |
||||
title: mockMessage.title, |
||||
message: mockMessage.content, |
||||
type: 'info', |
||||
duration: 10000, // 10秒后自动关闭 |
||||
showClose: true, |
||||
onClick: () => { |
||||
this.logMessage(`用户点击了通知: ${mockMessage.messageId}`) |
||||
} |
||||
}) |
||||
|
||||
this.logMessage(`已发送测试通知: ${mockMessage.title}`) |
||||
}, |
||||
|
||||
// 重新连接WebSocket |
||||
reconnectWebSocket() { |
||||
if (this.userId) { |
||||
websocketClient.init(this.userId) |
||||
this.checkConnectionStatus() |
||||
this.logMessage('正在重新连接WebSocket...') |
||||
} else { |
||||
this.logMessage('无法重新连接:未获取到用户ID') |
||||
this.$message.error('未获取到用户ID,请先登录') |
||||
} |
||||
}, |
||||
|
||||
// 断开WebSocket连接 |
||||
disconnectWebSocket() { |
||||
websocketClient.close() |
||||
this.checkConnectionStatus() |
||||
this.logMessage('WebSocket已断开连接') |
||||
}, |
||||
|
||||
// 记录消息日志 |
||||
logMessage(content) { |
||||
this.messageLogs.push({ |
||||
time: new Date(), |
||||
content: content |
||||
}) |
||||
// 保持日志最新的100条 |
||||
if (this.messageLogs.length > 100) { |
||||
this.messageLogs.shift() |
||||
} |
||||
// 滚动到底部 |
||||
this.$nextTick(() => { |
||||
const scrollbar = this.$el.querySelector('.log-scrollbar .el-scrollbar__wrap') |
||||
if (scrollbar) { |
||||
scrollbar.scrollTop = scrollbar.scrollHeight |
||||
} |
||||
}) |
||||
}, |
||||
|
||||
// 格式化时间 |
||||
formatTime(date) { |
||||
const d = new Date(date) |
||||
const hh = d.getHours().toString().padStart(2, '0') |
||||
const mm = d.getMinutes().toString().padStart(2, '0') |
||||
const ss = d.getSeconds().toString().padStart(2, '0') |
||||
return `${hh}:${mm}:${ss}` |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.notification-test-container { |
||||
padding: 20px; |
||||
} |
||||
|
||||
.card-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
.test-content { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.test-actions { |
||||
display: flex; |
||||
gap: 10px; |
||||
} |
||||
|
||||
.message-log h3 { |
||||
margin-bottom: 10px; |
||||
font-size: 16px; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.log-item { |
||||
display: flex; |
||||
padding: 5px 0; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
} |
||||
|
||||
.log-time { |
||||
color: #909399; |
||||
margin-right: 10px; |
||||
font-size: 12px; |
||||
min-width: 80px; |
||||
} |
||||
|
||||
.log-content { |
||||
flex: 1; |
||||
font-size: 14px; |
||||
} |
||||
</style> |
||||
Loading…
Reference in new issue