1339 lines
32 KiB
Vue
1339 lines
32 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
ref,
|
||
onMounted,
|
||
reactive,
|
||
onBeforeUnmount,
|
||
computed,
|
||
nextTick,
|
||
watch,
|
||
} from 'vue';
|
||
import {
|
||
ExclamationCircleOutlined,
|
||
WarningOutlined,
|
||
InfoCircleOutlined,
|
||
BellOutlined,
|
||
} from '@ant-design/icons-vue';
|
||
import * as echarts from 'echarts';
|
||
import { useRouter } from 'vue-router';
|
||
import { origGet } from '@/api/faultManage/actAlarm';
|
||
|
||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||
import useI18n from '@/hooks/useI18n';
|
||
import { useFullscreen } from '@vueuse/core';
|
||
|
||
const { t } = useI18n();
|
||
|
||
const router = useRouter();
|
||
|
||
const viewportDom = ref<HTMLElement | null>(null);
|
||
const { isFullscreen, toggle } = useFullscreen(viewportDom);
|
||
|
||
const trendRange = ref('1'); // '1':今天,'7':7天,'30':30天
|
||
|
||
// 加载状态
|
||
const loading = ref(false);
|
||
|
||
// 定时器引用
|
||
let refreshTimer: number | null = null;
|
||
|
||
// 最新告警列表
|
||
const allLatestAlarmsAct: any = ref([]);
|
||
|
||
const allLatestAlarmsHis: any = ref([]);
|
||
|
||
const newHistoryAlarms = ref(new Set());
|
||
const newActiveAlarms = ref(new Set());
|
||
|
||
const alarmTypeType = ref<any>([
|
||
{
|
||
value: 0,
|
||
name: t('views.index.Critical'),
|
||
},
|
||
{
|
||
value: 0,
|
||
name: t('views.index.Major'),
|
||
},
|
||
{
|
||
value: 0,
|
||
name: t('views.index.Minor'),
|
||
},
|
||
{
|
||
value: 0,
|
||
name: t('views.index.Warning'),
|
||
},
|
||
// {
|
||
// value: 0,
|
||
// name: t('views.index.Event'),
|
||
// },
|
||
]);
|
||
|
||
// 使用计算属性确保显示固定数量的告警
|
||
const latestAlarmsAct: any = computed(() => {
|
||
return allLatestAlarmsAct.value.slice(0, 10); // 始终显示前5条
|
||
});
|
||
|
||
const latestAlarmsHis: any = computed(() => {
|
||
return allLatestAlarmsHis.value.slice(0, 10); // 始终显示前5条
|
||
});
|
||
|
||
// 告警数据
|
||
const alarmData = reactive({
|
||
critical: 0,
|
||
major: 0,
|
||
minor: 0,
|
||
warning: 0,
|
||
});
|
||
|
||
// 图表引用
|
||
const trendChart = ref(null);
|
||
const typeChart = ref(null);
|
||
|
||
let trendChartInstance: echarts.ECharts | null = null;
|
||
|
||
// 跳转到告警页面
|
||
function fnToRouter(name: string, query?: any) {
|
||
router.push({ name, query });
|
||
}
|
||
|
||
function getFullHourAxis() {
|
||
const hours: string[] = [];
|
||
for (let i = 0; i < 24; i++) {
|
||
hours.push(i.toString().padStart(2, '0') + ':00');
|
||
}
|
||
return hours;
|
||
}
|
||
|
||
function getFullDayAxis(days: number) {
|
||
const daysArr: string[] = [];
|
||
const today = new Date();
|
||
for (let i = days - 1; i >= 0; i--) {
|
||
const d = new Date(today);
|
||
d.setDate(today.getDate() - i);
|
||
daysArr.push(d.toISOString().slice(0, 10));
|
||
}
|
||
return daysArr;
|
||
}
|
||
|
||
function getFull4HourAxis() {
|
||
const hours: string[] = [];
|
||
for (let i = 0; i < 24; i += 4) {
|
||
hours.push(i.toString().padStart(2, '0') + ':00');
|
||
}
|
||
return hours;
|
||
}
|
||
|
||
function fillTrendData(rawData: any[], axis: string[]) {
|
||
const map = new Map(rawData.map(item => [item.time, item]));
|
||
return axis.map(time => ({
|
||
time,
|
||
Critical: map.get(time)?.Critical ?? 0,
|
||
Major: map.get(time)?.Major ?? 0,
|
||
Minor: map.get(time)?.Minor ?? 0,
|
||
Warning: map.get(time)?.Warning ?? 0,
|
||
}));
|
||
}
|
||
|
||
const isFirstLoad = ref(true);
|
||
|
||
// 修改获取最新活动告警的方法
|
||
async function fetchLatestAlarmsAct() {
|
||
try {
|
||
const res = { code: 0, rows: [] }; // await alarmDashGetAct();
|
||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
|
||
// 只有非首次加载才检查新告警
|
||
if (!isFirstLoad.value) {
|
||
// 获取当前告警ID集合
|
||
const currentIds = new Set(
|
||
allLatestAlarmsAct.value.map((alarm: any) => alarm.alarmId)
|
||
);
|
||
console.log('currentIds', currentIds);
|
||
|
||
// 找出新告警
|
||
res.rows.forEach((alarm: any) => {
|
||
if (!currentIds.has(alarm.alarmId)) {
|
||
console.log('新活动告警:', alarm.alarmId, alarm.alarmTitle);
|
||
newActiveAlarms.value.add(alarm.alarmId);
|
||
// 3秒后移除新告警标记
|
||
setTimeout(() => {
|
||
newActiveAlarms.value.delete(alarm.alarmId);
|
||
}, 3000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新告警列表
|
||
allLatestAlarmsAct.value = res.rows;
|
||
}
|
||
} catch (error) {
|
||
console.error('Get Act-Data List error:', error);
|
||
}
|
||
}
|
||
|
||
async function fetchLatestAlarmsHis() {
|
||
try {
|
||
const res = { code: 0, rows: [] }; //await alarmDashGetHis();
|
||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
|
||
// 只有非首次加载才检查新告警
|
||
if (!isFirstLoad.value) {
|
||
// 获取当前告警ID集合
|
||
const currentIds = new Set(
|
||
allLatestAlarmsHis.value.map((alarm: any) => alarm.alarmId)
|
||
);
|
||
// 找出新告警
|
||
res.rows.forEach((alarm:any) => {
|
||
if (!currentIds.has(alarm.alarmId)) {
|
||
console.log('新历史告警:', alarm.alarmId, alarm.alarmTitle);
|
||
newHistoryAlarms.value.add(alarm.alarmId);
|
||
// 3秒后移除新告警标记
|
||
setTimeout(() => {
|
||
newHistoryAlarms.value.delete(alarm.alarmId);
|
||
}, 3000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新告警列表
|
||
allLatestAlarmsHis.value = res.rows;
|
||
}
|
||
} catch (error) {
|
||
console.error('Get His-Data List error:', error);
|
||
}
|
||
}
|
||
|
||
// 获取告警数据
|
||
async function fetchAlarmData() {
|
||
try {
|
||
loading.value = true;
|
||
const res = await origGet();
|
||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||
// 重置告警数据
|
||
alarmData.critical = 0;
|
||
alarmData.major = 0;
|
||
alarmData.minor = 0;
|
||
alarmData.warning = 0;
|
||
console.log('Get Data Success:', res.data);
|
||
// 解析告警数据
|
||
res.data.forEach((item: any) => {
|
||
if (item.severity === 'Critical') {
|
||
alarmData.critical = item.total;
|
||
} else if (item.severity === 'Major') {
|
||
alarmData.major = item.total;
|
||
} else if (item.severity === 'Minor') {
|
||
alarmData.minor = item.total;
|
||
} else if (item.severity === 'Warning') {
|
||
alarmData.warning = item.total;
|
||
}
|
||
});
|
||
|
||
await fetchLatestAlarmsAct();
|
||
await fetchLatestAlarmsHis();
|
||
|
||
if (isFirstLoad.value) {
|
||
isFirstLoad.value = false;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Get Data Er r:', error);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
const trendData: any = ref([]);
|
||
|
||
async function fetchAlarmTrend() {
|
||
try {
|
||
const res = { code: 0, rows: [] }; //await getAlarmTrend({ days: trendRange.value }); // 你需要让后端支持days参数
|
||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
|
||
trendData.value = res.rows;
|
||
updateTrendChart();
|
||
}
|
||
} catch (e) {
|
||
console.error('Get Trend Data Error:', e);
|
||
}
|
||
}
|
||
|
||
function updateTrendChart() {
|
||
if (!trendChartInstance) return;
|
||
|
||
let xAxisData: string[];
|
||
if (trendRange.value === '1') {
|
||
xAxisData = getFullHourAxis();
|
||
} else {
|
||
xAxisData = getFullDayAxis(Number(trendRange.value));
|
||
}
|
||
const filledData = fillTrendData(trendData.value, xAxisData);
|
||
|
||
const critical = filledData.map(item => item.Critical);
|
||
const major = filledData.map(item => item.Major);
|
||
const minor = filledData.map(item => item.Minor);
|
||
const warning = filledData.map(item => item.Warning);
|
||
|
||
trendChartInstance.setOption({
|
||
xAxis: {
|
||
data: xAxisData,
|
||
axisLabel: {
|
||
color: '#00fcff',
|
||
interval: xAxisData.length > 12 ? 2 : 0, // 超过12个点就隔2个显示
|
||
// rotate: 45 // 如果你想斜着显示
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
name: t('views.index.Critical'),
|
||
type: 'line',
|
||
data: critical,
|
||
showSymbol: true,
|
||
itemStyle: { color: '#f5222d' },
|
||
},
|
||
{
|
||
name: t('views.index.Major'),
|
||
type: 'line',
|
||
data: major,
|
||
showSymbol: true,
|
||
itemStyle: { color: '#fa8c16' },
|
||
},
|
||
{
|
||
name: t('views.index.Minor'),
|
||
type: 'line',
|
||
data: minor,
|
||
showSymbol: true,
|
||
itemStyle: { color: '#faad14' },
|
||
},
|
||
{
|
||
name: t('views.index.Warning'),
|
||
type: 'line',
|
||
data: warning,
|
||
showSymbol: true,
|
||
itemStyle: { color: '#52c41a' },
|
||
},
|
||
],
|
||
});
|
||
}
|
||
|
||
watch(trendRange, () => {
|
||
fetchAlarmTrend();
|
||
});
|
||
|
||
// 初始化图表
|
||
onMounted(() => {
|
||
fetchAlarmData();
|
||
fetchAlarmTrend(); // 定时刷新趋势数据
|
||
|
||
// 设置定时刷新 - 每10秒刷新一次
|
||
refreshTimer = window.setInterval(() => {
|
||
fetchAlarmData();
|
||
fetchAlarmTrend(); // 定时刷新趋势数据
|
||
}, 10000);
|
||
|
||
// 初始化告警趋势图
|
||
nextTick(() => {
|
||
trendChartInstance = echarts.init(trendChart.value);
|
||
const trendOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'line',
|
||
},
|
||
},
|
||
legend: {
|
||
data: alarmTypeType.value.map((item: any) => item.name),
|
||
textStyle: {
|
||
color: '#00fcff',
|
||
},
|
||
right: 10,
|
||
top: 10,
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
top: '15%',
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#0a73ff',
|
||
},
|
||
},
|
||
axisLabel: {
|
||
color: '#00fcff',
|
||
},
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
min: 0, // 设置最小值为0
|
||
minInterval: 1, // 最小间隔为1,确保刻度是整数
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#0a73ff',
|
||
},
|
||
},
|
||
axisLabel: {
|
||
color: '#00fcff',
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(10, 115, 255, 0.2)',
|
||
},
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
name: t('views.index.Critical'),
|
||
type: 'line',
|
||
emphasis: {
|
||
focus: 'series',
|
||
},
|
||
data: [0, 0, 0, 0, 0, 0],
|
||
itemStyle: {
|
||
color: '#f5222d',
|
||
},
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
sampling: 'lttb',
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(250,140,22,0.4)' },
|
||
{ offset: 1, color: 'rgba(250,140,22,0.05)' },
|
||
]),
|
||
},
|
||
},
|
||
{
|
||
name: t('views.index.Major'),
|
||
type: 'line',
|
||
emphasis: {
|
||
focus: 'series',
|
||
},
|
||
data: [0, 0, 0, 0, 0, 0],
|
||
itemStyle: {
|
||
color: '#fa8c16',
|
||
},
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
sampling: 'lttb',
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(250,140,22,0.4)' },
|
||
{ offset: 1, color: 'rgba(250,140,22,0.05)' },
|
||
]),
|
||
},
|
||
},
|
||
{
|
||
name: t('views.index.Minor'),
|
||
type: 'line',
|
||
emphasis: {
|
||
focus: 'series',
|
||
},
|
||
data: [0, 0, 0, 0, 0, 0],
|
||
itemStyle: {
|
||
color: '#faad14',
|
||
},
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
sampling: 'lttb',
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(250,173,20,0.4)' },
|
||
{ offset: 1, color: 'rgba(250,173,20,0.05)' },
|
||
]),
|
||
},
|
||
},
|
||
{
|
||
name: t('views.index.Warning'),
|
||
type: 'line',
|
||
emphasis: {
|
||
focus: 'series',
|
||
},
|
||
data: [0, 0, 0, 0, 0, 0],
|
||
itemStyle: {
|
||
color: '#52c41a',
|
||
},
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
sampling: 'lttb',
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(82,196,26,0.4)' },
|
||
{ offset: 1, color: 'rgba(82,196,26,0.05)' },
|
||
]),
|
||
},
|
||
},
|
||
],
|
||
};
|
||
trendChartInstance.setOption(trendOption);
|
||
trendChartInstance.resize();
|
||
});
|
||
|
||
// 响应式调整
|
||
window.addEventListener('resize', handleResize);
|
||
});
|
||
|
||
// 处理窗口大小变化
|
||
function handleResize() {
|
||
trendChartInstance?.resize();
|
||
}
|
||
|
||
// 组件销毁前清理
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', handleResize);
|
||
|
||
// 清除定时器
|
||
if (refreshTimer !== null) {
|
||
clearInterval(refreshTimer);
|
||
refreshTimer = null;
|
||
}
|
||
|
||
// 销毁图表实例
|
||
trendChartInstance?.dispose();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="viewport" ref="viewportDom">
|
||
<div class="alarm-dashboard">
|
||
<div class="dashboard-header">
|
||
<div
|
||
class="header-title"
|
||
@click="toggle"
|
||
:title="t('views.dashboard.overview.fullscreen')"
|
||
>
|
||
告警监控大屏
|
||
<FullscreenExitOutlined v-if="isFullscreen" />
|
||
<FullscreenOutlined v-else />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dashboard-content">
|
||
<!-- 告警概览卡片 -->
|
||
<div class="alarm-overview">
|
||
<div class="alarm-card Critical">
|
||
<div class="card-border-top"></div>
|
||
<div class="card-border-right"></div>
|
||
<div class="card-border-bottom"></div>
|
||
<div class="card-border-left"></div>
|
||
<div class="alarm-icon">
|
||
<ExclamationCircleOutlined />
|
||
</div>
|
||
<div class="alarm-info">
|
||
<div class="alarm-count">{{ alarmData.critical || 0 }}</div>
|
||
<div class="alarm-title">{{ t('views.index.Critical') }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="alarm-card Major">
|
||
<div class="card-border-top"></div>
|
||
<div class="card-border-right"></div>
|
||
<div class="card-border-bottom"></div>
|
||
<div class="card-border-left"></div>
|
||
<div class="alarm-icon">
|
||
<WarningOutlined />
|
||
</div>
|
||
<div class="alarm-info">
|
||
<div class="alarm-count">{{ alarmData.major || 0 }}</div>
|
||
<div class="alarm-title">{{ t('views.index.Major') }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="alarm-card Minor">
|
||
<div class="card-border-top"></div>
|
||
<div class="card-border-right"></div>
|
||
<div class="card-border-bottom"></div>
|
||
<div class="card-border-left"></div>
|
||
<div class="alarm-icon">
|
||
<InfoCircleOutlined />
|
||
</div>
|
||
<div class="alarm-info">
|
||
<div class="alarm-count">{{ alarmData.minor || 0 }}</div>
|
||
<div class="alarm-title">{{ t('views.index.Minor') }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="alarm-card Warning">
|
||
<div class="card-border-top"></div>
|
||
<div class="card-border-right"></div>
|
||
<div class="card-border-bottom"></div>
|
||
<div class="card-border-left"></div>
|
||
<div class="alarm-icon">
|
||
<BellOutlined />
|
||
</div>
|
||
<div class="alarm-info">
|
||
<div class="alarm-count">{{ alarmData.warning || 0 }}</div>
|
||
<div class="alarm-title">{{ t('views.index.Warning') }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dashboard-main">
|
||
<div class="main-left">
|
||
<!-- 告警趋势图 -->
|
||
<div class="alarm-trend panel">
|
||
<div class="panel-header">
|
||
<div class="panel-title">历史告警趋势分析</div>
|
||
<a-radio-group
|
||
v-model:value="trendRange"
|
||
size="small"
|
||
class="trend-range-radio"
|
||
>
|
||
<a-radio-button value="1">今天</a-radio-button>
|
||
<a-radio-button value="3">3天</a-radio-button>
|
||
<a-radio-button value="7">7天</a-radio-button>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="panel-content">
|
||
<div ref="trendChart" class="trend-chart"></div>
|
||
</div>
|
||
<div class="panel-corner panel-corner-lt"></div>
|
||
<div class="panel-corner panel-corner-rt"></div>
|
||
<div class="panel-corner panel-corner-lb"></div>
|
||
<div class="panel-corner panel-corner-rb"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-right">
|
||
<!-- 告警分布图 -->
|
||
<div class="alarm-distribution">
|
||
<div class="distribution-right panel">
|
||
<div class="panel-header">
|
||
<div class="panel-title">最新活动告警</div>
|
||
<div
|
||
class="header-more"
|
||
@click="fnToRouter('HistoryAlarm_2097')"
|
||
>
|
||
更多
|
||
</div>
|
||
</div>
|
||
<div class="panel-content">
|
||
<div class="alarm-list">
|
||
<transition-group name="alarm-fade">
|
||
<div
|
||
v-for="(item, index) in latestAlarmsAct"
|
||
:key="item.alarmId || index"
|
||
class="alarm-item"
|
||
:class="[
|
||
item.origSeverity,
|
||
{ 'new-alarm': newActiveAlarms.has(item.alarmId) },
|
||
]"
|
||
>
|
||
<div class="alarm-time">{{ item.eventTime }}</div>
|
||
<a-tooltip
|
||
style="
|
||
max-width: 300px;
|
||
overflow: hidden;
|
||
display: block;
|
||
"
|
||
:title="item.neType + '-' + item.alarmTitle"
|
||
placement="top"
|
||
>
|
||
<div class="alarm-content">
|
||
{{ item.neType + '-' + item.alarmTitle }}
|
||
</div>
|
||
</a-tooltip>
|
||
<div class="alarm-level">{{ item.origSeverity }}</div>
|
||
</div>
|
||
</transition-group>
|
||
</div>
|
||
</div>
|
||
<div class="panel-corner panel-corner-lt"></div>
|
||
<div class="panel-corner panel-corner-rt"></div>
|
||
<div class="panel-corner panel-corner-lb"></div>
|
||
<div class="panel-corner panel-corner-rb"></div>
|
||
</div>
|
||
|
||
<div class="distribution-right panel">
|
||
<div class="panel-header">
|
||
<div class="panel-title">最新历史告警</div>
|
||
<div
|
||
class="header-more"
|
||
@click="fnToRouter('HistoryAlarm_2097')"
|
||
>
|
||
更多
|
||
</div>
|
||
</div>
|
||
<div class="panel-content">
|
||
<div class="alarm-list">
|
||
<transition-group name="alarm-fade">
|
||
<div
|
||
v-for="(item, index) in latestAlarmsHis"
|
||
:key="item.alarmId || index"
|
||
class="alarm-item"
|
||
:class="[
|
||
item.origSeverity,
|
||
{ 'new-alarm': newHistoryAlarms.has(item.alarmId) },
|
||
]"
|
||
>
|
||
<div class="alarm-time">{{ item.eventTime }}</div>
|
||
<a-tooltip
|
||
style="
|
||
max-width: 300px;
|
||
overflow: hidden;
|
||
display: block;
|
||
"
|
||
:title="item.neType + '-' + item.alarmTitle"
|
||
placement="top"
|
||
>
|
||
<div class="alarm-content">
|
||
{{ item.neType + '-' + item.alarmTitle }}
|
||
</div>
|
||
</a-tooltip>
|
||
<div class="alarm-level">{{ item.origSeverity }}</div>
|
||
</div>
|
||
</transition-group>
|
||
</div>
|
||
</div>
|
||
<div class="panel-corner panel-corner-lt"></div>
|
||
<div class="panel-corner panel-corner-rt"></div>
|
||
<div class="panel-corner panel-corner-lb"></div>
|
||
<div class="panel-corner panel-corner-rb"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="less">
|
||
@keyframes borderFlash {
|
||
0% {
|
||
box-shadow: 0 0 5px #0a73ff;
|
||
}
|
||
|
||
50% {
|
||
box-shadow: 0 0 15px #0a73ff;
|
||
}
|
||
|
||
100% {
|
||
box-shadow: 0 0 5px #0a73ff;
|
||
}
|
||
}
|
||
|
||
@keyframes rotate {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.alarm-dashboard {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
color: #00fcff;
|
||
background-image: url(./images/bg.png);
|
||
background-size: cover;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 10, 50, 0.8);
|
||
z-index: 0;
|
||
}
|
||
}
|
||
|
||
.dashboard-header {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: 60px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
.header-title {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #00fcff;
|
||
text-shadow: 0 0 10px rgba(0, 252, 255, 0.5);
|
||
position: relative;
|
||
|
||
&::before,
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
width: 100px;
|
||
height: 2px;
|
||
background: linear-gradient(to right, transparent, #0a73ff, transparent);
|
||
}
|
||
|
||
&::before {
|
||
right: 120%;
|
||
}
|
||
|
||
&::after {
|
||
left: 120%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.dashboard-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px 20px;
|
||
}
|
||
|
||
.alarm-overview {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.alarm-card {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
background-color: rgba(0, 26, 87, 0.7);
|
||
box-shadow: 0 0 10px rgba(10, 115, 255, 0.3);
|
||
transition: all 0.3s;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.card-border-top,
|
||
.card-border-right,
|
||
.card-border-bottom,
|
||
.card-border-left {
|
||
position: absolute;
|
||
background-color: #0a73ff;
|
||
}
|
||
|
||
.card-border-top,
|
||
.card-border-bottom {
|
||
height: 2px;
|
||
width: 30%;
|
||
}
|
||
|
||
.card-border-left,
|
||
.card-border-right {
|
||
width: 2px;
|
||
height: 30%;
|
||
}
|
||
|
||
.card-border-top {
|
||
top: 0;
|
||
left: 0;
|
||
}
|
||
|
||
.card-border-right {
|
||
top: 0;
|
||
right: 0;
|
||
}
|
||
|
||
.card-border-bottom {
|
||
bottom: 0;
|
||
right: 0;
|
||
}
|
||
|
||
.card-border-left {
|
||
bottom: 0;
|
||
left: 0;
|
||
}
|
||
|
||
&:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 0 15px rgba(10, 115, 255, 0.5);
|
||
|
||
.card-border-top,
|
||
.card-border-right,
|
||
.card-border-bottom,
|
||
.card-border-left {
|
||
background-color: #00fcff;
|
||
}
|
||
}
|
||
|
||
.alarm-icon {
|
||
font-size: 28px;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.alarm-info {
|
||
.alarm-count {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.alarm-title {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
&.Critical {
|
||
.alarm-icon {
|
||
color: #f5222d;
|
||
}
|
||
}
|
||
|
||
&.Major {
|
||
.alarm-icon {
|
||
color: #fa8c16;
|
||
}
|
||
}
|
||
|
||
&.Minor {
|
||
.alarm-icon {
|
||
color: #faad14;
|
||
}
|
||
}
|
||
|
||
&.Warning {
|
||
.alarm-icon {
|
||
color: #52c41a;
|
||
}
|
||
}
|
||
}
|
||
|
||
.dashboard-main {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
|
||
.main-left {
|
||
flex: 2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.main-right {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.panel {
|
||
background-color: rgba(0, 26, 87, 0.7);
|
||
border: 1px solid rgba(10, 115, 255, 0.3);
|
||
border-radius: 4px;
|
||
box-shadow: 0 0 10px rgba(10, 115, 255, 0.3);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
margin-bottom: 20px;
|
||
|
||
.panel-corner {
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-color: #0a73ff;
|
||
|
||
&.panel-corner-lt {
|
||
top: -1px;
|
||
left: -1px;
|
||
border-top: 2px solid;
|
||
border-left: 2px solid;
|
||
}
|
||
|
||
&.panel-corner-rt {
|
||
top: -1px;
|
||
right: -1px;
|
||
border-top: 2px solid;
|
||
border-right: 2px solid;
|
||
}
|
||
|
||
&.panel-corner-lb {
|
||
bottom: -1px;
|
||
left: -1px;
|
||
border-bottom: 2px solid;
|
||
border-left: 2px solid;
|
||
}
|
||
|
||
&.panel-corner-rb {
|
||
bottom: -1px;
|
||
right: -1px;
|
||
border-bottom: 2px solid;
|
||
border-right: 2px solid;
|
||
}
|
||
}
|
||
|
||
.panel-header {
|
||
height: 40px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 15px;
|
||
border-bottom: 1px solid rgba(10, 115, 255, 0.3);
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #00fcff;
|
||
position: relative;
|
||
padding-left: 15px;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 4px;
|
||
height: 16px;
|
||
background-color: #0a73ff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.panel-content {
|
||
flex: 1;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
padding: 15px;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
.alarm-trend {
|
||
flex: 1;
|
||
min-height: 300px;
|
||
|
||
.trend-chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.alarm-distribution {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
|
||
.distribution-left {
|
||
flex: 1;
|
||
min-height: 300px;
|
||
|
||
.type-chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.distribution-right {
|
||
flex: 1;
|
||
min-height: 300px;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
|
||
.header-more {
|
||
color: #00fcff;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.alarm-list {
|
||
height: calc(100% - 40px); // 固定高度,减去标题高度
|
||
max-height: 300px; // 设置最大高度
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow-y: auto;
|
||
overflow-x: hidden; // 防止水平滚动
|
||
padding-right: 6px; // 为滚动条预留空间
|
||
margin-right: -2px; // 补偿滚动条空间
|
||
position: relative; // 确保滚动条位置固定
|
||
|
||
/* 始终显示滚动条,防止出现/消失导致的抖动 */
|
||
&::-webkit-scrollbar {
|
||
width: 4px;
|
||
background-color: rgba(0, 26, 87, 0.3);
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background-color: rgba(10, 115, 255, 0.5);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background-color: rgba(0, 26, 87, 0.3);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.alarm-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center; // 新增这一行
|
||
padding: 0 10px 0 0; // 只保留右侧padding
|
||
border-radius: 2px;
|
||
margin-bottom: 8px;
|
||
background-color: rgba(0, 26, 87, 0.5);
|
||
border-left: 3px solid transparent;
|
||
transition: all 0.3s;
|
||
height: 40px; // 固定每个告警项的高度
|
||
box-sizing: border-box; // 确保padding不会增加元素实际高度
|
||
min-width: 0; //防止flex子项撑开
|
||
|
||
&:last-child {
|
||
margin-bottom: 0; // 最后一项不需要底部间距
|
||
}
|
||
|
||
&:hover {
|
||
background-color: rgba(10, 115, 255, 0.2);
|
||
transform: translateX(3px);
|
||
}
|
||
|
||
&.Critical {
|
||
border-left-color: #f5222d;
|
||
}
|
||
|
||
&.Major {
|
||
border-left-color: #fa8c16;
|
||
}
|
||
|
||
&.Minor {
|
||
border-left-color: #faad14;
|
||
}
|
||
|
||
&.Warning {
|
||
border-left-color: #52c41a;
|
||
}
|
||
|
||
.alarm-time {
|
||
flex: 0 0 100px;
|
||
padding-left: 10px; // 只给时间内容加左间距
|
||
color: #0a73ff;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.alarm-content {
|
||
flex: 1;
|
||
margin: 0 10px;
|
||
min-width: 0;
|
||
max-width: 300px; // 限制最大宽度,和 tooltip max-width 保持一致
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.alarm-level {
|
||
flex: 0 0 50px;
|
||
text-align: right;
|
||
min-width: 40px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.trend-range-radio {
|
||
margin-left: 16px;
|
||
|
||
.ant-radio-button-wrapper {
|
||
background: rgba(0, 26, 87, 0.7);
|
||
color: #00fcff;
|
||
border: 1px solid #0a73ff;
|
||
transition: all 0.2s;
|
||
font-weight: bold;
|
||
|
||
&:not(:last-child) {
|
||
border-right: none;
|
||
}
|
||
|
||
&:hover {
|
||
color: #fff;
|
||
background: #0a73ff;
|
||
border-color: #00fcff;
|
||
}
|
||
}
|
||
|
||
.ant-radio-button-wrapper-checked {
|
||
background: linear-gradient(90deg, #0a73ff 0%, #00fcff 100%);
|
||
color: #001a57;
|
||
border-color: #00fcff !important;
|
||
box-shadow: 0 0 8px #00fcff;
|
||
}
|
||
}
|
||
|
||
/* 确保新告警动画样式足够明显 */
|
||
.alarm-item.new-alarm {
|
||
position: relative;
|
||
z-index: 3;
|
||
animation: newAlarmAppear 1.2s ease-out;
|
||
box-shadow: 0 0 15px rgba(0, 252, 255, 0.7);
|
||
transform: translateX(0) !important;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(10, 115, 255, 0.2),
|
||
rgba(0, 252, 255, 0.3)
|
||
);
|
||
z-index: -1;
|
||
border-radius: 2px;
|
||
animation: gradientShift 3s infinite alternate;
|
||
}
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -1px;
|
||
left: -1px;
|
||
right: -1px;
|
||
bottom: -1px;
|
||
border: 1px solid rgba(0, 252, 255, 0.7);
|
||
border-radius: 3px;
|
||
animation: borderPulse 1.5s infinite;
|
||
z-index: -1;
|
||
}
|
||
|
||
.alarm-content {
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
text-shadow: 0 0 5px rgba(0, 252, 255, 0.7);
|
||
}
|
||
|
||
.alarm-time,
|
||
.alarm-level {
|
||
color: #00fcff;
|
||
text-shadow: 0 0 3px rgba(0, 252, 255, 0.5);
|
||
}
|
||
}
|
||
|
||
/* 告警项目进入动画 */
|
||
.alarm-fade-enter-active {
|
||
transition: all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.alarm-fade-leave-active {
|
||
transition: all 0.5s cubic-bezier(0.36, 0, 0.66, -0.56);
|
||
position: absolute;
|
||
width: 100%;
|
||
}
|
||
|
||
.alarm-fade-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(-30px) scale(0.9);
|
||
}
|
||
|
||
.alarm-fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(30px) scale(0.9);
|
||
}
|
||
|
||
/* 新告警出现动画 */
|
||
@keyframes newAlarmAppear {
|
||
0% {
|
||
transform: translateX(-20px) scale(0.95);
|
||
opacity: 0;
|
||
box-shadow: 0 0 0 rgba(0, 252, 255, 0);
|
||
}
|
||
|
||
30% {
|
||
transform: translateX(0) scale(1.05);
|
||
opacity: 1;
|
||
box-shadow: 0 0 20px rgba(0, 252, 255, 0.8);
|
||
}
|
||
|
||
100% {
|
||
transform: translateX(0) scale(1);
|
||
box-shadow: 0 0 15px rgba(0, 252, 255, 0.7);
|
||
}
|
||
}
|
||
|
||
/* 边框脉冲动画 */
|
||
@keyframes borderPulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(0, 252, 255, 0.7);
|
||
}
|
||
|
||
70% {
|
||
box-shadow: 0 0 0 5px rgba(0, 252, 255, 0);
|
||
}
|
||
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(0, 252, 255, 0);
|
||
}
|
||
}
|
||
|
||
/* 渐变背景动画 */
|
||
@keyframes gradientShift {
|
||
0% {
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(10, 115, 255, 0.2),
|
||
rgba(0, 252, 255, 0.3)
|
||
);
|
||
}
|
||
|
||
100% {
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(0, 252, 255, 0.3),
|
||
rgba(10, 115, 255, 0.2)
|
||
);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.dashboard-main {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.main-left,
|
||
.main-right {
|
||
width: 100%;
|
||
}
|
||
|
||
.alarm-distribution {
|
||
flex-direction: row;
|
||
|
||
.distribution-left,
|
||
.distribution-right {
|
||
min-height: 250px;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.alarm-overview {
|
||
flex-wrap: wrap;
|
||
|
||
.alarm-card {
|
||
flex: 0 0 calc(50% - 8px);
|
||
}
|
||
}
|
||
|
||
.alarm-distribution {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|