最新告警加载 #92

Merged
pc8xi2fbj merged 1 commits from zhanghongwei_branch into develop 4 weeks ago

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

@ -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<List<Alert>> getPendingAlerts(
@Parameter(description = "区域ID可选") @RequestParam(required = false) String areaId) {
List<Alert> 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()));

@ -36,12 +36,13 @@
</div>
<!-- 警告通知 -->
<div v-if="latestAlert" class="alert-warning">
<div v-if="latestAlert && showAlertNotification" class="alert-warning">
<div class="alert-icon"></div>
<div class="alert-content">
<span>设备 {{ latestAlert.deviceId }} 异常请关注</span>
<div class="alert-detail">{{ latestAlert.alertMessage }}</div>
</div>
<div class="alert-close">×</div>
<div class="alert-close" @click="showAlertNotification = false">×</div>
</div>
<!-- 主要内容区域 - 修改为单列布局 -->
@ -49,10 +50,38 @@
<!-- 最新告警 - 居中显示 -->
<div class="content-card">
<h3 class="card-title">最新告警</h3>
<div class="alert-list">
<div v-for="alert in recentAlerts" :key="alert.id" class="alert-item">
<div class="alert-text">{{ alert.deviceId }}{{ alert.message }}</div>
<div :class="['alert-level', alert.level.toLowerCase()]">{{ formatAlertLevel(alert.level) }}</div>
<!-- 加载状态 -->
<div v-if="loadingAlerts" class="loading-state">
<div class="loading-spinner"></div>
<div class="loading-text">加载告警数据中...</div>
</div>
<!-- 权限错误提示 -->
<div v-else-if="alertPermissionError" class="permission-error">
<div class="error-icon">🔒</div>
<div class="error-content">
<div class="error-title">权限受限</div>
<div class="error-text">当前用户无告警查看权限</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="recentAlerts.length === 0" class="empty-state">
<div class="empty-icon"></div>
<div class="empty-text">暂无告警信息</div>
</div>
<!-- 告警列表 -->
<div v-else class="alert-list">
<div v-for="alert in recentAlerts" :key="alert.alertId" class="alert-item">
<div class="alert-text">{{ alert.deviceId }}{{ alert.alertMessage }}</div>
<div class="alert-meta">
<div :class="['alert-level', alert.alertLevel?.toLowerCase()]">
{{ formatAlertLevel(alert.alertLevel) }}
</div>
<div v-if="alert.areaId" class="alert-area">{{ alert.areaId }}</div>
</div>
</div>
</div>
</div>
@ -62,7 +91,7 @@
<h3 class="card-title">工单状态统计</h3>
<div class="chart-placeholder">
<div class="placeholder-text">
<div style="font-size: 16px; margin-bottom: 8px;">Axhub Charts</div>
<div style="font-size: 16px; margin-bottom: 8px;">待处理工单数: {{ stats.pendingWorkOrders }}</div>
<div style="font-size: 24px; margin-bottom: 12px;">柱状图</div>
<div style="color: #666; font-size: 12px;">
通过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<StatsData>({
const recentAlerts = ref<Alert[]>([])
const latestAlert = ref<Alert | null>(null)
const onlinePercentage = ref<number>(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<string, string> = {
'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<ResultVO<Record<string, number>>>(
'/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<ResultVO<any[]>>(
'/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<ResultVO<Alert[]>>(
'/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<string, number> = {
'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;
}
}
</style>
</style>
Loading…
Cancel
Save