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