xiaohuo 8 months ago
commit a26e383f74
  1. 1
      ALOps_sys_fe/alops-ui/package.json
  2. 22
      ALOps_sys_fe/alops-ui/src/layout/index.vue
  3. 28
      ALOps_sys_fe/alops-ui/src/permission.js
  4. 38
      ALOps_sys_fe/alops-ui/src/router/index.js
  5. 1
      ALOps_sys_fe/alops-ui/src/store/modules/permission.js
  6. 20
      ALOps_sys_fe/alops-ui/src/store/modules/user.js
  7. 292
      ALOps_sys_fe/alops-ui/src/utils/websocket.js
  8. 1990
      ALOps_sys_fe/alops-ui/src/views/dp.html
  9. 571
      ALOps_sys_fe/alops-ui/src/views/index_1.vue
  10. 2
      ALOps_sys_fe/alops-ui/src/views/login.vue
  11. 197
      ALOps_sys_fe/alops-ui/src/views/message/notificationTest.vue

@ -44,6 +44,7 @@
"screenfull": "5.0.2",
"sortablejs": "1.10.2",
"splitpanes": "2.4.1",
"tencentcloud-sdk-nodejs": "^4.1.135",
"vue": "2.6.12",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",

@ -1,5 +1,5 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="!isFullscreen" :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
@ -11,6 +11,10 @@
<settings ref="settingRef"/>
</div>
</div>
<!-- 全屏模式 -->
<div v-else class="fullscreen-wrapper">
<app-main/>
</div>
</template>
<script>
@ -38,6 +42,10 @@ export default {
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
isFullscreen() {
//
return this.$route.meta.fullscreen === true;
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
@ -77,6 +85,18 @@ export default {
}
}
.fullscreen-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: #f0f2f5;
}
.drawer-bg {
background: #000;
opacity: 0.3;

@ -21,7 +21,12 @@ router.beforeEach((to, from, next) => {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
// 登录后根据角色重定向到不同主页
if (store.getters.roles && store.getters.roles.includes('admin')) {
next({ path: '/index_admin' })
} else {
next({ path: '/' })
}
NProgress.done()
} else if (isWhiteList(to.path)) {
next()
@ -29,21 +34,32 @@ router.beforeEach((to, from, next) => {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
store.dispatch('user/getInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
store.dispatch('permission/GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
// 获取角色信息后,判断是跳转到默认路由还是管理员路由
const redirectPath = store.getters.roles.includes('admin') ? '/index_admin' : to.path
next({ path: redirectPath, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
store.dispatch('user/LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
// 如果访问的是根路径,根据角色重定向
if (to.path === '/') {
if (store.getters.roles.includes('admin')) {
next({ path: '/index_admin', replace: true })
} else {
next()
}
} else {
next()
}
}
}
} else {

@ -30,6 +30,32 @@ import Layout from '@/layout'
// 公共路由
export const constantRoutes = [
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index_admin',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true, roles: ['common'] }
}
]
},
// {
// path: '',
// component: Layout,
// redirect: 'index',
// children: [
// {
// path: 'index_admin',
// component: () => import('@/views/index_1'),
// name: 'IndexAdmin',
// meta: { title: '管理员首页', icon: 'dashboard', affix: true, fullscreen: true }
// }
// ]
// },
{
path: '/redirect',
component: Layout,
@ -70,7 +96,7 @@ export const constantRoutes = [
path: 'index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
meta: { title: '首页', icon: 'dashboard', affix: true, roles: ['common'] }
}
]
},
@ -180,5 +206,13 @@ Router.prototype.replace = function push(location) {
export default new Router({
mode: 'history', // 去掉url中的#
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
routes: [
...constantRoutes,
// 站内信测试页面路由
{
path: '/message/notificationTest',
component: () => import('@/views/message/notificationTest'),
hidden: true
}
]
})

@ -6,6 +6,7 @@ import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
const permission = {
namespaced: true,
state: {
routes: [],
addRoutes: [],

@ -2,10 +2,12 @@ import router from '@/router'
import { MessageBox, } from 'element-ui'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import websocketClient from '@/utils/websocket.js'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
const user = {
namespaced: true,
state: {
token: getToken(),
id: '',
@ -42,13 +44,10 @@ const user = {
actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
login({ commit }, userInfo) {
const { username, password, code, uuid } = userInfo
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
login(username.trim(), password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
@ -59,7 +58,7 @@ const user = {
},
// 获取用户信息
GetInfo({ commit, state }) {
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
@ -77,6 +76,9 @@ const user = {
commit('SET_NAME', user.userName)
commit('SET_NICK_NAME', user.nickName)
commit('SET_AVATAR', avatar)
// 初始化WebSocket连接,用于接收站内信
websocketClient.init(user.userId)
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
MessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
@ -104,6 +106,8 @@ const user = {
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
// 关闭WebSocket连接
websocketClient.close()
resolve()
}).catch(error => {
reject(error)
@ -116,6 +120,8 @@ const user = {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
// 关闭WebSocket连接
websocketClient.close()
resolve()
})
}

@ -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>

@ -141,7 +141,7 @@ export default {
Cookies.remove("password")
Cookies.remove('rememberMe')
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$store.dispatch("user/login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{})
}).catch(() => {
this.loading = false

@ -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…
Cancel
Save