From 187d9bddbe6e0f173a64d0fbe91935a4029ee04b Mon Sep 17 00:00:00 2001 From: ZHW <1941286652@qq.com> Date: Mon, 15 Dec 2025 20:24:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E6=96=B0=E5=91=8A=E8=AD=A6=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campus/water/config/SecurityConfig.java | 2 +- .../water/controller/AlertController.java | 7 +- .../resources/web/src/views/Dashboard.vue | 265 +++++++++++++++--- 3 files changed, 225 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/campus/water/config/SecurityConfig.java b/src/main/java/com/campus/water/config/SecurityConfig.java index 5f3e4cc..8cca8f5 100644 --- a/src/main/java/com/campus/water/config/SecurityConfig.java +++ b/src/main/java/com/campus/water/config/SecurityConfig.java @@ -80,7 +80,7 @@ public class SecurityConfig { .requestMatchers("/api/common/register").permitAll() .requestMatchers("/static/**", "/templates/**").permitAll() .requestMatchers(request -> "OPTIONS".equals(request.getMethod())).permitAll() - .requestMatchers("/api/alerts/**").hasAnyRole("ADMIN", "REPAIRMAN") + .requestMatchers("/api/alerts/**").hasAnyRole("SUPER_ADMIN","AREA_ADMIN", "REPAIRMAN") .requestMatchers("/api/app/student/**").hasAnyRole("STUDENT", "ADMIN") .requestMatchers("/api/app/repair/**").hasAnyRole("REPAIRMAN", "ADMIN") .requestMatchers("/api/web/**").hasAnyRole("SUPER_ADMIN", "AREA_ADMIN", "VIEWER","REPAIRMAN") diff --git a/src/main/java/com/campus/water/controller/AlertController.java b/src/main/java/com/campus/water/controller/AlertController.java index fae5ee4..a2abd99 100644 --- a/src/main/java/com/campus/water/controller/AlertController.java +++ b/src/main/java/com/campus/water/controller/AlertController.java @@ -64,15 +64,18 @@ public class AlertController { /** * 查询未处理告警(紧急优先) */ + // AlertController.java + @GetMapping("/pending") - @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')") + @PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN','ROLE_AREA_ADMIN', 'ROLE_REPAIRMAN')") // 添加 ROLE_ 前缀 public ResultVO> getPendingAlerts( @Parameter(description = "区域ID(可选)") @RequestParam(required = false) String areaId) { + List pendingAlerts = areaId != null ? alertRepository.findByAreaIdAndStatus(areaId, Alert.AlertStatus.pending) : alertRepository.findByStatus(Alert.AlertStatus.pending); - // 按优先级排序(紧急在前)- 使用方法引用替代lambda + // 按优先级排序(紧急在前) pendingAlerts.sort((a1, a2) -> Integer.compare(a2.getAlertLevel().getPriority(), a1.getAlertLevel().getPriority())); diff --git a/src/main/resources/web/src/views/Dashboard.vue b/src/main/resources/web/src/views/Dashboard.vue index 0aac9be..341fae9 100644 --- a/src/main/resources/web/src/views/Dashboard.vue +++ b/src/main/resources/web/src/views/Dashboard.vue @@ -36,12 +36,13 @@ -
+
⚠️
设备 {{ latestAlert.deviceId }} 异常,请关注! +
{{ latestAlert.alertMessage }}
-
×
+
×
@@ -49,10 +50,38 @@

最新告警

-
-
-
{{ alert.deviceId }}:{{ alert.message }}
-
{{ formatAlertLevel(alert.level) }}
+ + +
+
+
加载告警数据中...
+
+ + +
+
🔒
+
+
权限受限
+
当前用户无告警查看权限
+
+
+ + +
+
+
暂无告警信息
+
+ + +
+
+
{{ alert.deviceId }}:{{ alert.alertMessage }}
+
+
+ {{ formatAlertLevel(alert.alertLevel) }} +
+
{{ alert.areaId }}
+
@@ -62,7 +91,7 @@

工单状态统计

-
Axhub Charts
+
待处理工单数: {{ stats.pendingWorkOrders }}
柱状图
通过Group内data和config中继器可更改数据及配置 @@ -84,7 +113,7 @@ import { request } from '@/api/request' import { useAuthStore } from '@/stores/auth' import type { ResultVO } from '@/api/types/auth' -// 定义数据结构 +// 定义数据结构 - 修正为与后端实体匹配 interface StatsData { totalDevices: number onlineDevices: number @@ -93,12 +122,18 @@ interface StatsData { pendingWorkOrders: number } +// 修正:与后端 Alert 实体字段保持一致 interface Alert { - id: string + alertId: number deviceId: string - message: string - level: string + alertType: string + alertLevel: string // 'info' | 'warning' | 'error' | 'critical' + alertMessage: string + areaId: string + status: string // 'pending' | 'processing' | 'resolved' | 'closed' timestamp: string + resolvedTime?: string + resolvedBy?: string } // 响应式数据 @@ -112,7 +147,9 @@ const stats = ref({ const recentAlerts = ref([]) const latestAlert = ref(null) -const onlinePercentage = ref(0) +const loadingAlerts = ref(false) +const showAlertNotification = ref(true) +const alertPermissionError = ref(false) // 获取路由和认证store实例 const router = useRouter() @@ -121,11 +158,12 @@ const authStore = useAuthStore() // 格式化告警级别 const formatAlertLevel = (level: string): string => { const levelMap: Record = { - 'CRITICAL': '紧急', - 'ERROR': '错误', - 'WARNING': '警告' + 'critical': '紧急', + 'error': '错误', + 'warning': '警告', + 'info': '信息' } - return levelMap[level] || level + return levelMap[level?.toLowerCase()] || level } // 获取统计数据 @@ -142,8 +180,8 @@ const fetchStatsData = async () => { // 获取设备状态统计 const statusCountResult = await request>>( - '/api/web/device-status/status-count', - { method: 'GET' } + '/api/web/device-status/status-count', + { method: 'GET' } ) if (statusCountResult.code === 200 && statusCountResult.data) { @@ -152,19 +190,12 @@ const fetchStatsData = async () => { stats.value.offlineDevices = data.offline || 0 stats.value.alertDevices = data.fault || 0 stats.value.totalDevices = (data.online || 0) + (data.offline || 0) + (data.fault || 0) - - // 计算在线设备百分比 - if (stats.value.totalDevices > 0) { - onlinePercentage.value = Math.round((stats.value.onlineDevices / stats.value.totalDevices) * 100) - } else { - onlinePercentage.value = 0 - } } // 获取待处理工单数 const workOrderResult = await request>( - '/api/work-orders/by-status?status=pending', - { method: 'GET' } + '/api/work-orders/by-status?status=pending', + { method: 'GET' } ) if (workOrderResult.code === 200 && workOrderResult.data) { @@ -189,6 +220,9 @@ const fetchStatsData = async () => { // 获取告警数据 const fetchAlertData = async () => { + loadingAlerts.value = true + alertPermissionError.value = false + try { const token = authStore.token @@ -199,17 +233,37 @@ const fetchAlertData = async () => { return } - // 获取最新告警 + // 获取最新告警 - 使用正确的接口 const alertResult = await request>( - '/api/alerts/pending', - { method: 'GET' } + '/api/alerts/pending', + { method: 'GET' } ) + console.log('告警接口返回:', alertResult) + if (alertResult.code === 200 && alertResult.data) { - recentAlerts.value = alertResult.data.slice(0, 5) // 只取前5条 + // 确保数据是数组 + const alerts = Array.isArray(alertResult.data) ? alertResult.data : [] + + // 只取前5条 + recentAlerts.value = alerts.slice(0, 5) + + // 如果有告警,设置最新告警 if (recentAlerts.value.length > 0) { - latestAlert.value = recentAlerts.value[0] || null; - } + const sortedAlerts = [...recentAlerts.value].sort((a, b) => { + const priorityMap: Record = { + 'critical': 4, + 'error': 3, + 'warning': 2, + 'info': 1 + }; + return (priorityMap[b.alertLevel?.toLowerCase()] || 0) - (priorityMap[a.alertLevel?.toLowerCase()] || 0); + }); + latestAlert.value = sortedAlerts[0] ?? null; // 明确处理 undefined 情况 +} else { + latestAlert.value = null; +} + } } catch (error: any) { console.error('获取告警数据失败:', error) @@ -217,7 +271,7 @@ const fetchAlertData = async () => { // 特别处理403权限错误 if (error.message?.includes('403')) { console.warn('当前用户无权限访问告警数据') - // 清空告警数据但不显示错误提示 + alertPermissionError.value = true recentAlerts.value = [] latestAlert.value = null return @@ -234,10 +288,11 @@ const fetchAlertData = async () => { authStore.logout() router.push('/login') } + } finally { + loadingAlerts.value = false } } - // 组件挂载时获取数据 onMounted(() => { fetchStatsData() @@ -325,10 +380,21 @@ onMounted(() => { color: #333; } +.alert-detail { + font-size: 12px; + color: #666; + margin-top: 4px; +} + .alert-close { font-size: 20px; cursor: pointer; color: #999; + padding: 0 8px; +} + +.alert-close:hover { + color: #666; } /* 修改为单列布局 */ @@ -341,7 +407,7 @@ onMounted(() => { .content-card { background: white; border-radius: 8px; - padding: 20px; + padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } @@ -349,25 +415,116 @@ onMounted(() => { font-size: 18px; font-weight: 600; color: #1a1a1a; - margin-bottom: 16px; - text-align: center; /* 标题居中 */ + margin-bottom: 20px; + text-align: center; } -.alert-list { +/* 加载状态 */ +.loading-state { display: flex; flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; } -.alert-item { +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #f3f3f3; + border-top: 3px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 14px; + color: #666; +} + +/* 权限错误提示 */ +.permission-error { display: flex; - justify-content: space-between; align-items: center; - padding: 12px 0; + padding: 20px; + background: #f8f9fa; + border-radius: 6px; + margin: 10px 0; +} + +.error-icon { + font-size: 24px; + margin-right: 12px; + color: #6c757d; +} + +.error-content { + flex: 1; +} + +.error-title { + font-size: 14px; + font-weight: 600; + color: #495057; + margin-bottom: 4px; +} + +.error-text { + font-size: 12px; + color: #6c757d; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.empty-icon { + font-size: 40px; + margin-bottom: 12px; +} + +.empty-text { + font-size: 14px; + color: #666; +} + +/* 告警列表样式 */ +.alert-list { + display: flex; + flex-direction: column; +} + +.alert-item { + padding: 16px 0; + border-bottom: 1px solid #f0f0f0; +} + +.alert-item:last-child { + border-bottom: none; } .alert-text { font-size: 14px; color: #333; + margin-bottom: 8px; + line-height: 1.4; +} + +.alert-meta { + display: flex; + justify-content: space-between; + align-items: center; } .alert-level { @@ -375,6 +532,8 @@ onMounted(() => { padding: 4px 8px; border-radius: 4px; font-weight: 500; + min-width: 50px; + text-align: center; } .alert-level.critical { @@ -392,9 +551,17 @@ onMounted(() => { color: #ff8f00; } -.alert-divider { - height: 1px; - background: #f0f0f0; +.alert-level.info { + background: #e8f5e9; + color: #2e7d32; +} + +.alert-area { + font-size: 12px; + color: #666; + background: #f5f5f5; + padding: 4px 8px; + border-radius: 4px; } .chart-placeholder { @@ -415,5 +582,11 @@ onMounted(() => { .stats-grid { grid-template-columns: repeat(2, 1fr); } + + .alert-meta { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } } - + \ No newline at end of file -- 2.34.1