diff --git a/apps/web-antd/src/api/aiot/alarm/index.ts b/apps/web-antd/src/api/aiot/alarm/index.ts index 0c32d7ddb..c31650774 100644 --- a/apps/web-antd/src/api/aiot/alarm/index.ts +++ b/apps/web-antd/src/api/aiot/alarm/index.ts @@ -66,12 +66,38 @@ export namespace AiotAlarmApi { export interface AlertStatistics { total?: number; todayCount?: number; + yesterdayCount?: number; pendingCount?: number; handledCount?: number; + avgResponseMinutes?: number | null; byType?: Record; byStatus?: Record; byLevel?: Record; } + + /** 告警趋势项 */ + export interface TrendItem { + date: string; + total: number; + leave_post?: number; + intrusion?: number; + illegal_parking?: number; + vehicle_congestion?: number; + [key: string]: number | string | undefined; + } + + /** 设备告警排行项 */ + export interface DeviceTopItem { + deviceId: string; + deviceName: string; + count: number; + } + + /** 时段分布项 */ + export interface HourDistItem { + hour: number; + count: number; + } } // ==================== 告警管理 API ==================== @@ -115,6 +141,37 @@ export function getAlertStatistics(startTime?: string, endTime?: string) { ); } +/** 获取告警趋势 */ +export function getAlertTrend(days: number = 7) { + return requestClient.get( + '/aiot/alarm/alert/trend', + { params: { days } }, + ); +} + +/** 获取设备告警排行 */ +export function getAlertDeviceTop(limit: number = 10, days: number = 7) { + return requestClient.get( + '/aiot/alarm/alert/device-top', + { params: { limit, days } }, + ); +} + +/** 获取24小时告警分布 */ +export function getAlertHourDistribution(days: number = 7) { + return requestClient.get( + '/aiot/alarm/alert/hour-distribution', + { params: { days } }, + ); +} + +/** 获取最近告警列表 */ +export function getRecentAlerts(pageSize: number = 10) { + return requestClient.get('/aiot/alarm/alert/page', { + params: { pageNo: 1, pageSize }, + }); +} + // ==================== 摄像头告警汇总 API ==================== /** 以摄像头维度获取告警汇总 */ diff --git a/apps/web-antd/src/views/aiot/alarm/summary/chart-options.ts b/apps/web-antd/src/views/aiot/alarm/summary/chart-options.ts new file mode 100644 index 000000000..5f6c80f8b --- /dev/null +++ b/apps/web-antd/src/views/aiot/alarm/summary/chart-options.ts @@ -0,0 +1,201 @@ +import type { AiotAlarmApi } from '#/api/aiot/alarm'; + +const TYPE_NAMES: Record = { + leave_post: '离岗检测', + intrusion: '周界入侵', + illegal_parking: '车辆违停', + vehicle_congestion: '车辆拥堵', +}; + +const TYPE_COLORS: Record = { + leave_post: '#1890ff', + intrusion: '#f5222d', + illegal_parking: '#fa8c16', + vehicle_congestion: '#722ed1', +}; + +const LEVEL_NAMES: Record = { + 0: '紧急', + 1: '重要', + 2: '普通', + 3: '轻微', +}; + +const LEVEL_COLORS: Record = { + 0: '#f5222d', + 1: '#fa8c16', + 2: '#1890ff', + 3: '#8c8c8c', +}; + +/** 告警趋势面积图 */ +export function getTrendChartOptions(data: AiotAlarmApi.TrendItem[]): any { + const dates = data.map((d) => d.date.slice(5)); // MM-DD + const types = Object.keys(TYPE_NAMES); + + return { + tooltip: { + trigger: 'axis', + axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }, + }, + legend: { + data: types.map((t) => TYPE_NAMES[t]), + bottom: 0, + }, + grid: { left: '3%', right: '4%', bottom: '12%', top: '8%', containLabel: true }, + xAxis: { + type: 'category', + boundaryGap: false, + data: dates, + }, + yAxis: { type: 'value', minInterval: 1 }, + series: types.map((type) => ({ + name: TYPE_NAMES[type], + type: 'line', + smooth: true, + stack: 'total', + areaStyle: { opacity: 0.25 }, + emphasis: { focus: 'series' }, + itemStyle: { color: TYPE_COLORS[type] }, + data: data.map((d) => (d[type] as number) || 0), + })), + }; +} + +/** 告警类型环形图 */ +export function getTypePieChartOptions( + byType: Record, +): any { + const data = Object.entries(byType) + .map(([type, count]) => ({ + name: TYPE_NAMES[type] || type, + value: count, + itemStyle: { color: TYPE_COLORS[type] }, + })) + .filter((d) => d.value > 0); + const total = data.reduce((s, d) => s + d.value, 0); + + return { + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + legend: { bottom: 0, left: 'center' }, + graphic: { + type: 'text', + left: 'center', + top: '42%', + style: { + text: `${total}`, + fontSize: 28, + fontWeight: 'bold', + fill: '#333', + textAlign: 'center', + }, + }, + series: [ + { + type: 'pie', + radius: ['45%', '70%'], + center: ['50%', '48%'], + avoidLabelOverlap: false, + label: { show: false }, + data, + }, + ], + }; +} + +/** 设备告警 Top10 横向条形图 */ +export function getDeviceTopChartOptions( + data: AiotAlarmApi.DeviceTopItem[], +): any { + const sorted = [...data].reverse(); // 最多的在上面 + return { + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, + grid: { left: '3%', right: '8%', bottom: '3%', top: '3%', containLabel: true }, + xAxis: { type: 'value', minInterval: 1 }, + yAxis: { + type: 'category', + data: sorted.map((d) => { + const name = d.deviceName || d.deviceId; + return name.length > 12 ? `${name.slice(0, 12)}...` : name; + }), + axisLabel: { fontSize: 11 }, + }, + series: [ + { + type: 'bar', + data: sorted.map((d) => d.count), + itemStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 1, y2: 0, + colorStops: [ + { offset: 0, color: '#1890ff' }, + { offset: 1, color: '#36cfc9' }, + ], + }, + borderRadius: [0, 4, 4, 0], + }, + barMaxWidth: 20, + label: { show: true, position: 'right', fontSize: 11 }, + }, + ], + }; +} + +/** 告警级别分布柱状图 */ +export function getLevelBarChartOptions( + byLevel: Record, +): any { + const levels = [0, 1, 2, 3]; + return { + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, + grid: { left: '3%', right: '4%', bottom: '3%', top: '8%', containLabel: true }, + xAxis: { + type: 'category', + data: levels.map((l) => LEVEL_NAMES[l]), + }, + yAxis: { type: 'value', minInterval: 1 }, + series: [ + { + type: 'bar', + data: levels.map((l) => ({ + value: byLevel[l] || 0, + itemStyle: { color: LEVEL_COLORS[l], borderRadius: [4, 4, 0, 0] }, + })), + barMaxWidth: 40, + label: { show: true, position: 'top', fontSize: 11 }, + }, + ], + }; +} + +/** 24小时时段分布柱状图 */ +export function getHourDistChartOptions( + data: AiotAlarmApi.HourDistItem[], +): any { + const counts = data.map((d) => d.count); + const maxCount = Math.max(...counts, 1); + return { + tooltip: { trigger: 'axis', formatter: '{b}时: {c}次' }, + grid: { left: '3%', right: '4%', bottom: '3%', top: '8%', containLabel: true }, + xAxis: { + type: 'category', + data: data.map((d) => `${d.hour}`), + axisLabel: { formatter: '{value}时', fontSize: 10 }, + }, + yAxis: { type: 'value', minInterval: 1 }, + series: [ + { + type: 'bar', + data: counts.map((c) => ({ + value: c, + itemStyle: { + color: c >= maxCount * 0.8 ? '#f5222d' : c >= maxCount * 0.5 ? '#fa8c16' : '#1890ff', + borderRadius: [3, 3, 0, 0], + }, + })), + barMaxWidth: 16, + }, + ], + }; +} diff --git a/apps/web-antd/src/views/aiot/alarm/summary/index.vue b/apps/web-antd/src/views/aiot/alarm/summary/index.vue index 263ed7865..e3551d44d 100644 --- a/apps/web-antd/src/views/aiot/alarm/summary/index.vue +++ b/apps/web-antd/src/views/aiot/alarm/summary/index.vue @@ -1,83 +1,477 @@ + +