chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '仪表盘',
|
||||
enablePullDownRefresh: true,
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
@use '@/styles/variables.scss' as *;
|
||||
|
||||
.dashboardPage {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
|
||||
padding: $spacing-lg $spacing-lg $spacing-xl;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: $font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.userName {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-white;
|
||||
}
|
||||
|
||||
.settingsBtn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: $radius-round;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
padding: 0 $spacing-lg;
|
||||
margin-top: -40rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.statsGrid > view {
|
||||
width: calc(50% - 12rpx);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: $spacing-lg $spacing-lg 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.sectionAction {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.roomSelector {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
overflow-x: auto;
|
||||
padding-bottom: $spacing-xs;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.roomChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
border-radius: $radius-round;
|
||||
background: $color-bg-card;
|
||||
border: 2rpx solid $color-border;
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
transition: all $transition-base;
|
||||
white-space: nowrap;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.roomChipActive {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
color: $color-text-white;
|
||||
}
|
||||
|
||||
.telemetryGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.telemetryCard {
|
||||
width: calc(50% - 12rpx);
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.telemetryTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.telemetryLabel {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.telemetryIcon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.telemetryValue {
|
||||
font-size: $font-size-xxl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.telemetryUnit {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.miniChart {
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4rpx;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
background: linear-gradient(180deg, $color-primary-light 0%, $color-primary 100%);
|
||||
border-radius: 4rpx 4rpx 0 0;
|
||||
min-height: 8rpx;
|
||||
opacity: 0.7;
|
||||
transition: height $transition-base;
|
||||
}
|
||||
|
||||
.barActive {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loadingWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.errorWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
font-size: 56rpx;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.retryBtn {
|
||||
margin-top: $spacing-md;
|
||||
padding: $spacing-xs $spacing-lg;
|
||||
border-radius: $radius-button;
|
||||
background: $color-primary;
|
||||
color: $color-text-white;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.alarmPreview {
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
box-shadow: $shadow-card;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alarmItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
border-bottom: 2rpx solid $color-divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alarmDot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: $radius-round;
|
||||
margin-right: $spacing-md;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alarmContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.alarmTitle {
|
||||
font-size: $font-size-md;
|
||||
color: $color-text-primary;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
.alarmTime {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-tertiary;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.alarmSeverity {
|
||||
font-size: $font-size-xs;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-xs;
|
||||
margin-left: $spacing-sm;
|
||||
}
|
||||
|
||||
.emptyWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.trendCard {
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
.trendLoading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200rpx;
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useDidHide, usePullDownRefresh } from '@tarojs/taro';
|
||||
import styles from './index.module.scss';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import { getRooms } from '@/api/rooms';
|
||||
import { getDevices } from '@/api/devices';
|
||||
import { getAlarms } from '@/api/alarms';
|
||||
import { getLatestTelemetry, fetchTrend } from '@/api/telemetry';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { wsManager } from '@/utils/ws';
|
||||
import {
|
||||
formatRelativeTime,
|
||||
metricLabel,
|
||||
metricUnit,
|
||||
severityLabel,
|
||||
severityColor,
|
||||
formatNumber,
|
||||
} from '@/utils/format';
|
||||
import type { Room, Device, Alarm, TelemetryRecord, DashboardStats, TrendPoint } from '@/types';
|
||||
import MiniChart from '@/components/MiniChart';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const { user, token } = useStore();
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [alarms, setAlarms] = useState<Alarm[]>([]);
|
||||
const [selectedRoomId, setSelectedRoomId] = useState<string>('');
|
||||
const [telemetry, setTelemetry] = useState<TelemetryRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
roomCount: 0,
|
||||
deviceCount: 0,
|
||||
onlineDeviceCount: 0,
|
||||
alarmCount: 0,
|
||||
openAlarmCount: 0,
|
||||
});
|
||||
const [chartData, setChartData] = useState<number[]>([]);
|
||||
const [trend, setTrend] = useState<TrendPoint[]>([]);
|
||||
const [trendLoading, setTrendLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setError('');
|
||||
const [roomsRes, devicesRes, alarmsRes] = await Promise.all([
|
||||
getRooms().catch(() => [] as Room[]),
|
||||
getDevices().catch(() => [] as Device[]),
|
||||
getAlarms({ openOnly: true }).catch(() => [] as Alarm[]),
|
||||
]);
|
||||
|
||||
setRooms(roomsRes);
|
||||
setDevices(devicesRes);
|
||||
setAlarms(alarmsRes);
|
||||
|
||||
const onlineCount = devicesRes.filter((d) => d.onlineStatus === 'online').length;
|
||||
const openCount = alarmsRes.filter((a) => a.open).length;
|
||||
setStats({
|
||||
roomCount: roomsRes.length,
|
||||
deviceCount: devicesRes.length,
|
||||
onlineDeviceCount: onlineCount,
|
||||
alarmCount: alarmsRes.length,
|
||||
openAlarmCount: openCount,
|
||||
});
|
||||
|
||||
// 默认选中第一个蚕房
|
||||
if (roomsRes.length > 0 && !selectedRoomId) {
|
||||
setSelectedRoomId(roomsRes[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] 数据加载失败:', err);
|
||||
setError(err instanceof Error ? err.message : '数据加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedRoomId]);
|
||||
|
||||
const fetchTelemetry = useCallback(async () => {
|
||||
if (!selectedRoomId) return;
|
||||
const roomDevices = devices.filter((d) => d.roomId === selectedRoomId);
|
||||
if (roomDevices.length === 0) {
|
||||
setTelemetry([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const firstDevice = roomDevices[0];
|
||||
const telemetryRes = await getLatestTelemetry(firstDevice.deviceKey).catch(
|
||||
() => [] as TelemetryRecord[]
|
||||
);
|
||||
setTelemetry(telemetryRes);
|
||||
// 用遥测数据生成迷你图表数据
|
||||
const values = telemetryRes.map((t) => t.value);
|
||||
setChartData(values.length > 0 ? values.slice(-8) : []);
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] 遥测数据加载失败:', err);
|
||||
}
|
||||
}, [selectedRoomId, devices]);
|
||||
|
||||
const loadTrend = useCallback(async () => {
|
||||
setTrendLoading(true);
|
||||
try {
|
||||
const trendData = await fetchTrend(24).catch(() => [] as TrendPoint[]);
|
||||
setTrend(trendData);
|
||||
} finally {
|
||||
setTrendLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTelemetry();
|
||||
}, [fetchTelemetry]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTrend();
|
||||
}, [loadTrend]);
|
||||
|
||||
// 30秒轮询刷新
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
fetchData();
|
||||
fetchTelemetry();
|
||||
loadTrend();
|
||||
}, 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, [fetchData, fetchTelemetry, loadTrend]);
|
||||
|
||||
// WebSocket 实时数据
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
const unsub = wsManager.on('telemetry', (data) => {
|
||||
console.log('[Dashboard] WS telemetry:', data);
|
||||
fetchData();
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [token, fetchData]);
|
||||
|
||||
useDidShow(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
useDidHide(() => {
|
||||
// 页面隐藏时不需要额外操作,定时器在 unmount 时清理
|
||||
});
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
Promise.all([fetchData(), loadTrend()]).then(() => {
|
||||
Taro.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
const handleSettings = () => {
|
||||
Taro.navigateTo({ url: '/pages/settings/index' });
|
||||
};
|
||||
|
||||
const handleRoomTap = (roomId: string) => {
|
||||
setSelectedRoomId(roomId);
|
||||
fetchTelemetry();
|
||||
};
|
||||
|
||||
const handleRoomDetail = (roomId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/rooms/detail/index?id=${roomId}` });
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setLoading(true);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleAlarmTap = () => {
|
||||
Taro.switchTab({ url: '/pages/alerts/index' });
|
||||
};
|
||||
|
||||
const selectedRoom = rooms.find((r) => r.id === selectedRoomId);
|
||||
const recentAlarms = alarms.slice(0, 3);
|
||||
const maxChartValue = Math.max(...chartData, 1);
|
||||
|
||||
const getGreeting = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return '凌晨好';
|
||||
if (hour < 12) return '早上好';
|
||||
if (hour < 14) return '中午好';
|
||||
if (hour < 18) return '下午好';
|
||||
return '晚上好';
|
||||
};
|
||||
|
||||
if (loading && stats.roomCount === 0) {
|
||||
return (
|
||||
<View className={styles.dashboardPage}>
|
||||
<View className={styles.loadingWrap}>
|
||||
<Text className={styles.loadingText}>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && stats.roomCount === 0) {
|
||||
return (
|
||||
<View className={styles.dashboardPage}>
|
||||
<View className={styles.errorWrap}>
|
||||
<Text className={styles.errorIcon}>⚠️</Text>
|
||||
<Text className={styles.errorText}>{error}</Text>
|
||||
<View className={styles.retryBtn} onClick={handleRetry}>
|
||||
<Text style={{ color: '#fff' }}>重试</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.dashboardPage}>
|
||||
<View className={styles.header}>
|
||||
<View className={styles.headerLeft}>
|
||||
<Text className={styles.greeting}>{getGreeting()}</Text>
|
||||
<Text className={styles.userName}>{user?.fullName || user?.username || '管理员'}</Text>
|
||||
</View>
|
||||
<View className={styles.settingsBtn} onClick={handleSettings}>
|
||||
<Text>⚙️</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<View className={styles.statsGrid}>
|
||||
<StatCard
|
||||
label="蚕房数量"
|
||||
value={stats.roomCount}
|
||||
unit="间"
|
||||
icon="🏠"
|
||||
color="#10b981"
|
||||
/>
|
||||
<StatCard
|
||||
label="设备在线"
|
||||
value={`${stats.onlineDeviceCount}/${stats.deviceCount}`}
|
||||
icon="📡"
|
||||
color="#165dff"
|
||||
/>
|
||||
<StatCard
|
||||
label="活跃告警"
|
||||
value={stats.openAlarmCount}
|
||||
unit="条"
|
||||
icon="🔔"
|
||||
color="#f53f3f"
|
||||
trend={stats.openAlarmCount > 0 ? '需处理' : '正常'}
|
||||
trendType={stats.openAlarmCount > 0 ? 'up' : 'down'}
|
||||
onClick={handleAlarmTap}
|
||||
/>
|
||||
<StatCard
|
||||
label="设备总数"
|
||||
value={stats.deviceCount}
|
||||
unit="台"
|
||||
icon="📊"
|
||||
color="#ff7d00"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 24小时温度趋势 */}
|
||||
<View className={styles.section}>
|
||||
<View className={styles.sectionHeader}>
|
||||
<Text className={styles.sectionTitle}>24小时温度趋势</Text>
|
||||
</View>
|
||||
<View className={styles.trendCard}>
|
||||
{trendLoading && trend.length === 0 ? (
|
||||
<View className={styles.trendLoading}>
|
||||
<Text className={styles.loadingText}>趋势数据加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<MiniChart data={trend} metric="temp" color="#f53f3f" height={200} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 蚕房选择器 */}
|
||||
{rooms.length > 0 && (
|
||||
<View className={styles.section}>
|
||||
<View className={styles.sectionHeader}>
|
||||
<Text className={styles.sectionTitle}>实时监测</Text>
|
||||
{selectedRoom && (
|
||||
<Text
|
||||
className={styles.sectionAction}
|
||||
onClick={() => handleRoomDetail(selectedRoom.id)}
|
||||
>
|
||||
详情 >
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<ScrollView scrollX className={styles.roomSelector}>
|
||||
{rooms.map((room) => (
|
||||
<View
|
||||
key={room.id}
|
||||
className={`${styles.roomChip} ${selectedRoomId === room.id ? styles.roomChipActive : ''}`}
|
||||
onClick={() => handleRoomTap(room.id)}
|
||||
>
|
||||
{room.name}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 遥测数据 */}
|
||||
{telemetry.length > 0 ? (
|
||||
<View className={styles.section}>
|
||||
<View className={styles.telemetryGrid}>
|
||||
{telemetry.map((item) => (
|
||||
<View key={`${item.deviceKey}-${item.metric}`} className={styles.telemetryCard}>
|
||||
<View className={styles.telemetryTop}>
|
||||
<Text className={styles.telemetryLabel}>{metricLabel(item.metric)}</Text>
|
||||
<Text className={styles.telemetryIcon}>
|
||||
{item.metric === 'temperature' ? '🌡' : item.metric === 'humidity' ? '💧' : '📈'}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className={styles.telemetryValue}>{formatNumber(item.value)}</Text>
|
||||
<Text className={styles.telemetryUnit}>{metricUnit(item.metric)}</Text>
|
||||
</View>
|
||||
<View className={styles.miniChart}>
|
||||
{chartData.map((v, i) => (
|
||||
<View
|
||||
key={i}
|
||||
className={`${styles.bar} ${i === chartData.length - 1 ? styles.barActive : ''}`}
|
||||
style={{ height: `${Math.max((v / maxChartValue) * 100, 10)}%` }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
!loading && (
|
||||
<View className={styles.section}>
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>📭</Text>
|
||||
<Text className={styles.emptyText}>暂无遥测数据</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* 最近告警 */}
|
||||
<View className={styles.section}>
|
||||
<View className={styles.sectionHeader}>
|
||||
<Text className={styles.sectionTitle}>最近告警</Text>
|
||||
<Text className={styles.sectionAction} onClick={handleAlarmTap}>全部 ></Text>
|
||||
</View>
|
||||
{recentAlarms.length > 0 ? (
|
||||
<View className={styles.alarmPreview}>
|
||||
{recentAlarms.map((alarm) => (
|
||||
<View key={alarm.id} className={styles.alarmItem} onClick={handleAlarmTap}>
|
||||
<View
|
||||
className={styles.alarmDot}
|
||||
style={{ background: severityColor(alarm.severity) }}
|
||||
/>
|
||||
<View className={styles.alarmContent}>
|
||||
<Text className={styles.alarmTitle}>{alarm.title || alarm.message || alarm.code || '告警'}</Text>
|
||||
<Text className={styles.alarmTime}>{formatRelativeTime(alarm.triggeredAt)}</Text>
|
||||
</View>
|
||||
<View
|
||||
className={styles.alarmSeverity}
|
||||
style={{
|
||||
background: `${severityColor(alarm.severity)}20`,
|
||||
color: severityColor(alarm.severity),
|
||||
}}
|
||||
>
|
||||
{severityLabel(alarm.severity)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>✅</Text>
|
||||
<Text className={styles.emptyText}>暂无活跃告警</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
Reference in New Issue
Block a user