Przeglądaj źródła

告警管理重构

learshaw 2 miesięcy temu
rodzic
commit
45405eda95

+ 29 - 64
ems-ui-cloud/src/api/alarm/alarm-info.js

@@ -1,3 +1,4 @@
+import {get} from '@/api/commonApi';
 import request from '@/utils/request';
 
 // 查询能源设施告警列表
@@ -43,76 +44,40 @@ export function delAlarmInfo(id) {
   })
 }
 
-// ===== 统计接口 - 改用 request 方法 =====
+export const fetchAlarmIndex = (params) => {
+  return get('/alarm-info/alarm/type/index', params);
+};
 
-export function fetchAlarmIndex(params) {
-  return request({
-    url: '/ems/alarm-info/alarm/type/index',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchAlarmIndexDay = (params) => {
+  return get('/alarm-info/alarm/type/index/day', params);
+};
 
-export function fetchAlarmIndexDay(params) {
-  return request({
-    url: '/ems/alarm-info/alarm/type/index/day',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchAlarmIndexMonth = (params) => {
+  return get('/alarm-info/alarm/type/index/month', params);
+};
 
-export function fetchAlarmIndexMonth(params) {
-  return request({
-    url: '/ems/alarm-info/alarm/type/index/month',
-    method: 'get',
-    params: params
-  })
-}
 
-export function fetchAlarmIndexYear(params) {
-  return request({
-    url: '/ems/alarm-info/alarm/type/index/year',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchAlarmIndexYear = (params) => {
+  return get('/alarm-info/alarm/type/index/year', params);
+};
 
-export function fetchSubSysIndexDay(params) {
-  return request({
-    url: '/ems/alarm-info/subsys/index/day',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchSubSysIndexDay = (params) => {
+  return get('/alarm-info/subsys/index/day', params);
+};
 
-export function fetchSubSysIndexMonth(params) {
-  return request({
-    url: '/ems/alarm-info/subsys/index/month',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchSubSysIndexMonth = (params) => {
+  return get('/alarm-info/subsys/index/month', params);
+};
 
-export function fetchSubSysIndexYear(params) {
-  return request({
-    url: '/ems/alarm-info/subsys/index/year',
-    method: 'get',
-    params: params
-  })
-}
 
-export function fetchCntHandled(params) {
-  return request({
-    url: '/ems/alarm-info/cnt/handled',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchSubSysIndexYear = (params) => {
+  return get('/alarm-info/subsys/index/year', params);
+};
 
-export function fetchCntDateAlarmType(params) {
-  return request({
-    url: '/ems/alarm-info/cnt/date/alarm/type',
-    method: 'get',
-    params: params
-  })
-}
+export const fetchCntHandled = (params) => {
+  return get('/alarm-info/cnt/handled', params);
+};
+
+export const fetchCntDateAlarmType = (params) => {
+  return get('/alarm-info/cnt/date/alarm/type', params);
+};

+ 135 - 12
ems-ui-cloud/src/api/alarm/alarm.js

@@ -1,6 +1,10 @@
 import request from '@/utils/request'
 
-// 查询能源设施告警策略列表
+/**
+ * 告警记录API
+ */
+
+// 查询告警列表
 export function listAlarm(query) {
   return request({
     url: '/ems/alarm/list',
@@ -9,7 +13,16 @@ export function listAlarm(query) {
   })
 }
 
-// 查询能源设施告警策略详细
+// 查询活动告警
+export function listActiveAlarm(areaCode) {
+  return request({
+    url: '/ems/alarm/active',
+    method: 'get',
+    params: { areaCode }
+  })
+}
+
+// 获取告警详情
 export function getAlarm(id) {
   return request({
     url: '/ems/alarm/' + id,
@@ -17,28 +30,138 @@ export function getAlarm(id) {
   })
 }
 
-// 新增能源设施告警策略
-export function addAlarm(data) {
+// 根据告警ID获取详情
+export function getAlarmByAlarmId(alarmId) {
   return request({
-    url: '/ems/alarm',
+    url: '/ems/alarm/detail/' + alarmId,
+    method: 'get'
+  })
+}
+
+// 手动上报告警
+export function reportAlarm(data) {
+  return request({
+    url: '/ems/alarm/report',
     method: 'post',
     data: data
   })
 }
 
-// 修改能源设施告警策略
-export function updateAlarm(data) {
+// 确认告警
+export function confirmAlarm(alarmId, remark) {
   return request({
-    url: '/ems/alarm',
+    url: '/ems/alarm/confirm/' + alarmId,
     method: 'put',
-    data: data
+    params: { remark }
   })
 }
 
-// 删除能源设施告警策略
-export function delAlarm(id) {
+// 处置告警
+export function handleAlarm(alarmId, handleContent, handleResult) {
   return request({
-    url: '/ems/alarm/' + id,
+    url: '/ems/alarm/handle/' + alarmId,
+    method: 'put',
+    params: { handleContent, handleResult }
+  })
+}
+
+// 解决告警
+export function resolveAlarm(alarmId, resolveRemark) {
+  return request({
+    url: '/ems/alarm/resolve/' + alarmId,
+    method: 'put',
+    params: { resolveRemark }
+  })
+}
+
+// 关闭告警
+export function closeAlarm(alarmId, reason) {
+  return request({
+    url: '/ems/alarm/close/' + alarmId,
+    method: 'put',
+    params: { reason }
+  })
+}
+
+// 批量确认告警
+export function batchConfirmAlarm(alarmIds) {
+  return request({
+    url: '/ems/alarm/batchConfirm',
+    method: 'put',
+    data: alarmIds
+  })
+}
+
+// 删除告警
+export function delAlarm(ids) {
+  return request({
+    url: '/ems/alarm/' + ids,
     method: 'delete'
   })
 }
+
+// 导出告警
+export function exportAlarm(query) {
+  return request({
+    url: '/ems/alarm/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 统计接口 ====================
+
+// 告警统计概览
+export function getAlarmStatsOverview(query) {
+  return request({
+    url: '/ems/alarm/stats/overview',
+    method: 'get',
+    params: query
+  })
+}
+
+// 按级别统计
+export function countAlarmByLevel(query) {
+  return request({
+    url: '/ems/alarm/stats/byLevel',
+    method: 'get',
+    params: query
+  })
+}
+
+// 按状态统计
+export function countAlarmByStatus(query) {
+  return request({
+    url: '/ems/alarm/stats/byStatus',
+    method: 'get',
+    params: query
+  })
+}
+
+// 按时间趋势统计
+export function countAlarmByTrend(query, granularity) {
+  return request({
+    url: '/ems/alarm/stats/trend',
+    method: 'get',
+    params: { ...query, granularity }
+  })
+}
+
+// 按子系统统计
+export function countAlarmBySubsystem(query) {
+  return request({
+    url: '/ems/alarm/stats/bySubsystem',
+    method: 'get',
+    params: query
+  })
+}
+
+// 处理率统计
+export function getAlarmHandleRate(query) {
+  return request({
+    url: '/ems/alarm/stats/handleRate',
+    method: 'get',
+    params: query
+  })
+}

+ 128 - 0
ems-ui-cloud/src/api/alarm/alarmRule.js

@@ -0,0 +1,128 @@
+import request from '@/utils/request'
+
+/**
+ * 告警规则管理API
+ */
+
+// 查询告警规则列表
+export function listAlarmRule(query) {
+  return request({
+    url: '/ems/alarm/rule/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询全部规则(不分页)
+export function listAllAlarmRule(query) {
+  return request({
+    url: '/ems/alarm/rule/listAll',
+    method: 'get',
+    params: query
+  })
+}
+
+// 获取告警规则详情
+export function getAlarmRule(id) {
+  return request({
+    url: '/ems/alarm/rule/' + id,
+    method: 'get'
+  })
+}
+
+// 根据规则代码获取详情
+export function getAlarmRuleByCode(ruleCode) {
+  return request({
+    url: '/ems/alarm/rule/code/' + ruleCode,
+    method: 'get'
+  })
+}
+
+// 新增告警规则
+export function addAlarmRule(data) {
+  return request({
+    url: '/ems/alarm/rule',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改告警规则
+export function updateAlarmRule(data) {
+  return request({
+    url: '/ems/alarm/rule',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除告警规则
+export function delAlarmRule(ids) {
+  return request({
+    url: '/ems/alarm/rule/' + ids,
+    method: 'delete'
+  })
+}
+
+// 启用/禁用规则
+export function updateRuleEnabled(ruleCode, enabled) {
+  return request({
+    url: '/ems/alarm/rule/enable/' + ruleCode + '/' + enabled,
+    method: 'put'
+  })
+}
+
+// 批量启用/禁用
+export function batchUpdateRuleEnabled(ruleCodes, enabled) {
+  return request({
+    url: '/ems/alarm/rule/batchEnable',
+    method: 'put',
+    data: ruleCodes,
+    params: { enabled }
+  })
+}
+
+// 复制规则
+export function copyAlarmRule(sourceRuleCode, newRuleName) {
+  return request({
+    url: '/ems/alarm/rule/copy',
+    method: 'post',
+    params: { sourceRuleCode, newRuleName }
+  })
+}
+
+// 查询指定分组的规则
+export function listRuleByGroup(groupCode) {
+  return request({
+    url: '/ems/alarm/rule/group/' + groupCode,
+    method: 'get'
+  })
+}
+
+// 查询适用于巡检的规则
+export function listRuleForInspection(deviceModel) {
+  return request({
+    url: '/ems/alarm/rule/forInspection',
+    method: 'get',
+    params: { deviceModel }
+  })
+}
+
+// 查询适用于实时监测的规则
+export function listRuleForRealtime(deviceModel, attrKey) {
+  return request({
+    url: '/ems/alarm/rule/forRealtime',
+    method: 'get',
+    params: { deviceModel, attrKey }
+  })
+}
+
+// 导出告警规则
+export function exportAlarmRule(query) {
+  return request({
+    url: '/ems/alarm/rule/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  })
+}

+ 134 - 0
ems-ui-cloud/src/enums/alarmEnum.js

@@ -0,0 +1,134 @@
+/**
+ * 告警模块枚举定义
+ */
+
+// 告警级别
+export const ALARM_LEVEL_OPTIONS = [
+  { value: 1, label: '一般', color: '#409EFF', type: 'primary' },
+  { value: 2, label: '重要', color: '#E6A23C', type: 'warning' },
+  { value: 3, label: '紧急', color: '#F56C6C', type: 'danger' }
+]
+
+// 告警状态
+export const ALARM_STATUS_OPTIONS = [
+  { value: 0, label: '活动', color: '#F56C6C', type: 'danger' },
+  { value: 1, label: '已确认', color: '#E6A23C', type: 'warning' },
+  { value: 2, label: '处置中', color: '#409EFF', type: 'primary' },
+  { value: 3, label: '已解决', color: '#67C23A', type: 'success' },
+  { value: 4, label: '已关闭', color: '#909399', type: 'info' },
+  { value: 5, label: '已恢复', color: '#67C23A', type: 'success' }
+]
+
+// 告警来源
+export const ALARM_SOURCE_OPTIONS = [
+  { value: 1, label: '实时监测', icon: 'el-icon-data-line', color: '#409EFF' },
+  { value: 2, label: '自动巡检', icon: 'el-icon-discover', color: '#67C23A' },
+  { value: 3, label: '手动上报', icon: 'el-icon-edit', color: '#E6A23C' }
+]
+
+// 检查类型
+export const CHECK_TYPE_OPTIONS = [
+  { value: 1, label: '范围检查', icon: 'el-icon-data-analysis' },
+  { value: 2, label: '等值检查', icon: 'el-icon-finished' },
+  { value: 3, label: '非空检查', icon: 'el-icon-document' },
+  { value: 4, label: '在线状态', icon: 'el-icon-connection' },
+  { value: 5, label: '变化率', icon: 'el-icon-sort' }
+]
+
+// 目标类型
+export const TARGET_TYPE_OPTIONS = [
+  { value: 0, label: '区域', icon: 'el-icon-location' },
+  { value: 1, label: '设施', icon: 'el-icon-office-building' },
+  { value: 2, label: '设备', icon: 'el-icon-cpu' }
+]
+
+// 操作符
+export const OPERATOR_OPTIONS = [
+  { value: 'GT', label: '大于 (>)' },
+  { value: 'GE', label: '大于等于 (>=)' },
+  { value: 'LT', label: '小于 (<)' },
+  { value: 'LE', label: '小于等于 (<=)' },
+  { value: 'EQ', label: '等于 (=)' },
+  { value: 'NE', label: '不等于 (!=)' },
+  { value: 'RANGE', label: '范围' },
+  { value: 'IN', label: '包含' }
+]
+
+// 处置类型
+export const HANDLE_TYPE_OPTIONS = [
+  { value: 1, label: '确认', icon: 'el-icon-check', color: '#E6A23C' },
+  { value: 2, label: '派单', icon: 'el-icon-s-promotion', color: '#409EFF' },
+  { value: 3, label: '处置', icon: 'el-icon-s-tools', color: '#67C23A' },
+  { value: 4, label: '关闭', icon: 'el-icon-circle-close', color: '#909399' },
+  { value: 5, label: '备注', icon: 'el-icon-edit-outline', color: '#606266' }
+]
+
+/**
+ * 获取告警级别信息
+ */
+export function getAlarmLevelInfo(level) {
+  const item = ALARM_LEVEL_OPTIONS.find(o => o.value === level)
+  return item || { label: '未知', color: '#909399', type: 'info' }
+}
+
+/**
+ * 获取告警状态信息
+ */
+export function getAlarmStatusInfo(status) {
+  const item = ALARM_STATUS_OPTIONS.find(o => o.value === status)
+  return item || { label: '未知', color: '#909399', type: 'info' }
+}
+
+/**
+ * 获取告警来源信息
+ */
+export function getAlarmSourceInfo(source) {
+  const item = ALARM_SOURCE_OPTIONS.find(o => o.value === source)
+  return item || { label: '未知', icon: 'el-icon-question', color: '#909399' }
+}
+
+/**
+ * 获取检查类型标签
+ */
+export function getCheckTypeLabel(type) {
+  const item = CHECK_TYPE_OPTIONS.find(o => o.value === type)
+  return item ? item.label : '未知'
+}
+
+/**
+ * 获取目标类型标签
+ */
+export function getTargetTypeLabel(type) {
+  const item = TARGET_TYPE_OPTIONS.find(o => o.value === type)
+  return item ? item.label : '未知'
+}
+
+/**
+ * 获取目标类型图标
+ */
+export function getTargetTypeIcon(type) {
+  const item = TARGET_TYPE_OPTIONS.find(o => o.value === type)
+  return item ? item.icon : 'el-icon-question'
+}
+
+/**
+ * 获取处置类型信息
+ */
+export function getHandleTypeInfo(type) {
+  const item = HANDLE_TYPE_OPTIONS.find(o => o.value === type)
+  return item || { label: '未知', icon: 'el-icon-question', color: '#909399' }
+}
+
+/**
+ * 判断告警是否为活动状态
+ */
+export function isActiveAlarm(status) {
+  return status !== null && status !== undefined && status <= 2
+}
+
+/**
+ * 判断告警是否已结束
+ */
+export function isEndedAlarm(status) {
+  return status !== null && status !== undefined && status >= 3
+}

+ 0 - 554
ems-ui-cloud/src/views/alarm/alarm-info/index.vue

@@ -1,554 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-row :gutter="20">
-      <!-- 左侧树形区域 -->
-      <el-col :span="5" :xs="24">
-        <div class="head-container">
-          <el-input
-            v-model="areaName"
-            placeholder="请输入服务区名称"
-            clearable
-            size="small"
-            prefix-icon="el-icon-search"
-            style="margin-bottom: 20px"
-            @input="filterTree"
-          />
-        </div>
-        <div class="head-container tree-container">
-          <el-tree
-            ref="tree"
-            :data="areaOptions"
-            :props="defaultProps"
-            :expand-on-click-node="false"
-            :filter-node-method="filterNode"
-            node-key="id"
-            :default-expanded-keys="defaultExpandedKeys"
-            highlight-current
-            @node-click="handleNodeClick"
-          >
-            <span class="custom-tree-node" slot-scope="{ node, data }">
-              <span class="tree-label">
-                <i :class="getTreeIcon(data)" class="tree-icon"></i>
-                {{ node.label }}
-              </span>
-            </span>
-          </el-tree>
-        </div>
-      </el-col>
-
-      <!-- 右侧内容区域 -->
-      <el-col :span="19" :xs="24">
-        <div class="content-wrapper">
-          <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
-            <el-form-item label="告警类型" prop="alarmType">
-              <el-select v-model="queryParams.alarmType" placeholder="请选择告警类型" clearable>
-                <el-option v-for="dict in dict.type.alarm_type" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="子系统" prop="systemCode">
-              <el-select v-model="queryParams.systemCode" placeholder="请选择子系统">
-                <el-option v-for="subsystem in subsystemList" :key="subsystem.systemCode" :label="subsystem.systemName" :value="subsystem.systemCode"></el-option>
-              </el-select>
-            </el-form-item>
-            <el-form-item label="告警状态" prop="alarmState">
-              <el-select v-model="queryParams.alarmState" placeholder="请选择告警状态">
-                <el-option v-for="dict in dict.type.alarm_state" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-              <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-
-          <el-row :gutter="10" class="mb8">
-            <el-col :span="1.5">
-              <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['ems:alarm-info:add']">新增</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['ems:alarm-info:edit']">修改</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['ems:alarm-info:remove']">删除</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['ems:alarm-info:export']">导出</el-button>
-            </el-col>
-            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-          </el-row>
-
-          <el-table v-loading="loading" :data="alarmInfoList" @selection-change="handleSelectionChange">
-            <el-table-column label="告警代码" align="center" prop="alarmCode" />
-            <el-table-column label="园区名称" align="left" prop="areaShortName" />
-            <el-table-column label="子系统" align="center" prop="subSystemName" />
-            <el-table-column label="对象类型" align="center" prop="objType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.obj_type" :value="scope.row.objType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="对象代码" align="center" prop="objCode" />
-            <el-table-column label="告警时间" align="center" prop="alarmDate" width="180">
-              <template slot-scope="scope">
-                <span>{{ parseTime(scope.row.alarmDate, '{y}-{m}-{d}') + " " + parseTime(scope.row.alarmTime, '{hh}:{mm}:{s}')}}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="告警描述" align="center" prop="alarmMsg" />
-            <el-table-column label="告警类型" align="center" prop="alarmType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.alarm_type" :value="scope.row.alarmType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="告警状态" align="center" prop="alarmState">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.alarm_state" :value="scope.row.alarmState" />
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-              <template slot-scope="scope">
-                <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['ems:alarm-info:edit']">修改</el-button>
-                <el-button size="mini" type="text" icon="el-icon-delete" class="deleteBtn" @click="handleDelete(scope.row)" v-hasPermi="['basecfg:alarm-info:remove']">删除</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-
-          <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
-        </div>
-      </el-col>
-    </el-row>
-
-    <!-- 添加或修改能源设施告警对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="园区代码" prop="areaCode">
-          <el-input v-model="form.areaCode" placeholder="请输入园区代码" />
-        </el-form-item>
-        <el-form-item label="对象类型" prop="objType">
-          <el-select v-model="form.objType" placeholder="请选择对象类型">
-            <el-option v-for="dict in dict.type.obj_type" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="对象代码" prop="objCode">
-          <el-input v-model="form.objCode" placeholder="请输入对象代码" />
-        </el-form-item>
-        <el-form-item label="告警时间" prop="alarmTime">
-          <el-date-picker v-model="form.alarmTime" type="datetime" format="yyyy-MM-dd HH:mm" value-format="yyyy-MM-dd HH:mm:00" :style="{ width: '100%' }" placeholder="请选择告警时间" @change="dateChange" clearable>
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="子系统" prop="systemCode">
-          <el-select v-model="form.systemCode" placeholder="请选择子系统">
-            <el-option v-for="subsystem in subsystemList" :key="subsystem.systemCode" :label="subsystem.systemName" :value="subsystem.systemCode"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="告警代码" prop="alarmCode">
-          <el-input v-model="form.alarmCode" placeholder="请输入告警代码" />
-        </el-form-item>
-        <el-form-item label="告警描述" prop="alarmMsg">
-          <el-input v-model="form.alarmMsg" placeholder="请输入告警描述" />
-        </el-form-item>
-        <el-form-item label="告警类型" prop="alarmType">
-          <el-select v-model="form.alarmType" placeholder="请选择告警类型">
-            <el-option v-for="dict in dict.type.alarm_type" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="告警状态" prop="alarmState">
-          <el-select v-model="form.alarmState" placeholder="请选择告警状态">
-            <el-option v-for="dict in dict.type.alarm_state" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import {listSubsystem} from '@/api/adapter/subsystem'
-import {addAlarmInfo, delAlarmInfo, getAlarmInfo, listAlarmInfo, updateAlarmInfo} from '@/api/alarm/alarm-info'
-import dayjs from 'dayjs'
-import {areaTreeSelect} from '@/api/basecfg/area'
-
-export default {
-  name: 'Alarm-info',
-  dicts: ['obj_type', 'alarm_type', 'alarm_state'],
-  data () {
-    return {
-      subsystemList: [],
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 能源设施告警表格数据
-      alarmInfoList: [],
-      // 弹出层标题
-      title: '',
-      // 是否显示弹出层
-      open: false,
-      // 默认展开的节点
-      defaultExpandedKeys: [],
-      defaultProps: {
-        children: 'children',
-        label: 'label'
-      },
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        areaCode: null,
-        objType: null,
-        objCode: null,
-        alarmCode: null,
-        alarmMsg: null,
-        alarmType: null,
-        alarmState: null,
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        areaCode: [
-          {
-            required: true,
-            message: '园区代码不能为空',
-            trigger: 'blur',
-          },
-        ],
-        systemCode: [
-          {
-            required: true,
-            message: '子系统不能为空',
-            trigger: 'blur',
-          },
-        ],
-        objType: [
-          {
-            required: true,
-            message: '对象类型不能为空',
-            trigger: 'change',
-          },
-        ],
-        objCode: [
-          {
-            required: true,
-            message: '对象代码不能为空',
-            trigger: 'blur',
-          },
-        ],
-        alarmTime: [
-          {
-            required: true,
-            message: '告警时间不能为空',
-            trigger: 'blur',
-          },
-        ],
-        alarmType: [
-          {
-            required: true,
-            message: '告警类型不能为空',
-            trigger: 'change',
-          },
-        ],
-        alarmState: [
-          {
-            required: true,
-            message: '告警状态不能为空',
-            trigger: 'blur',
-          },
-        ],
-      },
-      areaName: undefined,
-      areaOptions: [],
-    }
-  },
-  watch: {
-    // 根据名称筛选区域树
-    areaName (val) {
-      this.$refs.tree.filter(val)
-    }
-  },
-  created () {
-    this.getAreaTree('0', 1)
-    this.getList()
-    this.getSubList()
-  },
-  methods: {
-    // 获取树节点图标
-    getTreeIcon(data) {
-      if (data.id === '-1') {
-        return 'el-icon-s-home'
-      }
-      return 'el-icon-office-building'
-    },
-
-    // 过滤树
-    filterTree() {
-      this.$refs.tree.filter(this.areaName)
-    },
-
-    /** 查询能源设施告警列表 */
-    getList () {
-      this.loading = true
-      listAlarmInfo(this.queryParams).then(response => {
-        this.alarmInfoList = response.rows
-        this.total = response.total
-        this.loading = false
-      })
-    },
-
-    // 取消按钮
-    cancel () {
-      this.open = false
-      this.reset()
-    },
-
-    // 表单重置
-    reset () {
-      this.form = {
-        id: null,
-        areaCode: null,
-        objType: null,
-        objCode: null,
-        alarmTime: null,
-        alarmCode: null,
-        alarmMsg: null,
-        alarmType: null,
-        alarmState: null,
-      }
-      this.resetForm('form')
-    },
-
-    async getSubList () {
-      const {rows} = await listSubsystem({
-        pageNum: 1,
-        pageSize: 999,
-      })
-      this.subsystemList = rows
-    },
-
-    dateChange (val) {
-      this.$refs.form.alarmTime = val
-      this.$refs.form.alarmDate = dayjs(val).format('YYYY-MM-DD')
-      this.form.alarmDate = dayjs(val).format('YYYY-MM-DD')
-    },
-
-    /** 搜索按钮操作 */
-    handleQuery () {
-      this.queryParams.pageNum = 1
-      this.getList()
-    },
-
-    /** 重置按钮操作 */
-    resetQuery () {
-      this.resetForm('queryForm')
-      this.handleQuery()
-    },
-
-    // 多选框选中数据
-    handleSelectionChange (selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length !== 1
-      this.multiple = !selection.length
-    },
-
-    /** 新增按钮操作 */
-    handleAdd () {
-      this.reset()
-      this.open = true
-      this.title = '添加能源设施告警'
-    },
-
-    /** 修改按钮操作 */
-    handleUpdate (row) {
-      this.reset()
-      const id = row.id || this.ids
-      getAlarmInfo(id).then(response => {
-        this.form = response.data
-        this.open = true
-        this.title = '修改能源设施告警'
-      })
-    },
-
-    /** 提交按钮 */
-    submitForm () {
-      this.$refs['form'].validate(valid => {
-        if (valid) {
-          if (this.form.id != null) {
-            updateAlarmInfo(this.form).then(response => {
-              this.$modal.msgSuccess('修改成功')
-              this.open = false
-              this.getList()
-            })
-          } else {
-            addAlarmInfo(this.form).then(response => {
-              this.$modal.msgSuccess('新增成功')
-              this.open = false
-              this.getList()
-            })
-          }
-        }
-      })
-    },
-
-    /** 删除按钮操作 */
-    handleDelete (row) {
-      const ids = row.id || this.ids
-      this.$modal.confirm('是否确认删除能源设施告警编号为"' + ids + '"的数据项?').then(function () {
-        return delAlarmInfo(ids)
-      }).then(() => {
-        this.getList()
-        this.$modal.msgSuccess('删除成功')
-      }).catch(() => { })
-    },
-
-    /** 导出按钮操作 */
-    handleExport () {
-      this.download('ems/alarm-info/export', {
-        ...this.queryParams,
-      }, `alarm-info_${new Date().getTime()}.xlsx`)
-    },
-
-    getAreaTree(areaCode, layer) {
-      areaTreeSelect(areaCode, layer).then(response => {
-        this.areaOptions = [{
-          id: '-1',
-          label: '全部',
-          children: []
-        }].concat(response.data)
-        this.queryParams.areaCode = '-1'
-
-        // 设置默认展开第一级
-        this.defaultExpandedKeys = ['-1']
-
-        // 默认选中全部
-        this.$nextTick(() => {
-          if (this.$refs.tree) {
-            this.$refs.tree.setCurrentKey('-1')
-          }
-        })
-      })
-    },
-
-    // 筛选节点
-    filterNode (value, data) {
-      if (!value) return true
-      return data.label.indexOf(value) !== -1
-    },
-
-    handleNodeClick (data, node) {
-      this.queryParams.areaCode = data.id
-      this.getList()
-    },
-  },
-}
-</script>
-
-<style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-  background: #f5f7fa;
-  min-height: calc(100vh - 84px);
-
-  .head-container {
-    background: #fff;
-    padding: 15px;
-    border-radius: 8px;
-    margin-bottom: 15px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-
-    &.tree-container {
-      max-height: calc(100vh - 280px);
-      overflow-y: auto;
-
-      &::-webkit-scrollbar {
-        width: 6px;
-      }
-
-      &::-webkit-scrollbar-track {
-        background: #f1f1f1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb {
-        background: #c1c1c1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb:hover {
-        background: #a8a8a8;
-      }
-
-      ::v-deep .el-tree {
-        background: transparent;
-
-        .el-tree-node__content {
-          height: 40px;
-          padding: 0 8px;
-          transition: all 0.3s;
-
-          &:hover {
-            background-color: #f5f7fa;
-          }
-        }
-
-        .el-tree-node.is-current > .el-tree-node__content {
-          background-color: #ecf5ff;
-          color: #409eff;
-        }
-
-        .custom-tree-node {
-          flex: 1;
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          font-size: 14px;
-
-          .tree-label {
-            display: flex;
-            align-items: center;
-
-            .tree-icon {
-              margin-right: 8px;
-              font-size: 16px;
-              color: #909399;
-              transition: color 0.3s;
-            }
-          }
-        }
-
-        .el-tree-node.is-current .tree-icon {
-          color: #409eff;
-        }
-      }
-    }
-  }
-
-  .content-wrapper {
-    background: #fff;
-    border-radius: 8px;
-    padding: 20px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-    min-height: calc(100vh - 160px);
-  }
-}
-
-// 响应式布局
-@media (max-width: 768px) {
-  .app-container {
-    padding: 10px;
-
-    .el-col {
-      margin-bottom: 20px;
-    }
-  }
-}
-</style>

+ 0 - 504
ems-ui-cloud/src/views/alarm/index.vue

@@ -1,504 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-row :gutter="20">
-      <!-- 左侧树形区域 -->
-      <el-col :span="5" :xs="24">
-        <div class="head-container">
-          <el-input
-            v-model="areaName"
-            placeholder="请输入服务区名称"
-            clearable
-            size="small"
-            prefix-icon="el-icon-search"
-            style="margin-bottom: 20px"
-            @input="filterTree"
-          />
-        </div>
-        <div class="head-container tree-container">
-          <el-tree
-            ref="tree"
-            :data="areaOptions"
-            :props="defaultProps"
-            :expand-on-click-node="false"
-            :filter-node-method="filterNode"
-            node-key="id"
-            :default-expanded-keys="defaultExpandedKeys"
-            highlight-current
-            @node-click="handleNodeClick"
-          >
-            <span class="custom-tree-node" slot-scope="{ node, data }">
-              <span class="tree-label">
-                <i :class="getTreeIcon(data)" class="tree-icon"></i>
-                {{ node.label }}
-              </span>
-            </span>
-          </el-tree>
-        </div>
-      </el-col>
-
-      <!-- 右侧内容区域 -->
-      <el-col :span="19" :xs="24">
-        <div class="content-wrapper">
-          <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
-            <el-form-item label="策略名称" prop="policyName">
-              <el-input v-model="queryParams.policyName" placeholder="请输入策略名称" clearable @keyup.enter.native="handleQuery" />
-            </el-form-item>
-            <el-form-item label="对象类型" prop="alarmObjType">
-              <el-select v-model="queryParams.alarmObjType" placeholder="请选择告警对象类型" clearable>
-                <el-option v-for="dict in dict.type.obj_type" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="告警代码" prop="alarmCode">
-              <el-input v-model="queryParams.alarmCode" placeholder="请输入告警代码" clearable @keyup.enter.native="handleQuery" />
-            </el-form-item>
-            <el-form-item label="告警类型" prop="alarmType">
-              <el-select v-model="queryParams.alarmType" placeholder="请选择告警类型" clearable>
-                <el-option v-for="dict in dict.type.alarm_type" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-              <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-
-          <el-row :gutter="10" class="mb8">
-            <el-col :span="1.5">
-              <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['ems:alarm-strategy:add']">新增</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['ems:alarm-strategy:edit']">修改</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['ems:alarm-strategy:remove']">删除</el-button>
-            </el-col>
-            <el-col :span="1.5">
-              <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['ems:alarm-strategy:export']">导出</el-button>
-            </el-col>
-            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-          </el-row>
-
-          <el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange">
-            <el-table-column type="selection" width="55" align="center" />
-            <el-table-column label="策略代码" align="left" prop="policyCode" />
-            <el-table-column label="策略名称" align="center" prop="policyName" />
-            <el-table-column label="告警对象类型" align="center" prop="alarmObjType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.obj_type" :value="scope.row.alarmObjType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="告警对象指标" align="center" prop="alarmObjIndex" />
-            <el-table-column label="告警规则" align="center" prop="alarmRuleType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.alarm_thre_type" :value="scope.row.alarmRuleType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="告警阈值" align="center" prop="alarmThresholdValue" />
-            <el-table-column label="告警代码" align="center" prop="alarmCode" />
-            <el-table-column label="告警描述" align="center" prop="alarmMsg" />
-            <el-table-column label="告警类型" align="center" prop="alarmType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.alarm_type" :value="scope.row.alarmType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-              <template slot-scope="scope">
-                <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['ems:alarm-strategy:edit']">修改</el-button>
-                <el-button size="mini" type="text" icon="el-icon-delete" class="deleteBtn" @click="handleDelete(scope.row)" v-hasPermi="['ems:alarm-strategy:remove']">删除</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-
-          <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
-        </div>
-      </el-col>
-    </el-row>
-
-    <!-- 添加或修改能源设施告警策略对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
-        <el-form-item label="归属区域" prop="areaCode">
-          <el-select v-model="form.areaCode" placeholder="请选择归属区域" >
-            <el-option v-for="item in sourceAreaOptions" :label="item.label" :value="item.id" :key="item.id" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="策略代码" prop="policyCode">
-          <el-input v-model="form.policyCode" placeholder="请输入策略代码" />
-        </el-form-item>
-        <el-form-item label="策略名称" prop="policyName">
-          <el-input v-model="form.policyName" placeholder="请输入策略名称" />
-        </el-form-item>
-        <el-form-item label="告警对象类型" prop="alarmObjType">
-          <el-select v-model="form.alarmObjType" placeholder="请选择告警对象类型">
-            <el-option v-for="dict in dict.type.obj_type" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="告警对象指标" prop="alarmObjIndex">
-          <el-input v-model="form.alarmObjIndex" placeholder="请输入告警对象指标" />
-        </el-form-item>
-        <el-form-item label="告警规则" prop="alarmRuleType">
-          <el-select v-model="form.alarmRuleType" placeholder="请选择告警规则">
-            <el-option v-for="dict in dict.type.alarm_thre_type" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="告警阈值" prop="alarmThresholdValue">
-          <el-input v-model="form.alarmThresholdValue" placeholder="请输入告警阈值" />
-        </el-form-item>
-        <el-form-item label="告警代码" prop="alarmCode">
-          <el-input v-model="form.alarmCode" placeholder="请输入告警代码" />
-        </el-form-item>
-        <el-form-item label="告警描述" prop="alarmMsg">
-          <el-input v-model="form.alarmMsg" placeholder="请输入告警描述" />
-        </el-form-item>
-        <el-form-item label="告警类型" prop="alarmType">
-          <el-select v-model="form.alarmType" placeholder="请选择告警类型">
-            <el-option v-for="dict in dict.type.alarm_type" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import {listAlarm, getAlarm, delAlarm, addAlarm, updateAlarm} from "@/api/alarm/alarm"
-import {areaTreeSelect} from '@/api/basecfg/area'
-
-export default {
-  name: "Alarm",
-  dicts: ['obj_type', 'alarm_thre_type', 'alarm_type'],
-  data () {
-    return {
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 能源设施告警策略表格数据
-      alarmList: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 默认展开的节点
-      defaultExpandedKeys: [],
-      defaultProps: {
-        children: 'children',
-        label: 'label'
-      },
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        policyName: null,
-        alarmObjType: null,
-        alarmCode: null,
-        alarmType: null,
-        areaCode: null
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        areaCode: [
-          {required: true, message: "区域不能为空", trigger: "blur"}
-        ],
-        policyCode: [
-          {required: true, message: "策略代码不能为空", trigger: "blur"}
-        ],
-        policyName: [
-          {required: true, message: "策略名称不能为空", trigger: "blur"}
-        ],
-        alarmObjType: [
-          {required: true, message: "告警对象类型不能为空", trigger: "change"}
-        ],
-        alarmObjIndex: [
-          {required: true, message: "告警对象指标不能为空", trigger: "blur"}
-        ],
-      },
-      areaName: undefined,
-      areaOptions: [],
-      sourceAreaOptions: [],
-    }
-  },
-  watch: {
-    // 根据名称筛选区域树
-    areaName (val) {
-      this.$refs.tree.filter(val)
-    }
-  },
-  async created () {
-    await this.getAreaTreeByTag('0', 1)
-    this.getList()
-  },
-  methods: {
-    // 获取树节点图标
-    getTreeIcon(data) {
-      if (data.id === '-1') {
-        return 'el-icon-s-home'
-      }
-      return 'el-icon-office-building'
-    },
-
-    // 过滤树
-    filterTree() {
-      this.$refs.tree.filter(this.areaName)
-    },
-
-    /** 查询能源设施告警策略列表 */
-    getList () {
-      this.loading = true
-      listAlarm(this.queryParams).then(response => {
-        this.alarmList = response.rows
-        this.total = response.total
-        this.loading = false
-      })
-    },
-
-    /** 查询区域树结构 */
-    async getAreaTreeByTag(areaCode, layer) {
-      await areaTreeSelect(areaCode, layer).then(response => {
-        this.areaOptions = [{
-          id: '-1',
-          label: '全部',
-          children: []
-        }].concat(response.data)
-        this.sourceAreaOptions = response.data
-        this.queryParams.areaCode = '-1'
-
-        // 设置默认展开第一级
-        this.defaultExpandedKeys = ['-1']
-
-        // 默认选中全部
-        this.$nextTick(() => {
-          if (this.$refs.tree) {
-            this.$refs.tree.setCurrentKey('-1')
-          }
-        })
-      })
-    },
-
-    // 筛选节点
-    filterNode (value, data) {
-      if (!value) return true
-      return data.label.indexOf(value) !== -1
-    },
-
-    handleNodeClick (data, node) {
-      this.queryParams.areaCode = data.id
-      this.getList()
-    },
-
-    // 取消按钮
-    cancel () {
-      this.open = false
-      this.reset()
-    },
-
-    // 表单重置
-    reset () {
-      this.form = {
-        id: null,
-        policyCode: null,
-        policyName: null,
-        alarmObjType: null,
-        alarmObjIndex: null,
-        alarmRuleType: null,
-        alarmThresholdValue: null,
-        alarmCode: null,
-        alarmMsg: null,
-        alarmType: null
-      }
-      this.resetForm("form")
-    },
-
-    /** 搜索按钮操作 */
-    handleQuery () {
-      this.queryParams.pageNum = 1
-      this.getList()
-    },
-
-    /** 重置按钮操作 */
-    resetQuery () {
-      this.resetForm("queryForm")
-      this.handleQuery()
-    },
-
-    // 多选框选中数据
-    handleSelectionChange (selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length !== 1
-      this.multiple = !selection.length
-    },
-
-    /** 新增按钮操作 */
-    handleAdd () {
-      this.reset()
-      this.open = true
-      this.title = "添加能源设施告警策略"
-    },
-
-    /** 修改按钮操作 */
-    handleUpdate (row) {
-      this.reset()
-      const id = row.id || this.ids
-      getAlarm(id).then(response => {
-        this.form = response.data
-        this.open = true
-        this.title = "修改能源设施告警策略"
-      })
-    },
-
-    /** 提交按钮 */
-    submitForm () {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          if (this.form.id != null) {
-            updateAlarm(this.form).then(response => {
-              this.$modal.msgSuccess("修改成功")
-              this.open = false
-              this.getList()
-            })
-          } else {
-            addAlarm(this.form).then(response => {
-              this.$modal.msgSuccess("新增成功")
-              this.open = false
-              this.getList()
-            })
-          }
-        }
-      })
-    },
-
-    /** 删除按钮操作 */
-    handleDelete (row) {
-      const ids = row.id || this.ids
-      this.$modal.confirm('是否确认删除能源设施告警策略编号为"' + ids + '"的数据项?').then(function () {
-        return delAlarm(ids)
-      }).then(() => {
-        this.getList()
-        this.$modal.msgSuccess("删除成功")
-      }).catch(() => { })
-    },
-
-    /** 导出按钮操作 */
-    handleExport () {
-      this.download('ems/alarm/export', {
-        ...this.queryParams
-      }, `alarm_${new Date().getTime()}.xlsx`)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-  background: #f5f7fa;
-  min-height: calc(100vh - 84px);
-
-  .head-container {
-    background: #fff;
-    padding: 15px;
-    border-radius: 8px;
-    margin-bottom: 15px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-
-    &.tree-container {
-      max-height: calc(100vh - 280px);
-      overflow-y: auto;
-
-      &::-webkit-scrollbar {
-        width: 6px;
-      }
-
-      &::-webkit-scrollbar-track {
-        background: #f1f1f1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb {
-        background: #c1c1c1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb:hover {
-        background: #a8a8a8;
-      }
-
-      ::v-deep .el-tree {
-        background: transparent;
-
-        .el-tree-node__content {
-          height: 40px;
-          padding: 0 8px;
-          transition: all 0.3s;
-
-          &:hover {
-            background-color: #f5f7fa;
-          }
-        }
-
-        .el-tree-node.is-current > .el-tree-node__content {
-          background-color: #ecf5ff;
-          color: #409eff;
-        }
-
-        .custom-tree-node {
-          flex: 1;
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          font-size: 14px;
-
-          .tree-label {
-            display: flex;
-            align-items: center;
-
-            .tree-icon {
-              margin-right: 8px;
-              font-size: 16px;
-              color: #909399;
-              transition: color 0.3s;
-            }
-          }
-        }
-
-        .el-tree-node.is-current .tree-icon {
-          color: #409eff;
-        }
-      }
-    }
-  }
-
-  .content-wrapper {
-    background: #fff;
-    border-radius: 8px;
-    padding: 20px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-    min-height: calc(100vh - 160px);
-  }
-}
-
-// 响应式布局
-@media (max-width: 768px) {
-  .app-container {
-    padding: 10px;
-
-    .el-col {
-      margin-bottom: 20px;
-    }
-  }
-}
-</style>

+ 556 - 0
ems-ui-cloud/src/views/alarm/list/components/AlarmDetail.vue

@@ -0,0 +1,556 @@
+<template>
+  <el-drawer :title="'告警详情'" :visible.sync="visible" size="600px" direction="rtl"
+             :before-close="handleClose" custom-class="alarm-detail-drawer">
+    <div v-loading="loading" class="detail-content">
+      <!-- 告警状态头部 -->
+      <div class="status-header" :class="statusClass">
+        <div class="status-icon">
+          <i :class="statusIcon"></i>
+        </div>
+        <div class="status-info">
+          <div class="status-title">
+            <el-tag :type="getAlarmLevelInfo(alarm.alarmLevel).type" size="medium" effect="dark">
+              {{ getAlarmLevelInfo(alarm.alarmLevel).label }}
+            </el-tag>
+            <el-tag :type="getAlarmStatusInfo(alarm.alarmStatus).type" size="medium" effect="light" style="margin-left: 8px">
+              {{ getAlarmStatusInfo(alarm.alarmStatus).label }}
+            </el-tag>
+          </div>
+          <div class="status-desc">
+            {{ alarm.alarmMsg || formatAlarmContent(alarm) }}
+          </div>
+        </div>
+      </div>
+
+      <!-- 基本信息 -->
+      <div class="info-section">
+        <div class="section-title">
+          <i class="el-icon-info"></i> 基本信息
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="告警ID">
+            <code class="code-text">{{ alarm.alarmId }}</code>
+            <el-button type="text" size="mini" icon="el-icon-document-copy"
+                       @click="copyText(alarm.alarmId)" style="padding: 0; margin-left: 8px">复制</el-button>
+          </el-descriptions-item>
+          <el-descriptions-item label="告警来源">
+            <span :style="{ color: getAlarmSourceInfo(alarm.alarmSource).color }">
+              <i :class="getAlarmSourceInfo(alarm.alarmSource).icon" style="margin-right: 4px"></i>
+              {{ getAlarmSourceInfo(alarm.alarmSource).label }}
+            </span>
+          </el-descriptions-item>
+          <el-descriptions-item label="告警时间">
+            <i class="el-icon-time" style="margin-right: 4px"></i>
+            {{ alarm.alarmTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="持续时长">
+            <span v-if="alarm.duration" class="duration-text">{{ formatDuration(alarm.duration) }}</span>
+            <span v-else-if="isActiveAlarm(alarm.alarmStatus)" class="duration-active">
+              进行中 <i class="el-icon-loading"></i>
+            </span>
+            <span v-else>-</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="重复次数">
+            <el-badge :value="alarm.repeatCount || 1" type="warning" class="repeat-badge" />
+          </el-descriptions-item>
+          <el-descriptions-item label="最后发生">
+            {{ alarm.lastOccurTime || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 告警目标 -->
+      <div class="info-section">
+        <div class="section-title">
+          <i class="el-icon-aim"></i> 告警目标
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="目标类型">
+            <i :class="getTargetTypeIcon(alarm.targetType)" style="margin-right: 4px"></i>
+            {{ getTargetTypeLabel(alarm.targetType) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="目标代码">
+            <code class="code-text">{{ alarm.targetCode }}</code>
+          </el-descriptions-item>
+          <el-descriptions-item label="目标名称" :span="2">
+            <span class="target-name">{{ alarm.targetName }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="设备模型">
+            {{ alarm.deviceModelName || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="所属区域">
+            {{ alarm.areaName || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="安装位置" :span="2">
+            <i class="el-icon-location-outline" style="margin-right: 4px"></i>
+            {{ alarm.location || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 告警详情 -->
+      <div class="info-section">
+        <div class="section-title">
+          <i class="el-icon-warning-outline"></i> 告警详情
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="触发规则">
+            <el-link type="primary" v-if="alarm.ruleCode" @click="viewRule">
+              {{ alarm.ruleName || alarm.ruleCode }}
+            </el-link>
+            <span v-else>-</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="告警代码">
+            {{ alarm.alarmCode || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="监测属性">
+            <el-tag size="mini" effect="plain">{{ alarm.attrName || alarm.attrKey || '-' }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="阈值配置">
+            {{ alarm.thresholdValue || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="告警时值" :span="2">
+            <div class="alarm-value-display">
+              <code class="value-code">{{ alarm.attrValue || '-' }}</code>
+              <span class="value-unit" v-if="alarm.attrUnit">{{ alarm.attrUnit }}</span>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="子系统">
+            {{ alarm.subsystemName || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="来源引用" v-if="alarm.sourceRef">
+            <el-link type="primary" @click="viewSourceRef">{{ alarm.sourceRef }}</el-link>
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 处置信息 -->
+      <div class="info-section" v-if="alarm.alarmStatus >= 1">
+        <div class="section-title">
+          <i class="el-icon-s-check"></i> 处置信息
+        </div>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="确认时间" v-if="alarm.confirmTime">
+            {{ alarm.confirmTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="确认人" v-if="alarm.confirmBy">
+            {{ alarm.confirmBy }}
+          </el-descriptions-item>
+          <el-descriptions-item label="解决时间" v-if="alarm.resolveTime">
+            {{ alarm.resolveTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="解决人" v-if="alarm.resolveBy">
+            {{ alarm.resolveBy }}
+          </el-descriptions-item>
+          <el-descriptions-item label="恢复时间" v-if="alarm.recoveryTime" :span="2">
+            {{ alarm.recoveryTime }}
+            <el-tag size="mini" type="success" style="margin-left: 8px">自动恢复</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="解决备注" v-if="alarm.resolveRemark" :span="2">
+            {{ alarm.resolveRemark }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 处置记录时间线 -->
+      <div class="info-section" v-if="handleList && handleList.length > 0">
+        <div class="section-title">
+          <i class="el-icon-time"></i> 处置记录
+        </div>
+        <el-timeline class="handle-timeline">
+          <el-timeline-item v-for="item in handleList" :key="item.id"
+                            :timestamp="item.handleTime" placement="top"
+                            :type="getHandleTimelineType(item.handleType)"
+                            :icon="getHandleTypeInfo(item.handleType).icon">
+            <el-card shadow="never" class="timeline-card">
+              <div class="timeline-header">
+                <span class="handle-type">{{ getHandleTypeInfo(item.handleType).label }}</span>
+                <span class="handle-by">{{ item.handleBy }}</span>
+              </div>
+              <div class="timeline-content" v-if="item.handleContent">
+                {{ item.handleContent }}
+              </div>
+              <div class="timeline-result" v-if="item.handleResult">
+                <i class="el-icon-arrow-right"></i> {{ item.handleResult }}
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="action-footer" v-if="!isEndedAlarm(alarm.alarmStatus)">
+        <el-button v-if="alarm.alarmStatus === 0" type="warning" @click="handleConfirm">
+          <i class="el-icon-check"></i> 确认告警
+        </el-button>
+        <el-button type="primary" @click="handleProcess">
+          <i class="el-icon-s-tools"></i> 处置告警
+        </el-button>
+        <el-button type="success" @click="handleResolve">
+          <i class="el-icon-circle-check"></i> 标记解决
+        </el-button>
+        <el-button type="info" @click="handleCloseAlarm">
+          <i class="el-icon-circle-close"></i> 关闭告警
+        </el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+import { getAlarmByAlarmId, confirmAlarm } from '@/api/alarm/alarm'
+import {
+  getAlarmLevelInfo, getAlarmStatusInfo, getAlarmSourceInfo,
+  getTargetTypeIcon, getTargetTypeLabel, getHandleTypeInfo,
+  isActiveAlarm, isEndedAlarm
+} from '@/enums/alarmEnum'
+
+export default {
+  name: 'AlarmDetail',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      alarm: {},
+      handleList: []
+    }
+  },
+  computed: {
+    statusClass() {
+      if (this.alarm.alarmStatus === 0) {
+        if (this.alarm.alarmLevel === 3) return 'status-urgent'
+        if (this.alarm.alarmLevel === 2) return 'status-important'
+        return 'status-active'
+      }
+      if (this.alarm.alarmStatus >= 3) return 'status-ended'
+      return 'status-processing'
+    },
+    statusIcon() {
+      if (this.alarm.alarmStatus === 0) return 'el-icon-warning'
+      if (this.alarm.alarmStatus === 1) return 'el-icon-view'
+      if (this.alarm.alarmStatus === 2) return 'el-icon-s-tools'
+      return 'el-icon-circle-check'
+    }
+  },
+  methods: {
+    getAlarmLevelInfo,
+    getAlarmStatusInfo,
+    getAlarmSourceInfo,
+    getTargetTypeIcon,
+    getTargetTypeLabel,
+    getHandleTypeInfo,
+    isActiveAlarm,
+    isEndedAlarm,
+
+    async open(alarmId) {
+      this.visible = true
+      this.loading = true
+      this.alarm = {}
+      this.handleList = []
+
+      try {
+        const res = await getAlarmByAlarmId(alarmId)
+        this.alarm = res.data || {}
+        this.handleList = this.alarm.handleList || []
+      } catch (e) {
+        this.$modal.msgError('获取告警详情失败')
+        this.visible = false
+      } finally {
+        this.loading = false
+      }
+    },
+
+    handleClose() {
+      this.visible = false
+    },
+
+    formatAlarmContent(alarm) {
+      if (!alarm.targetName) return ''
+      const attrName = alarm.attrName || alarm.attrKey || '属性'
+      return `${alarm.targetName} ${attrName}异常`
+    },
+
+    formatDuration(seconds) {
+      if (!seconds) return ''
+      if (seconds < 60) return `${seconds}秒`
+      if (seconds < 3600) return `${Math.floor(seconds / 60)}分${seconds % 60}秒`
+      const hours = Math.floor(seconds / 3600)
+      const mins = Math.floor((seconds % 3600) / 60)
+      if (seconds < 86400) return `${hours}小时${mins}分`
+      const days = Math.floor(seconds / 86400)
+      return `${days}天${hours % 24}小时`
+    },
+
+    getHandleTimelineType(handleType) {
+      const typeMap = {
+        1: 'warning',  // 确认
+        2: 'primary',  // 派单
+        3: '',         // 处置
+        4: 'info',     // 关闭
+        5: 'info'      // 备注
+      }
+      return typeMap[handleType] || ''
+    },
+
+    copyText(text) {
+      navigator.clipboard.writeText(text).then(() => {
+        this.$modal.msgSuccess('复制成功')
+      })
+    },
+
+    viewRule() {
+      // 跳转到规则详情或打开规则详情弹窗
+      this.$router.push({
+        path: '/alarm/rule',
+        query: { ruleCode: this.alarm.ruleCode }
+      })
+    },
+
+    viewSourceRef() {
+      // 根据来源类型跳转
+      if (this.alarm.alarmSource === 2 && this.alarm.sourceRef) {
+        // 巡检报告
+        this.$router.push({
+          path: '/inspection/report',
+          query: { reportCode: this.alarm.sourceRef }
+        })
+      }
+    },
+
+    handleConfirm() {
+      this.$modal.confirm('确认此告警吗?').then(() => {
+        return confirmAlarm(this.alarm.alarmId, '已确认')
+      }).then(() => {
+        this.$modal.msgSuccess('确认成功')
+        this.open(this.alarm.alarmId)
+        this.$emit('refresh')
+      }).catch(() => {})
+    },
+
+    handleProcess() {
+      this.visible = false
+      this.$parent.$refs.alarmHandle.open(this.alarm, 'handle')
+    },
+
+    handleResolve() {
+      this.visible = false
+      this.$parent.$refs.alarmHandle.open(this.alarm, 'resolve')
+    },
+
+    handleCloseAlarm() {
+      this.visible = false
+      this.$parent.$refs.alarmHandle.open(this.alarm, 'close')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alarm-detail-drawer {
+  ::v-deep .el-drawer__body {
+    padding: 0;
+    overflow-y: auto;
+  }
+}
+
+.detail-content {
+  padding: 20px;
+}
+
+// 状态头部
+.status-header {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+
+  &.status-urgent {
+    background: linear-gradient(135deg, rgba(245, 108, 108, 0.15) 0%, rgba(245, 108, 108, 0.05) 100%);
+    border-left: 4px solid #f56c6c;
+  }
+
+  &.status-important {
+    background: linear-gradient(135deg, rgba(230, 162, 60, 0.15) 0%, rgba(230, 162, 60, 0.05) 100%);
+    border-left: 4px solid #e6a23c;
+  }
+
+  &.status-active {
+    background: linear-gradient(135deg, rgba(64, 158, 255, 0.15) 0%, rgba(64, 158, 255, 0.05) 100%);
+    border-left: 4px solid #409eff;
+  }
+
+  &.status-processing {
+    background: linear-gradient(135deg, rgba(103, 194, 58, 0.15) 0%, rgba(103, 194, 58, 0.05) 100%);
+    border-left: 4px solid #67c23a;
+  }
+
+  &.status-ended {
+    background: linear-gradient(135deg, rgba(144, 147, 153, 0.15) 0%, rgba(144, 147, 153, 0.05) 100%);
+    border-left: 4px solid #909399;
+  }
+
+  .status-icon {
+    width: 48px;
+    height: 48px;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.8);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 16px;
+
+    i {
+      font-size: 24px;
+      color: #f56c6c;
+    }
+  }
+
+  .status-info {
+    flex: 1;
+  }
+
+  .status-title {
+    margin-bottom: 8px;
+  }
+
+  .status-desc {
+    font-size: 14px;
+    color: #606266;
+    line-height: 1.5;
+  }
+}
+
+// 信息区块
+.info-section {
+  margin-bottom: 20px;
+
+  .section-title {
+    font-weight: 600;
+    font-size: 14px;
+    color: #303133;
+    margin-bottom: 12px;
+    padding-left: 8px;
+    border-left: 3px solid #409eff;
+
+    i {
+      margin-right: 6px;
+      color: #409eff;
+    }
+  }
+}
+
+.code-text {
+  background: #f5f7fa;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 12px;
+  color: #409eff;
+}
+
+.target-name {
+  font-weight: 600;
+  color: #303133;
+}
+
+.alarm-value-display {
+  display: flex;
+  align-items: center;
+
+  .value-code {
+    background: #fef0f0;
+    color: #f56c6c;
+    padding: 4px 12px;
+    border-radius: 4px;
+    font-family: 'Monaco', 'Menlo', monospace;
+    font-size: 14px;
+    font-weight: 600;
+  }
+
+  .value-unit {
+    margin-left: 8px;
+    color: #909399;
+    font-size: 12px;
+  }
+}
+
+.duration-text {
+  font-weight: 600;
+  color: #e6a23c;
+}
+
+.duration-active {
+  color: #f56c6c;
+
+  i {
+    margin-left: 4px;
+  }
+}
+
+.repeat-badge {
+  ::v-deep .el-badge__content {
+    font-size: 12px;
+  }
+}
+
+// 处置记录时间线
+.handle-timeline {
+  padding-left: 10px;
+
+  ::v-deep .el-timeline-item__wrapper {
+    padding-left: 20px;
+  }
+}
+
+.timeline-card {
+  ::v-deep .el-card__body {
+    padding: 12px;
+  }
+}
+
+.timeline-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+
+  .handle-type {
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .handle-by {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+
+.timeline-content {
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.5;
+}
+
+.timeline-result {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #67c23a;
+
+  i {
+    margin-right: 4px;
+  }
+}
+
+// 操作按钮
+.action-footer {
+  position: sticky;
+  bottom: 0;
+  background: #fff;
+  padding: 16px 0;
+  border-top: 1px solid #ebeef5;
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+</style>

+ 311 - 0
ems-ui-cloud/src/views/alarm/list/components/AlarmHandle.vue

@@ -0,0 +1,311 @@
+<template>
+  <el-dialog :title="dialogTitle" :visible.sync="visible" width="550px" append-to-body
+             :close-on-click-modal="false" @close="handleClose">
+    <div class="handle-dialog">
+      <!-- 告警信息摘要 -->
+      <div class="alarm-summary">
+        <div class="summary-header">
+          <el-tag :type="getAlarmLevelInfo(alarm.alarmLevel).type" size="small" effect="dark">
+            {{ getAlarmLevelInfo(alarm.alarmLevel).label }}
+          </el-tag>
+          <span class="summary-target">{{ alarm.targetName }}</span>
+        </div>
+        <div class="summary-content">
+          {{ alarm.alarmMsg || formatAlarmContent(alarm) }}
+        </div>
+        <div class="summary-info">
+          <span><i class="el-icon-time"></i> {{ alarm.alarmTime }}</span>
+          <span v-if="alarm.location"><i class="el-icon-location-outline"></i> {{ alarm.location }}</span>
+        </div>
+      </div>
+
+      <!-- 处置表单 -->
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" class="handle-form">
+        <!-- 确认操作 -->
+        <template v-if="handleMode === 'confirm'">
+          <el-form-item label="确认备注" prop="remark">
+            <el-input v-model="form.remark" type="textarea" :rows="3"
+                      placeholder="请输入确认备注(可选)" maxlength="500" show-word-limit />
+          </el-form-item>
+        </template>
+
+        <!-- 处置操作 -->
+        <template v-if="handleMode === 'handle'">
+          <el-form-item label="处置内容" prop="handleContent">
+            <el-input v-model="form.handleContent" type="textarea" :rows="3"
+                      placeholder="请描述处置措施和操作" maxlength="1000" show-word-limit />
+          </el-form-item>
+          <el-form-item label="处置结果" prop="handleResult">
+            <el-select v-model="form.handleResult" placeholder="请选择处置结果" style="width: 100%">
+              <el-option label="处置中" value="处置中" />
+              <el-option label="已处置" value="已处置" />
+              <el-option label="需跟进" value="需跟进" />
+              <el-option label="转派他人" value="转派他人" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="是否解决">
+            <el-switch v-model="form.markResolved" active-text="是" inactive-text="否" />
+            <span class="form-tip">勾选后将同时标记告警为已解决</span>
+          </el-form-item>
+        </template>
+
+        <!-- 解决操作 -->
+        <template v-if="handleMode === 'resolve'">
+          <el-form-item label="解决方式" prop="resolveType">
+            <el-radio-group v-model="form.resolveType">
+              <el-radio label="fixed">已修复问题</el-radio>
+              <el-radio label="false_alarm">误报/已确认正常</el-radio>
+              <el-radio label="other">其他</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="解决说明" prop="resolveRemark">
+            <el-input v-model="form.resolveRemark" type="textarea" :rows="3"
+                      placeholder="请描述解决方案或情况说明" maxlength="500" show-word-limit />
+          </el-form-item>
+        </template>
+
+        <!-- 关闭操作 -->
+        <template v-if="handleMode === 'close'">
+          <el-form-item label="关闭原因" prop="closeReason">
+            <el-select v-model="form.closeReason" placeholder="请选择关闭原因" style="width: 100%">
+              <el-option label="误报告警" value="误报告警" />
+              <el-option label="重复告警" value="重复告警" />
+              <el-option label="测试告警" value="测试告警" />
+              <el-option label="已在其他渠道处理" value="已在其他渠道处理" />
+              <el-option label="其他原因" value="其他原因" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="详细说明" prop="closeRemark">
+            <el-input v-model="form.closeRemark" type="textarea" :rows="3"
+                      placeholder="请补充说明关闭原因" maxlength="500" show-word-limit />
+          </el-form-item>
+        </template>
+      </el-form>
+    </div>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button :type="submitBtnType" @click="submitForm" :loading="submitting">
+        {{ submitBtnText }}
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { confirmAlarm, handleAlarm, resolveAlarm, closeAlarm } from '@/api/alarm/alarm'
+import { getAlarmLevelInfo } from '@/enums/alarmEnum'
+
+export default {
+  name: 'AlarmHandle',
+  data() {
+    return {
+      visible: false,
+      submitting: false,
+      handleMode: 'handle', // confirm, handle, resolve, close
+      alarm: {},
+      form: {
+        remark: '',
+        handleContent: '',
+        handleResult: '处置中',
+        markResolved: false,
+        resolveType: 'fixed',
+        resolveRemark: '',
+        closeReason: '',
+        closeRemark: '',
+        attachments: []
+      },
+      rules: {
+        handleContent: [
+          { required: true, message: '请输入处置内容', trigger: 'blur' }
+        ],
+        resolveRemark: [
+          { required: true, message: '请输入解决说明', trigger: 'blur' }
+        ],
+        closeReason: [
+          { required: true, message: '请选择关闭原因', trigger: 'change' }
+        ]
+      }
+    }
+  },
+  computed: {
+    dialogTitle() {
+      const titles = {
+        confirm: '确认告警',
+        handle: '处置告警',
+        resolve: '解决告警',
+        close: '关闭告警'
+      }
+      return titles[this.handleMode] || '处置告警'
+    },
+    submitBtnType() {
+      const types = {
+        confirm: 'warning',
+        handle: 'primary',
+        resolve: 'success',
+        close: 'info'
+      }
+      return types[this.handleMode] || 'primary'
+    },
+    submitBtnText() {
+      const texts = {
+        confirm: '确 认',
+        handle: '提交处置',
+        resolve: '标记解决',
+        close: '关闭告警'
+      }
+      return texts[this.handleMode] || '确 定'
+    }
+  },
+  methods: {
+    getAlarmLevelInfo,
+
+    open(alarm, mode = 'handle') {
+      this.alarm = { ...alarm }
+      this.handleMode = mode
+      this.resetForm()
+      this.visible = true
+    },
+
+    resetForm() {
+      this.form = {
+        remark: '',
+        handleContent: '',
+        handleResult: '处置中',
+        markResolved: false,
+        resolveType: 'fixed',
+        resolveRemark: '',
+        closeReason: '',
+        closeRemark: '',
+        attachments: []
+      }
+      this.$nextTick(() => {
+        this.$refs.form && this.$refs.form.clearValidate()
+      })
+    },
+
+    handleClose() {
+      this.visible = false
+      this.resetForm()
+    },
+
+    formatAlarmContent(alarm) {
+      if (!alarm.targetName) return ''
+      const attrName = alarm.attrName || alarm.attrKey || '属性'
+      return `${alarm.targetName} ${attrName}异常`
+    },
+
+    handleFileChange(file, fileList) {
+      this.form.attachments = fileList
+    },
+
+    handleFileRemove(file, fileList) {
+      this.form.attachments = fileList
+    },
+
+    submitForm() {
+      this.$refs.form.validate(async (valid) => {
+        if (!valid) return
+
+        this.submitting = true
+        try {
+          switch (this.handleMode) {
+            case 'confirm':
+              await confirmAlarm(this.alarm.alarmId, this.form.remark)
+              this.$modal.msgSuccess('确认成功')
+              break
+
+            case 'handle':
+              await handleAlarm(this.alarm.alarmId, this.form.handleContent, this.form.handleResult)
+              if (this.form.markResolved) {
+                await resolveAlarm(this.alarm.alarmId, '处置后解决')
+              }
+              this.$modal.msgSuccess('处置成功')
+              break
+
+            case 'resolve':
+              const resolveRemark = `[${this.form.resolveType === 'fixed' ? '已修复' :
+                this.form.resolveType === 'false_alarm' ? '误报' : '其他'}] ${this.form.resolveRemark}`
+              await resolveAlarm(this.alarm.alarmId, resolveRemark)
+              this.$modal.msgSuccess('已标记解决')
+              break
+
+            case 'close':
+              const closeRemark = `[${this.form.closeReason}] ${this.form.closeRemark || ''}`
+              await closeAlarm(this.alarm.alarmId, closeRemark)
+              this.$modal.msgSuccess('已关闭告警')
+              break
+          }
+
+          this.handleClose()
+          this.$emit('success')
+        } catch (err) {
+          this.$modal.msgError(err.message || '操作失败')
+        } finally {
+          this.submitting = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.handle-dialog {
+  padding: 0 10px;
+}
+
+// 告警摘要
+.alarm-summary {
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 16px;
+  margin-bottom: 20px;
+
+  .summary-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .summary-target {
+      margin-left: 10px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .summary-content {
+    font-size: 14px;
+    color: #606266;
+    line-height: 1.5;
+    margin-bottom: 10px;
+  }
+
+  .summary-info {
+    display: flex;
+    gap: 16px;
+    font-size: 12px;
+    color: #909399;
+
+    i {
+      margin-right: 4px;
+    }
+  }
+}
+
+// 处置表单
+.handle-form {
+  .form-tip {
+    font-size: 12px;
+    color: #909399;
+    margin-left: 10px;
+  }
+}
+
+// 文件上传
+.handle-upload {
+  ::v-deep .el-upload-list {
+    margin-top: 10px;
+  }
+}
+</style>

+ 484 - 0
ems-ui-cloud/src/views/alarm/list/components/AlarmPanel.vue

@@ -0,0 +1,484 @@
+<template>
+  <div class="alarm-panel" :class="{ 'has-urgent': urgentCount > 0 }">
+    <!-- 面板头部 -->
+    <div class="panel-header">
+      <div class="header-left">
+        <span class="panel-title">
+          <i class="el-icon-bell" :class="{ 'shake': urgentCount > 0 }"></i>
+          实时告警
+        </span>
+        <el-badge :value="activeAlarms.length" :max="99" class="alarm-badge" v-if="activeAlarms.length > 0" />
+      </div>
+      <div class="header-right">
+        <el-tooltip content="刷新" placement="top">
+          <el-button type="text" icon="el-icon-refresh" :loading="loading" @click="loadAlarms" />
+        </el-tooltip>
+        <el-tooltip content="查看全部" placement="top">
+          <el-button type="text" icon="el-icon-more" @click="goToList" />
+        </el-tooltip>
+      </div>
+    </div>
+
+    <!-- 紧急告警提示 -->
+    <div v-if="urgentCount > 0" class="urgent-alert">
+      <i class="el-icon-warning"></i>
+      <span>有 {{ urgentCount }} 条紧急告警需要处理!</span>
+      <el-button type="text" size="mini" @click="filterUrgent">立即查看</el-button>
+    </div>
+
+    <!-- 快速统计 -->
+    <div class="quick-stats">
+      <div class="stat-item" @click="filterByLevel(null)">
+        <span class="stat-num">{{ activeAlarms.length }}</span>
+        <span class="stat-label">活动</span>
+      </div>
+      <div class="stat-item urgent" @click="filterByLevel(3)">
+        <span class="stat-num">{{ urgentCount }}</span>
+        <span class="stat-label">紧急</span>
+      </div>
+      <div class="stat-item important" @click="filterByLevel(2)">
+        <span class="stat-num">{{ importantCount }}</span>
+        <span class="stat-label">重要</span>
+      </div>
+      <div class="stat-item normal" @click="filterByLevel(1)">
+        <span class="stat-num">{{ normalCount }}</span>
+        <span class="stat-label">一般</span>
+      </div>
+    </div>
+
+    <!-- 告警列表 -->
+    <div class="alarm-list" v-loading="loading">
+      <div v-if="displayAlarms.length === 0" class="empty-state">
+        <i class="el-icon-check"></i>
+        <span>暂无活动告警</span>
+      </div>
+
+      <transition-group name="alarm-item" tag="div" v-else>
+        <div v-for="alarm in displayAlarms" :key="alarm.alarmId"
+             class="alarm-item" :class="getAlarmClass(alarm)" @click="handleClick(alarm)">
+          <div class="alarm-level">
+            <el-tag :type="getAlarmLevelInfo(alarm.alarmLevel).type" size="mini" effect="dark">
+              {{ getAlarmLevelInfo(alarm.alarmLevel).label }}
+            </el-tag>
+          </div>
+          <div class="alarm-info">
+            <div class="alarm-target">{{ alarm.targetName }}</div>
+            <div class="alarm-msg">{{ alarm.alarmMsg || formatAlarmContent(alarm) }}</div>
+          </div>
+          <div class="alarm-time">
+            <span>{{ formatTimeAgo(alarm.alarmTime) }}</span>
+            <el-badge v-if="alarm.repeatCount > 1" :value="alarm.repeatCount" type="warning" class="repeat-badge" />
+          </div>
+          <div class="alarm-action">
+            <el-button type="text" size="mini" @click.stop="handleQuickConfirm(alarm)"
+                       v-if="alarm.alarmStatus === 0">确认</el-button>
+          </div>
+        </div>
+      </transition-group>
+    </div>
+
+    <!-- 查看更多 -->
+    <div class="panel-footer" v-if="activeAlarms.length > maxDisplay">
+      <el-button type="text" @click="goToList">
+        查看全部 {{ activeAlarms.length }} 条告警
+        <i class="el-icon-arrow-right"></i>
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listActiveAlarm, confirmAlarm } from '@/api/alarm/alarm'
+import { getAlarmLevelInfo } from '@/enums/alarmEnum'
+import { parseTime } from '@/utils/ruoyi'
+
+export default {
+  name: 'AlarmPanel',
+  props: {
+    // 最大显示条数
+    maxDisplay: {
+      type: Number,
+      default: 5
+    },
+    // 区域代码
+    areaCode: {
+      type: String,
+      default: ''
+    },
+    // 自动刷新间隔(ms),0表示不自动刷新
+    autoRefresh: {
+      type: Number,
+      default: 30000
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      activeAlarms: [],
+      filterLevel: null,
+      refreshTimer: null
+    }
+  },
+  computed: {
+    displayAlarms() {
+      let list = this.activeAlarms
+      if (this.filterLevel !== null) {
+        list = list.filter(a => a.alarmLevel === this.filterLevel)
+      }
+      return list.slice(0, this.maxDisplay)
+    },
+    urgentCount() {
+      return this.activeAlarms.filter(a => a.alarmLevel === 3).length
+    },
+    importantCount() {
+      return this.activeAlarms.filter(a => a.alarmLevel === 2).length
+    },
+    normalCount() {
+      return this.activeAlarms.filter(a => a.alarmLevel === 1).length
+    }
+  },
+  watch: {
+    areaCode() {
+      this.loadAlarms()
+    }
+  },
+  created() {
+    this.loadAlarms()
+    this.startAutoRefresh()
+  },
+  beforeDestroy() {
+    this.stopAutoRefresh()
+  },
+  methods: {
+    getAlarmLevelInfo,
+
+    /** 加载活动告警 */
+    async loadAlarms() {
+      this.loading = true
+      try {
+        const res = await listActiveAlarm(this.areaCode)
+        this.activeAlarms = res.data || []
+        // 按级别和时间排序
+        this.activeAlarms.sort((a, b) => {
+          if (b.alarmLevel !== a.alarmLevel) {
+            return b.alarmLevel - a.alarmLevel
+          }
+          return new Date(b.alarmTime) - new Date(a.alarmTime)
+        })
+      } catch (e) {
+        console.error('加载活动告警失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+
+    /** 启动自动刷新 */
+    startAutoRefresh() {
+      if (this.autoRefresh > 0) {
+        this.refreshTimer = setInterval(() => {
+          this.loadAlarms()
+        }, this.autoRefresh)
+      }
+    },
+
+    /** 停止自动刷新 */
+    stopAutoRefresh() {
+      if (this.refreshTimer) {
+        clearInterval(this.refreshTimer)
+        this.refreshTimer = null
+      }
+    },
+
+    /** 获取告警样式类 */
+    getAlarmClass(alarm) {
+      if (alarm.alarmLevel === 3) return 'urgent'
+      if (alarm.alarmLevel === 2) return 'important'
+      return 'normal'
+    },
+
+    /** 格式化告警内容 */
+    formatAlarmContent(alarm) {
+      const attrName = alarm.attrName || alarm.attrKey || '属性'
+      return `${attrName}异常`
+    },
+
+    /** 格式化相对时间 */
+    formatTimeAgo(time) {
+      if (!time) return ''
+      const now = new Date()
+      const past = new Date(time)
+      const diff = Math.floor((now - past) / 1000)
+
+      if (diff < 60) return '刚刚'
+      if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
+      if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
+      if (diff < 604800) return `${Math.floor(diff / 86400)}天前`
+      return parseTime(time, '{m}-{d} {h}:{i}')
+    },
+
+    /** 按级别筛选 */
+    filterByLevel(level) {
+      this.filterLevel = this.filterLevel === level ? null : level
+    },
+
+    /** 筛选紧急告警 */
+    filterUrgent() {
+      this.filterLevel = 3
+    },
+
+    /** 点击告警项 */
+    handleClick(alarm) {
+      this.$emit('click', alarm)
+    },
+
+    /** 快速确认 */
+    async handleQuickConfirm(alarm) {
+      try {
+        await confirmAlarm(alarm.alarmId, '快速确认')
+        this.$message.success('确认成功')
+        this.loadAlarms()
+      } catch (e) {
+        this.$message.error('确认失败')
+      }
+    },
+
+    /** 跳转到告警列表 */
+    goToList() {
+      this.$router.push({
+        path: '/alarm',
+        query: this.filterLevel ? { alarmLevel: this.filterLevel } : {}
+      })
+    },
+
+    /** 刷新 */
+    refresh() {
+      this.loadAlarms()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alarm-panel {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+
+  &.has-urgent {
+    border: 1px solid rgba(245, 108, 108, 0.3);
+  }
+}
+
+// 头部
+.panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px;
+  border-bottom: 1px solid #ebeef5;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+  }
+
+  .panel-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #f56c6c;
+
+      &.shake {
+        animation: shake 0.5s infinite;
+      }
+    }
+  }
+
+  .alarm-badge {
+    margin-left: 8px;
+  }
+}
+
+@keyframes shake {
+  0%, 100% { transform: rotate(0deg); }
+  25% { transform: rotate(-10deg); }
+  75% { transform: rotate(10deg); }
+}
+
+// 紧急提示
+.urgent-alert {
+  background: linear-gradient(90deg, #fef0f0 0%, #fff 100%);
+  padding: 10px 16px;
+  display: flex;
+  align-items: center;
+  color: #f56c6c;
+  font-size: 13px;
+
+  i {
+    margin-right: 8px;
+    font-size: 16px;
+    animation: pulse 1s infinite;
+  }
+
+  span {
+    flex: 1;
+  }
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+// 快速统计
+.quick-stats {
+  display: flex;
+  padding: 12px 16px;
+  border-bottom: 1px solid #ebeef5;
+  background: #fafafa;
+
+  .stat-item {
+    flex: 1;
+    text-align: center;
+    cursor: pointer;
+    padding: 8px;
+    border-radius: 4px;
+    transition: all 0.3s;
+
+    &:hover {
+      background: #fff;
+    }
+
+    .stat-num {
+      display: block;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    .stat-label {
+      font-size: 12px;
+      color: #909399;
+    }
+
+    &.urgent .stat-num { color: #f56c6c; }
+    &.important .stat-num { color: #e6a23c; }
+    &.normal .stat-num { color: #409eff; }
+  }
+}
+
+// 告警列表
+.alarm-list {
+  padding: 8px 0;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 40px 20px;
+  color: #c0c4cc;
+
+  i {
+    font-size: 48px;
+    display: block;
+    margin-bottom: 10px;
+    color: #67c23a;
+  }
+}
+
+.alarm-item {
+  display: flex;
+  align-items: center;
+  padding: 12px 16px;
+  cursor: pointer;
+  transition: all 0.3s;
+  border-left: 3px solid transparent;
+
+  &:hover {
+    background: #f5f7fa;
+  }
+
+  &.urgent {
+    border-left-color: #f56c6c;
+    background: rgba(245, 108, 108, 0.05);
+  }
+
+  &.important {
+    border-left-color: #e6a23c;
+  }
+
+  &.normal {
+    border-left-color: #409eff;
+  }
+
+  .alarm-level {
+    margin-right: 12px;
+  }
+
+  .alarm-info {
+    flex: 1;
+    min-width: 0;
+
+    .alarm-target {
+      font-weight: 500;
+      color: #303133;
+      font-size: 14px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
+    .alarm-msg {
+      font-size: 12px;
+      color: #909399;
+      margin-top: 4px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .alarm-time {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 12px;
+    color: #c0c4cc;
+    margin: 0 12px;
+    white-space: nowrap;
+  }
+
+  .alarm-action {
+    opacity: 0;
+    transition: opacity 0.3s;
+  }
+
+  &:hover .alarm-action {
+    opacity: 1;
+  }
+}
+
+// 列表动画
+.alarm-item-enter-active,
+.alarm-item-leave-active {
+  transition: all 0.3s;
+}
+
+.alarm-item-enter,
+.alarm-item-leave-to {
+  opacity: 0;
+  transform: translateX(-20px);
+}
+
+// 底部
+.panel-footer {
+  text-align: center;
+  padding: 12px;
+  border-top: 1px solid #ebeef5;
+}
+</style>

+ 489 - 0
ems-ui-cloud/src/views/alarm/list/components/AlarmReport.vue

@@ -0,0 +1,489 @@
+<template>
+  <el-dialog title="手动上报告警" :visible.sync="visible" width="650px" append-to-body
+             :close-on-click-modal="false" @close="handleClose">
+    <el-form ref="form" :model="form" :rules="rules" label-width="100px" v-loading="formLoading">
+      <!-- 目标选择 -->
+      <el-divider content-position="left"><span class="divider-title">告警目标</span></el-divider>
+
+      <el-form-item label="目标类型" prop="targetType">
+        <el-radio-group v-model="form.targetType" @change="handleTargetTypeChange">
+          <el-radio-button :label="2">设备</el-radio-button>
+          <el-radio-button :label="1">设施</el-radio-button>
+          <el-radio-button :label="0">区域</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item label="所属区域" prop="areaCode">
+        <el-cascader v-model="form.areaCode" :options="areaOptions" :props="areaCascaderProps"
+                     placeholder="请选择区域" clearable filterable style="width: 100%"
+                     @change="handleAreaChange" />
+      </el-form-item>
+
+      <!-- 设备选择 - 改为先加载区域下设备列表,支持搜索过滤 -->
+      <el-form-item v-if="form.targetType === 2" label="选择设备" prop="targetCode">
+        <el-select v-model="form.targetCode" placeholder="请先选择区域,或直接搜索设备"
+                   filterable :filter-method="filterDevice"
+                   :loading="deviceLoading" style="width: 100%"
+                   @change="handleDeviceChange" @focus="handleDeviceFocus">
+          <el-option v-for="item in filteredDeviceOptions" :key="item.deviceCode"
+                     :label="item.deviceName" :value="item.deviceCode">
+            <div class="device-option">
+              <span class="device-name">{{ item.deviceName }}</span>
+              <span class="device-location">{{ item.location || item.areaPath }}</span>
+            </div>
+          </el-option>
+          <template #empty>
+            <div class="empty-tip">
+              <span v-if="!form.areaCode">请先选择区域</span>
+              <span v-else-if="deviceLoading">加载中...</span>
+              <span v-else>该区域下暂无设备</span>
+            </div>
+          </template>
+        </el-select>
+      </el-form-item>
+
+      <!-- 设施选择 - 支持全量加载或按区域过滤 -->
+      <el-form-item v-if="form.targetType === 1" label="选择设施" prop="targetCode">
+        <el-select v-model="form.targetCode" placeholder="请选择设施"
+                   filterable style="width: 100%"
+                   :loading="facsLoading"
+                   @change="handleFacsChange">
+          <el-option v-for="item in filteredFacsOptions" :key="item.facsCode"
+                     :label="item.facsName" :value="item.facsCode">
+            <div class="facs-option">
+              <span class="facs-name">{{ item.facsName }}</span>
+              <span class="facs-area">{{ item.refAreaName }}</span>
+            </div>
+          </el-option>
+          <template #empty>
+            <div class="empty-tip">
+              <span v-if="facsLoading">加载中...</span>
+              <span v-else>暂无设施数据</span>
+            </div>
+          </template>
+        </el-select>
+      </el-form-item>
+
+      <!-- 目标名称:设备/设施选择后自动填充,区域类型时也自动填充 -->
+      <el-form-item label="目标名称" prop="targetName">
+        <el-input v-model="form.targetName" placeholder="选择设备/设施后自动填充"
+                  :disabled="form.targetType !== 0 && !!form.targetCode" />
+      </el-form-item>
+
+      <el-form-item label="位置信息">
+        <el-input v-model="form.location" placeholder="设备安装位置" />
+      </el-form-item>
+
+      <!-- 告警信息 -->
+      <el-divider content-position="left"><span class="divider-title">告警信息</span></el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="告警级别" prop="alarmLevel">
+            <el-select v-model="form.alarmLevel" placeholder="请选择级别" style="width: 100%">
+              <el-option v-for="item in ALARM_LEVEL_OPTIONS" :key="item.value"
+                         :label="item.label" :value="item.value">
+                <span :style="{ color: item.color }">
+                  <i class="el-icon-warning-outline"></i> {{ item.label }}
+                </span>
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="告警代码">
+            <el-input v-model="form.alarmCode" placeholder="告警分类代码(可选)" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="告警属性">
+            <el-input v-model="form.attrKey" placeholder="如:temperature" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="属性名称">
+            <el-input v-model="form.attrName" placeholder="如:温度" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="当前值">
+            <el-input v-model="form.attrValue" placeholder="告警时的属性值" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="阈值">
+            <el-input v-model="form.thresholdValue" placeholder="正常范围或阈值" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-form-item label="告警描述" prop="alarmMsg">
+        <el-input v-model="form.alarmMsg" type="textarea" :rows="3"
+                  placeholder="请详细描述告警情况" maxlength="500" show-word-limit />
+      </el-form-item>
+
+      <el-form-item label="子系统">
+        <el-select v-model="form.subsystemCode" placeholder="请选择所属子系统" clearable style="width: 100%">
+          <el-option v-for="item in subsystemOptions" :key="item.systemCode"
+                     :label="item.systemName" :value="item.systemCode" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button type="danger" @click="submitForm" :loading="submitting">
+        <i class="el-icon-bell"></i> 立即上报
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { reportAlarm } from '@/api/alarm/alarm'
+import { areaTreeSelect } from '@/api/basecfg/area'
+import { listDevRecursionByArea } from '@/api/device/device'
+import { listAllFacs } from '@/api/basecfg/emsfacs'
+import { listSubsystem } from '@/api/adapter/subsystem'
+import { ALARM_LEVEL_OPTIONS } from '@/enums/alarmEnum'
+
+export default {
+  name: 'AlarmReport',
+  data() {
+    return {
+      visible: false,
+      formLoading: false,
+      submitting: false,
+      deviceLoading: false,
+      facsLoading: false,
+      form: {
+        targetType: 2,
+        areaCode: null,
+        targetCode: '',
+        targetName: '',
+        deviceModel: '',
+        location: '',
+        alarmLevel: 2,
+        alarmCode: '',
+        attrKey: '',
+        attrName: '',
+        attrValue: '',
+        thresholdValue: '',
+        alarmMsg: '',
+        subsystemCode: ''
+      },
+      rules: {
+        targetType: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
+        areaCode: [{ required: true, message: '请选择所属区域', trigger: 'change' }],
+        targetCode: [{ required: true, message: '请选择告警目标', trigger: 'change' }],
+        targetName: [{ required: true, message: '请输入目标名称', trigger: 'blur' }],
+        alarmLevel: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
+        alarmMsg: [{ required: true, message: '请输入告警描述', trigger: 'blur' }]
+      },
+      ALARM_LEVEL_OPTIONS,
+      areaOptions: [],
+      areaCascaderProps: {
+        value: 'id',
+        label: 'label',
+        children: 'children',
+        checkStrictly: true,
+        emitPath: false
+      },
+      // 设备相关
+      deviceOptions: [],         // 原始设备列表
+      filteredDeviceOptions: [], // 过滤后的设备列表
+      deviceFilterText: '',      // 设备搜索关键字
+      // 设施相关
+      allFacsOptions: [],        // 全部设施列表(缓存)
+      filteredFacsOptions: [],   // 过滤后的设施列表
+      // 子系统
+      subsystemOptions: []
+    }
+  },
+  created() {
+    this.loadAreas()
+    this.loadSubsystems()
+    this.loadAllFacs() // 预加载全部设施
+  },
+  methods: {
+    async loadAreas() {
+      try {
+        const res = await areaTreeSelect('0', 3)
+        this.areaOptions = res.data || []
+      } catch (e) {
+        console.error('加载区域失败', e)
+      }
+    },
+
+    async loadSubsystems() {
+      try {
+        const res = await listSubsystem({})
+        this.subsystemOptions = res.rows || res.data || []
+      } catch (e) {
+        console.error('加载子系统失败', e)
+      }
+    },
+
+    // 预加载全部设施列表
+    async loadAllFacs() {
+      try {
+        this.facsLoading = true
+        const res = await listAllFacs({})
+        this.allFacsOptions = res.data || res.rows || []
+        this.filteredFacsOptions = [...this.allFacsOptions]
+      } catch (e) {
+        console.error('加载设施列表失败', e)
+      } finally {
+        this.facsLoading = false
+      }
+    },
+
+    // 根据区域加载设备列表
+    async loadDevicesByArea(areaCode) {
+      if (!areaCode) {
+        this.deviceOptions = []
+        this.filteredDeviceOptions = []
+        return
+      }
+
+      this.deviceLoading = true
+      try {
+        const params = {
+          areaCode: areaCode,
+          pageNum: 1,
+          pageSize: 9999  // 获取区域下所有设备
+        }
+        const res = await listDevRecursionByArea(params)
+        this.deviceOptions = res.rows || []
+        this.filteredDeviceOptions = [...this.deviceOptions]
+      } catch (e) {
+        console.error('加载设备列表失败', e)
+        this.deviceOptions = []
+        this.filteredDeviceOptions = []
+      } finally {
+        this.deviceLoading = false
+      }
+    },
+
+    open() {
+      this.resetForm()
+      this.visible = true
+    },
+
+    resetForm() {
+      this.form = {
+        targetType: 2,
+        areaCode: null,
+        targetCode: '',
+        targetName: '',
+        deviceModel: '',
+        location: '',
+        alarmLevel: 2,
+        alarmCode: '',
+        attrKey: '',
+        attrName: '',
+        attrValue: '',
+        thresholdValue: '',
+        alarmMsg: '',
+        subsystemCode: ''
+      }
+      this.deviceOptions = []
+      this.filteredDeviceOptions = []
+      this.deviceFilterText = ''
+      // 重置设施过滤
+      this.filteredFacsOptions = [...this.allFacsOptions]
+      this.$nextTick(() => {
+        this.$refs.form && this.$refs.form.clearValidate()
+      })
+    },
+
+    handleClose() {
+      this.visible = false
+      this.resetForm()
+    },
+
+    handleTargetTypeChange(val) {
+      this.form.targetCode = ''
+      this.form.targetName = ''
+      this.form.location = ''
+      this.form.deviceModel = ''
+      this.deviceOptions = []
+      this.filteredDeviceOptions = []
+
+      // 切换到设施类型时,根据已选区域过滤设施
+      if (val === 1) {
+        this.filterFacsByArea(this.form.areaCode)
+      }
+
+      // 切换到设备类型时,如果已选区域则加载设备
+      if (val === 2 && this.form.areaCode) {
+        this.loadDevicesByArea(this.form.areaCode)
+      }
+
+      // 切换到区域类型时,自动填充
+      if (val === 0 && this.form.areaCode) {
+        this.form.targetCode = this.form.areaCode
+        const areaName = this.findAreaName(this.areaOptions, this.form.areaCode)
+        this.form.targetName = areaName || this.form.areaCode
+      }
+    },
+
+    async handleAreaChange(val) {
+      this.form.targetCode = ''
+      this.form.targetName = ''
+      this.form.location = ''
+
+      if (!val) {
+        this.deviceOptions = []
+        this.filteredDeviceOptions = []
+        this.filteredFacsOptions = [...this.allFacsOptions]
+        return
+      }
+
+      // 设备类型:加载区域下的设备
+      if (this.form.targetType === 2) {
+        await this.loadDevicesByArea(val)
+      }
+
+      // 设施类型:根据区域过滤设施
+      if (this.form.targetType === 1) {
+        this.filterFacsByArea(val)
+      }
+
+      // 区域类型:直接使用区域信息
+      if (this.form.targetType === 0) {
+        this.form.targetCode = val
+        const areaName = this.findAreaName(this.areaOptions, val)
+        this.form.targetName = areaName || val
+      }
+    },
+
+    // 根据区域过滤设施
+    filterFacsByArea(areaCode) {
+      if (!areaCode) {
+        this.filteredFacsOptions = [...this.allFacsOptions]
+      } else {
+        this.filteredFacsOptions = this.allFacsOptions.filter(
+          facs => facs.refArea === areaCode
+        )
+      }
+    },
+
+    findAreaName(tree, code) {
+      for (const node of tree) {
+        if (node.id === code) return node.label
+        if (node.children && node.children.length > 0) {
+          const found = this.findAreaName(node.children, code)
+          if (found) return found
+        }
+      }
+      return null
+    },
+
+    // 设备下拉框获得焦点时
+    handleDeviceFocus() {
+      // 如果已选区域但设备列表为空,则加载
+      if (this.form.areaCode && this.deviceOptions.length === 0) {
+        this.loadDevicesByArea(this.form.areaCode)
+      }
+    },
+
+    // 设备本地过滤
+    filterDevice(query) {
+      this.deviceFilterText = query
+      if (!query) {
+        this.filteredDeviceOptions = [...this.deviceOptions]
+      } else {
+        const keyword = query.toLowerCase()
+        this.filteredDeviceOptions = this.deviceOptions.filter(device => {
+          return (device.deviceName && device.deviceName.toLowerCase().includes(keyword)) ||
+            (device.deviceCode && device.deviceCode.toLowerCase().includes(keyword)) ||
+            (device.location && device.location.toLowerCase().includes(keyword))
+        })
+      }
+    },
+
+    handleDeviceChange(val) {
+      const device = this.deviceOptions.find(d => d.deviceCode === val)
+      if (device) {
+        this.form.targetName = device.deviceName
+        this.form.location = device.location
+        this.form.deviceModel = device.deviceModel
+        this.form.subsystemCode = device.subsystemCode
+      }
+    },
+
+    handleFacsChange(val) {
+      const facs = this.allFacsOptions.find(f => f.facsCode === val)
+      if (facs) {
+        this.form.targetName = facs.facsName
+        // 如果未选择区域,自动填充设施所属区域
+        if (!this.form.areaCode && facs.refArea) {
+          this.form.areaCode = facs.refArea
+        }
+      }
+    },
+
+    submitForm() {
+      this.$refs.form.validate(async (valid) => {
+        if (!valid) return
+
+        this.submitting = true
+        try {
+          await reportAlarm(this.form)
+          this.$modal.msgSuccess('告警上报成功')
+          this.handleClose()
+          this.$emit('success')
+        } catch (err) {
+          this.$modal.msgError(err.message || '上报失败')
+        } finally {
+          this.submitting = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.divider-title {
+  font-weight: 600;
+  color: #f56c6c;
+  font-size: 14px;
+}
+
+.device-option,
+.facs-option {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .device-name,
+  .facs-name {
+    color: #303133;
+  }
+
+  .device-location,
+  .facs-area {
+    font-size: 12px;
+    color: #909399;
+    max-width: 180px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+
+.empty-tip {
+  padding: 10px 0;
+  text-align: center;
+  color: #909399;
+  font-size: 14px;
+}
+</style>

+ 529 - 0
ems-ui-cloud/src/views/alarm/list/components/AlarmStats.vue

@@ -0,0 +1,529 @@
+<template>
+  <div class="alarm-stats-container">
+    <!-- 统计类型切换 -->
+    <div class="stats-header" v-if="showHeader">
+      <span class="stats-title">
+        <i class="el-icon-data-analysis"></i> {{ title }}
+      </span>
+      <el-radio-group v-model="activeType" size="mini" @change="handleTypeChange">
+        <el-radio-button label="level">级别分布</el-radio-button>
+        <el-radio-button label="status">状态分布</el-radio-button>
+        <el-radio-button label="trend">趋势分析</el-radio-button>
+        <el-radio-button label="subsystem">子系统分布</el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <!-- 图表区域 -->
+    <div class="chart-wrapper" v-loading="loading">
+      <!-- 级别分布饼图 -->
+      <div v-show="activeType === 'level'" ref="levelChart" class="chart-area"></div>
+
+      <!-- 状态分布饼图 -->
+      <div v-show="activeType === 'status'" ref="statusChart" class="chart-area"></div>
+
+      <!-- 趋势折线图 -->
+      <div v-show="activeType === 'trend'" ref="trendChart" class="chart-area"></div>
+
+      <!-- 子系统柱状图 -->
+      <div v-show="activeType === 'subsystem'" ref="subsystemChart" class="chart-area"></div>
+
+      <!-- 无数据提示 -->
+      <div v-if="noData" class="no-data">
+        <i class="el-icon-document-remove"></i>
+        <span>暂无数据</span>
+      </div>
+    </div>
+
+    <!-- 底部汇总 -->
+    <div class="stats-summary" v-if="showSummary">
+      <div class="summary-item">
+        <span class="summary-label">告警总数</span>
+        <span class="summary-value">{{ summaryData.total || 0 }}</span>
+      </div>
+      <div class="summary-item danger">
+        <span class="summary-label">活动告警</span>
+        <span class="summary-value">{{ summaryData.active || 0 }}</span>
+      </div>
+      <div class="summary-item warning">
+        <span class="summary-label">今日新增</span>
+        <span class="summary-value">{{ summaryData.today || 0 }}</span>
+      </div>
+      <div class="summary-item success">
+        <span class="summary-label">处理率</span>
+        <span class="summary-value">{{ summaryData.handleRate || 0 }}%</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import {
+  countAlarmByLevel, countAlarmByStatus, countAlarmByTrend, countAlarmBySubsystem,
+  getAlarmStatsOverview
+} from '@/api/alarm/alarm'
+import { ALARM_LEVEL_OPTIONS, ALARM_STATUS_OPTIONS } from '@/enums/alarmEnum'
+
+export default {
+  name: 'AlarmStats',
+  props: {
+    title: {
+      type: String,
+      default: '告警统计'
+    },
+    showHeader: {
+      type: Boolean,
+      default: true
+    },
+    showSummary: {
+      type: Boolean,
+      default: true
+    },
+    defaultType: {
+      type: String,
+      default: 'level'
+    },
+    areaCode: {
+      type: String,
+      default: ''
+    },
+    height: {
+      type: String,
+      default: '300px'
+    },
+    // 趋势图粒度:day-按小时, month-按天
+    trendGranularity: {
+      type: String,
+      default: 'day'
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      noData: false,
+      activeType: this.defaultType,
+      charts: {
+        level: null,
+        status: null,
+        trend: null,
+        subsystem: null
+      },
+      statsData: {
+        level: [],
+        status: [],
+        trend: [],
+        subsystem: []
+      },
+      summaryData: {
+        total: 0,
+        active: 0,
+        today: 0,
+        handleRate: 0
+      }
+    }
+  },
+  watch: {
+    areaCode() {
+      this.loadData()
+    }
+  },
+  mounted() {
+    this.initCharts()
+    this.loadData()
+    // 监听窗口大小变化
+    window.addEventListener('resize', this.resizeCharts)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeCharts)
+    this.disposeCharts()
+  },
+  methods: {
+    /** 初始化图表 */
+    initCharts() {
+      this.$nextTick(() => {
+        this.charts.level = echarts.init(this.$refs.levelChart)
+        this.charts.status = echarts.init(this.$refs.statusChart)
+        this.charts.trend = echarts.init(this.$refs.trendChart)
+        this.charts.subsystem = echarts.init(this.$refs.subsystemChart)
+      })
+    },
+
+    /** 销毁图表 */
+    disposeCharts() {
+      Object.values(this.charts).forEach(chart => {
+        chart && chart.dispose()
+      })
+    },
+
+    /** 调整图表大小 */
+    resizeCharts() {
+      Object.values(this.charts).forEach(chart => {
+        chart && chart.resize()
+      })
+    },
+
+    /** 加载数据 */
+    async loadData() {
+      this.loading = true
+      const query = { areaCode: this.areaCode }
+
+      try {
+        // 加载汇总数据
+        const overviewRes = await getAlarmStatsOverview(query)
+        const overview = overviewRes.data || {}
+        this.summaryData = {
+          total: overview.totalCount || 0,
+          active: overview.activeCount || 0,
+          today: overview.todayCount || 0,
+          handleRate: overview.handleRate?.rate || 0
+        }
+
+        // 根据当前类型加载对应数据
+        await this.loadChartData(this.activeType)
+      } catch (e) {
+        console.error('加载统计数据失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+
+    /** 加载图表数据 */
+    async loadChartData(type) {
+      const query = { areaCode: this.areaCode }
+
+      try {
+        switch (type) {
+          case 'level':
+            const levelRes = await countAlarmByLevel(query)
+            this.statsData.level = levelRes.data || []
+            this.renderLevelChart()
+            break
+
+          case 'status':
+            const statusRes = await countAlarmByStatus(query)
+            this.statsData.status = statusRes.data || []
+            this.renderStatusChart()
+            break
+
+          case 'trend':
+            const trendRes = await countAlarmByTrend(query, this.trendGranularity)
+            this.statsData.trend = trendRes.data || []
+            this.renderTrendChart()
+            break
+
+          case 'subsystem':
+            const subsystemRes = await countAlarmBySubsystem(query)
+            this.statsData.subsystem = subsystemRes.data || []
+            this.renderSubsystemChart()
+            break
+        }
+
+        this.noData = false
+      } catch (e) {
+        console.error('加载图表数据失败', e)
+        this.noData = true
+      }
+    },
+
+    /** 切换统计类型 */
+    handleTypeChange(type) {
+      this.loadChartData(type)
+    },
+
+    /** 渲染级别分布图 */
+    renderLevelChart() {
+      const data = this.statsData.level.map(item => {
+        const levelInfo = ALARM_LEVEL_OPTIONS.find(o => o.value === item.alarmLevel)
+        return {
+          name: levelInfo ? levelInfo.label : `级别${item.alarmLevel}`,
+          value: item.cnt,
+          itemStyle: { color: levelInfo ? levelInfo.color : '#909399' }
+        }
+      })
+
+      const option = {
+        tooltip: {
+          trigger: 'item',
+          formatter: '{b}: {c} ({d}%)'
+        },
+        legend: {
+          orient: 'vertical',
+          right: 20,
+          top: 'center'
+        },
+        series: [{
+          type: 'pie',
+          radius: ['40%', '70%'],
+          center: ['40%', '50%'],
+          avoidLabelOverlap: true,
+          itemStyle: {
+            borderRadius: 6,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: true,
+            formatter: '{b}\n{c}'
+          },
+          data: data
+        }]
+      }
+
+      this.charts.level && this.charts.level.setOption(option)
+    },
+
+    /** 渲染状态分布图 */
+    renderStatusChart() {
+      const data = this.statsData.status.map(item => {
+        const statusInfo = ALARM_STATUS_OPTIONS.find(o => o.value === item.alarmStatus)
+        return {
+          name: statusInfo ? statusInfo.label : `状态${item.alarmStatus}`,
+          value: item.cnt,
+          itemStyle: { color: statusInfo ? statusInfo.color : '#909399' }
+        }
+      })
+
+      const option = {
+        tooltip: {
+          trigger: 'item',
+          formatter: '{b}: {c} ({d}%)'
+        },
+        legend: {
+          orient: 'vertical',
+          right: 20,
+          top: 'center'
+        },
+        series: [{
+          type: 'pie',
+          radius: ['40%', '70%'],
+          center: ['40%', '50%'],
+          roseType: 'radius',
+          itemStyle: {
+            borderRadius: 6,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: true,
+            formatter: '{b}\n{c}'
+          },
+          data: data
+        }]
+      }
+
+      this.charts.status && this.charts.status.setOption(option)
+    },
+
+    /** 渲染趋势图 */
+    renderTrendChart() {
+      const xData = this.statsData.trend.map(item => item.timeIndex)
+      const yData = this.statsData.trend.map(item => item.cnt)
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: xData,
+          axisLine: {
+            lineStyle: { color: '#dcdfe6' }
+          },
+          axisLabel: {
+            color: '#606266',
+            fontSize: 11
+          }
+        },
+        yAxis: {
+          type: 'value',
+          axisLine: { show: false },
+          axisTick: { show: false },
+          splitLine: {
+            lineStyle: { color: '#ebeef5', type: 'dashed' }
+          },
+          axisLabel: {
+            color: '#909399'
+          }
+        },
+        series: [{
+          name: '告警数',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 6,
+          sampling: 'average',
+          itemStyle: {
+            color: '#f56c6c'
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(245, 108, 108, 0.3)' },
+              { offset: 1, color: 'rgba(245, 108, 108, 0.05)' }
+            ])
+          },
+          data: yData
+        }]
+      }
+
+      this.charts.trend && this.charts.trend.setOption(option)
+    },
+
+    /** 渲染子系统分布图 */
+    renderSubsystemChart() {
+      const data = this.statsData.subsystem
+      const xData = data.map(item => item.subsystemName || item.subsystemCode || '未知')
+      const yData = data.map(item => item.cnt)
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          data: xData,
+          axisLine: {
+            lineStyle: { color: '#dcdfe6' }
+          },
+          axisLabel: {
+            color: '#606266',
+            fontSize: 11,
+            rotate: xData.length > 5 ? 30 : 0
+          }
+        },
+        yAxis: {
+          type: 'value',
+          axisLine: { show: false },
+          axisTick: { show: false },
+          splitLine: {
+            lineStyle: { color: '#ebeef5', type: 'dashed' }
+          },
+          axisLabel: {
+            color: '#909399'
+          }
+        },
+        series: [{
+          name: '告警数',
+          type: 'bar',
+          barWidth: '50%',
+          itemStyle: {
+            borderRadius: [4, 4, 0, 0],
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: '#409eff' },
+              { offset: 1, color: '#79bbff' }
+            ])
+          },
+          data: yData
+        }]
+      }
+
+      this.charts.subsystem && this.charts.subsystem.setOption(option)
+    },
+
+    /** 刷新数据 */
+    refresh() {
+      this.loadData()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alarm-stats-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+}
+
+.stats-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #ebeef5;
+
+  .stats-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+}
+
+.chart-wrapper {
+  position: relative;
+  min-height: 300px;
+}
+
+.chart-area {
+  width: 100%;
+  height: v-bind(height);
+}
+
+.no-data {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+  color: #c0c4cc;
+
+  i {
+    font-size: 48px;
+    display: block;
+    margin-bottom: 10px;
+  }
+}
+
+.stats-summary {
+  display: flex;
+  justify-content: space-around;
+  margin-top: 16px;
+  padding-top: 16px;
+  border-top: 1px solid #ebeef5;
+
+  .summary-item {
+    text-align: center;
+
+    .summary-label {
+      display: block;
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 4px;
+    }
+
+    .summary-value {
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    &.danger .summary-value { color: #f56c6c; }
+    &.warning .summary-value { color: #e6a23c; }
+    &.success .summary-value { color: #67c23a; }
+  }
+}
+</style>

+ 952 - 0
ems-ui-cloud/src/views/alarm/list/index.vue

@@ -0,0 +1,952 @@
+<template>
+  <div class="app-container alarm-container">
+    <!-- 实时告警横幅 -->
+    <div v-if="urgentAlarms.length > 0" class="urgent-banner">
+      <div class="urgent-icon">
+        <i class="el-icon-warning"></i>
+      </div>
+      <div class="urgent-content">
+        <el-carousel height="36px" direction="vertical" :autoplay="true" :interval="3000" indicator-position="none">
+          <el-carousel-item v-for="item in urgentAlarms" :key="item.alarmId">
+            <span class="urgent-text" @click="handleView(item)">
+              【紧急】{{ item.alarmMsg || item.targetName + ' - ' + item.attrName + '异常' }}
+              <span class="urgent-time">{{ item.alarmTime }}</span>
+            </span>
+          </el-carousel-item>
+        </el-carousel>
+      </div>
+      <el-button type="text" class="urgent-btn" @click="showUrgentList">查看全部({{ urgentAlarms.length }})</el-button>
+    </div>
+
+    <!-- 统计卡片 -->
+    <div class="stats-cards">
+      <div class="stat-card active" @click="filterByStatus(null)">
+        <div class="stat-icon">
+          <i class="el-icon-bell"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.totalCount || 0 }}</div>
+          <div class="stat-label">告警总数</div>
+        </div>
+        <div class="stat-trend" v-if="statsData.todayCount">
+          今日 +{{ statsData.todayCount }}
+        </div>
+      </div>
+      <div class="stat-card danger" @click="filterByStatus(0)">
+        <div class="stat-icon">
+          <i class="el-icon-warning-outline"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.activeCount || 0 }}</div>
+          <div class="stat-label">活动告警</div>
+        </div>
+        <div class="stat-badge" v-if="statsData.activeCount > 0"></div>
+      </div>
+      <div class="stat-card warning" @click="filterByLevel(3)">
+        <div class="stat-icon">
+          <i class="el-icon-s-opportunity"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.urgentCount || 0 }}</div>
+          <div class="stat-label">紧急告警</div>
+        </div>
+      </div>
+      <div class="stat-card success">
+        <div class="stat-icon">
+          <i class="el-icon-circle-check"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.handleRate || 0 }}%</div>
+          <div class="stat-label">处理率</div>
+        </div>
+        <div class="stat-progress">
+          <el-progress :percentage="statsData.handleRate || 0" :show-text="false" :stroke-width="4" />
+        </div>
+      </div>
+    </div>
+
+    <!-- 搜索区域 -->
+    <el-card class="search-card" shadow="hover">
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
+        <el-form-item label="目标名称" prop="targetName">
+          <el-input v-model="queryParams.targetName" placeholder="设备/设施名称" clearable style="width: 160px"
+                    @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="告警级别" prop="alarmLevel">
+          <el-select v-model="queryParams.alarmLevel" placeholder="全部级别" clearable style="width: 120px">
+            <el-option v-for="item in ALARM_LEVEL_OPTIONS" :key="item.value" :label="item.label" :value="item.value">
+              <span :style="{ color: item.color }">{{ item.label }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="告警状态" prop="alarmStatus">
+          <el-select v-model="queryParams.alarmStatus" placeholder="全部状态" clearable style="width: 120px">
+            <el-option v-for="item in ALARM_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="告警来源" prop="alarmSource">
+          <el-select v-model="queryParams.alarmSource" placeholder="全部来源" clearable style="width: 120px">
+            <el-option v-for="item in ALARM_SOURCE_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="告警时间">
+          <el-date-picker v-model="dateRange" type="datetimerange" range-separator="至"
+                          start-placeholder="开始时间" end-placeholder="结束时间" style="width: 340px"
+                          value-format="yyyy-MM-dd HH:mm:ss" :default-time="['00:00:00', '23:59:59']" />
+        </el-form-item>
+        <el-form-item label="子系统" prop="subsystemCode">
+          <el-select v-model="queryParams.subsystemCode" placeholder="全部子系统" clearable style="width: 140px">
+            <el-option v-for="item in subsystemOptions" :key="item.systemCode"
+                       :label="item.systemName" :value="item.systemCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 操作工具栏 -->
+    <el-card class="table-card" shadow="hover">
+      <div slot="header" class="card-header">
+        <div class="card-title-area">
+          <span class="card-title">
+            <i class="el-icon-message-solid"></i> 告警列表
+          </span>
+          <el-radio-group v-model="statusFilter" size="mini" @change="handleStatusFilter">
+            <el-radio-button :label="null">全部</el-radio-button>
+            <el-radio-button :label="0">
+              活动 <el-badge :value="statsData.activeCount" :max="99" class="status-badge" v-if="statsData.activeCount > 0" />
+            </el-radio-button>
+            <el-radio-button :label="1">已确认</el-radio-button>
+            <el-radio-button :label="2">处置中</el-radio-button>
+            <el-radio-button :label="'ended'">已结束</el-radio-button>
+          </el-radio-group>
+        </div>
+        <div class="card-actions">
+          <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleReport"
+                     v-hasPermi="['ems:alarm:add']">手动上报</el-button>
+          <el-button type="success" plain icon="el-icon-check" size="mini" :disabled="multiple"
+                     @click="handleBatchConfirm" v-hasPermi="['ems:alarm:handle']">批量确认</el-button>
+          <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
+                     @click="handleDelete" v-hasPermi="['ems:alarm:remove']">删除</el-button>
+          <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                     v-hasPermi="['ems:alarm:export']">导出</el-button>
+          <el-tooltip content="刷新数据" placement="top">
+            <el-button circle icon="el-icon-refresh" size="mini" @click="getList" />
+          </el-tooltip>
+          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </div>
+      </div>
+
+      <!-- 告警表格 -->
+      <el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange"
+                border stripe highlight-current-row :row-class-name="tableRowClassName"
+                @row-dblclick="handleView">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="告警级别" align="center" width="90">
+          <template slot-scope="scope">
+            <el-tag :type="getAlarmLevelInfo(scope.row.alarmLevel).type" size="small" effect="dark"
+                    :class="{ 'blink': scope.row.alarmStatus === 0 && scope.row.alarmLevel === 3 }">
+              {{ getAlarmLevelInfo(scope.row.alarmLevel).label }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警状态" align="center" width="90">
+          <template slot-scope="scope">
+            <el-tag :type="getAlarmStatusInfo(scope.row.alarmStatus).type" size="small" effect="light">
+              {{ getAlarmStatusInfo(scope.row.alarmStatus).label }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警目标" min-width="180" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <div class="target-info">
+              <i :class="getTargetTypeIcon(scope.row.targetType)" class="target-icon"></i>
+              <div class="target-detail">
+                <el-link type="primary" @click="handleView(scope.row)" class="target-name">
+                  {{ scope.row.targetName || scope.row.targetCode }}
+                </el-link>
+                <span class="target-location" v-if="scope.row.location">{{ scope.row.location }}</span>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警内容" min-width="200" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <div class="alarm-content">
+              <span class="alarm-msg">{{ scope.row.alarmMsg || formatAlarmContent(scope.row) }}</span>
+              <span class="alarm-value" v-if="scope.row.attrValue">
+                当前值: <code>{{ scope.row.attrValue }}</code>
+              </span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警来源" align="center" width="100">
+          <template slot-scope="scope">
+            <el-tooltip :content="getAlarmSourceInfo(scope.row.alarmSource).label">
+              <i :class="getAlarmSourceInfo(scope.row.alarmSource).icon"
+                 :style="{ color: getAlarmSourceInfo(scope.row.alarmSource).color, fontSize: '18px' }"></i>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警时间" align="center" width="160">
+          <template slot-scope="scope">
+            <div class="time-info">
+              <span class="time-main">{{ formatTime(scope.row.alarmTime) }}</span>
+              <span class="time-duration" v-if="scope.row.duration">
+                持续 {{ formatDuration(scope.row.duration) }}
+              </span>
+              <span class="repeat-count" v-if="scope.row.repeatCount > 1">
+                <el-badge :value="scope.row.repeatCount" type="warning" />
+              </span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="子系统" prop="subsystemName" width="100" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <template v-if="!isEndedAlarm(scope.row.alarmStatus)">
+              <el-button v-if="scope.row.alarmStatus === 0" size="mini" type="text" icon="el-icon-check"
+                         @click="handleConfirm(scope.row)" v-hasPermi="['ems:alarm:handle']">确认</el-button>
+              <el-button size="mini" type="text" icon="el-icon-s-tools"
+                         @click="handleProcess(scope.row)" v-hasPermi="['ems:alarm:handle']">处置</el-button>
+              <el-dropdown @command="(cmd) => handleCommand(cmd, scope.row)" trigger="click">
+                <el-button size="mini" type="text" icon="el-icon-more"></el-button>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="resolve" icon="el-icon-circle-check">标记解决</el-dropdown-item>
+                  <el-dropdown-item command="close" icon="el-icon-circle-close">关闭告警</el-dropdown-item>
+                  <el-dropdown-item command="view" icon="el-icon-view" divided>查看详情</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </template>
+            <template v-else>
+              <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">详情</el-button>
+              <el-button size="mini" type="text" icon="el-icon-delete" class="delete-btn"
+                         @click="handleDelete(scope.row)" v-hasPermi="['ems:alarm:remove']">删除</el-button>
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
+                  :limit.sync="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 告警详情抽屉 -->
+    <alarm-detail ref="alarmDetail" @refresh="getList" />
+
+    <!-- 告警处置对话框 -->
+    <alarm-handle ref="alarmHandle" @success="handleSuccess" />
+
+    <!-- 手动上报对话框 -->
+    <alarm-report ref="alarmReport" @success="handleSuccess" />
+
+    <!-- 紧急告警列表对话框 -->
+    <el-dialog title="紧急告警列表" :visible.sync="urgentDialogVisible" width="900px" append-to-body>
+      <el-table :data="urgentAlarms" border stripe max-height="400">
+        <el-table-column label="告警目标" prop="targetName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="告警内容" min-width="200" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{ scope.row.alarmMsg || formatAlarmContent(scope.row) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="告警时间" prop="alarmTime" width="160" />
+        <el-table-column label="操作" width="120" align="center">
+          <template slot-scope="scope">
+            <el-button type="text" size="small" @click="handleUrgentProcess(scope.row)">立即处置</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer">
+        <el-button @click="urgentDialogVisible = false">关 闭</el-button>
+        <el-button type="primary" @click="handleBatchConfirmUrgent">全部确认</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listAlarm, listActiveAlarm, delAlarm, confirmAlarm, batchConfirmAlarm, getAlarmStatsOverview, exportAlarm } from '@/api/alarm/alarm'
+import { listSubsystem } from '@/api/adapter/subsystem'
+import { ALARM_LEVEL_OPTIONS, ALARM_STATUS_OPTIONS, ALARM_SOURCE_OPTIONS, getAlarmLevelInfo, getAlarmStatusInfo, getAlarmSourceInfo, getTargetTypeIcon, isEndedAlarm } from '@/enums/alarmEnum'
+import AlarmDetail from './components/AlarmDetail.vue'
+import AlarmHandle from './components/AlarmHandle.vue'
+import AlarmReport from './components/AlarmReport.vue'
+import { parseTime } from '@/utils/ruoyi'
+
+export default {
+  name: 'Alarm',
+  components: { AlarmDetail, AlarmHandle, AlarmReport },
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      alarmList: [],
+      ids: [],
+      alarmIds: [],
+      single: true,
+      multiple: true,
+      dateRange: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        targetName: null,
+        alarmLevel: null,
+        alarmStatus: null,
+        alarmSource: null,
+        subsystemCode: null,
+        startRecTime: null,
+        endRecTime: null
+      },
+      statusFilter: null,
+      statsData: {
+        totalCount: 0,
+        activeCount: 0,
+        urgentCount: 0,
+        handleRate: 0,
+        todayCount: 0
+      },
+      urgentAlarms: [],
+      urgentDialogVisible: false,
+      subsystemOptions: [],
+      ALARM_LEVEL_OPTIONS,
+      ALARM_STATUS_OPTIONS,
+      ALARM_SOURCE_OPTIONS,
+      // 自动刷新定时器
+      refreshTimer: null
+    }
+  },
+  created() {
+    this.loadSubsystems()
+    this.getList()
+    this.loadStats()
+    this.loadUrgentAlarms()
+    // 启动自动刷新
+    this.startAutoRefresh()
+  },
+  beforeDestroy() {
+    // 清除定时器
+    this.stopAutoRefresh()
+  },
+  methods: {
+    getAlarmLevelInfo,
+    getAlarmStatusInfo,
+    getAlarmSourceInfo,
+    getTargetTypeIcon,
+    isEndedAlarm,
+
+    /** 加载子系统选项 */
+    async loadSubsystems() {
+      try {
+        const res = await listSubsystem({})
+        this.subsystemOptions = res.rows || res.data || []
+      } catch (e) {
+        console.error('加载子系统失败', e)
+      }
+    },
+
+    /** 加载统计数据 */
+    async loadStats() {
+      try {
+        const res = await getAlarmStatsOverview({})
+        const data = res.data || {}
+        this.statsData = {
+          totalCount: data.totalCount || 0,
+          activeCount: data.activeCount || 0,
+          urgentCount: data.urgentCount || 0,
+          handleRate: data.handleRate?.rate || 0,
+          todayCount: data.todayCount || 0
+        }
+      } catch (e) {
+        console.error('加载统计失败', e)
+      }
+    },
+
+    /** 加载紧急告警 */
+    async loadUrgentAlarms() {
+      try {
+        const res = await listActiveAlarm()
+        const allActive = res.data || []
+        this.urgentAlarms = allActive.filter(a => a.alarmLevel === 3 && a.alarmStatus === 0)
+      } catch (e) {
+        console.error('加载紧急告警失败', e)
+      }
+    },
+
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      // 处理时间范围
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startRecTime = this.dateRange[0]
+        this.queryParams.endRecTime = this.dateRange[1]
+      } else {
+        this.queryParams.startRecTime = null
+        this.queryParams.endRecTime = null
+      }
+
+      listAlarm(this.queryParams).then(res => {
+        this.alarmList = res.rows || []
+        this.total = res.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.dateRange = []
+      this.statusFilter = null
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+
+    /** 状态筛选 */
+    handleStatusFilter(val) {
+      if (val === 'ended') {
+        // 已结束包括:已解决、已关闭、已恢复
+        this.queryParams.alarmStatus = null
+        // 需要后端支持或特殊处理
+      } else {
+        this.queryParams.alarmStatus = val
+      }
+      this.handleQuery()
+    },
+
+    /** 按状态筛选 */
+    filterByStatus(status) {
+      this.statusFilter = status
+      this.queryParams.alarmStatus = status
+      this.handleQuery()
+    },
+
+    /** 按级别筛选 */
+    filterByLevel(level) {
+      this.queryParams.alarmLevel = level
+      this.handleQuery()
+    },
+
+    /** 多选框选中数据 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.alarmIds = selection.map(item => item.alarmId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+
+    /** 表格行样式 */
+    tableRowClassName({ row }) {
+      if (row.alarmStatus === 0) {
+        if (row.alarmLevel === 3) return 'urgent-row'
+        if (row.alarmLevel === 2) return 'important-row'
+        return 'active-row'
+      }
+      if (row.alarmStatus >= 3) return 'ended-row'
+      return ''
+    },
+
+    /** 格式化告警内容 */
+    formatAlarmContent(row) {
+      const attrName = row.attrName || row.attrKey || '属性'
+      return `${row.targetName || '设备'} ${attrName}异常`
+    },
+
+    /** 格式化时间 */
+    formatTime(time) {
+      return parseTime(time, '{y}-{m}-{d} {h}:{i}')
+    },
+
+    /** 格式化持续时长 */
+    formatDuration(seconds) {
+      if (!seconds) return ''
+      if (seconds < 60) return `${seconds}秒`
+      if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
+      if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时`
+      return `${Math.floor(seconds / 86400)}天`
+    },
+
+    /** 查看详情 */
+    handleView(row) {
+      this.$refs.alarmDetail.open(row.alarmId)
+    },
+
+    /** 确认告警 */
+    handleConfirm(row) {
+      this.$modal.confirm(`确认告警【${row.targetName}】吗?`).then(() => {
+        return confirmAlarm(row.alarmId, '已确认')
+      }).then(() => {
+        this.$modal.msgSuccess('确认成功')
+        this.handleSuccess()
+      }).catch(() => {})
+    },
+
+    /** 处置告警 */
+    handleProcess(row) {
+      this.$refs.alarmHandle.open(row, 'handle')
+    },
+
+    /** 下拉菜单命令 */
+    handleCommand(cmd, row) {
+      switch (cmd) {
+        case 'resolve':
+          this.$refs.alarmHandle.open(row, 'resolve')
+          break
+        case 'close':
+          this.$refs.alarmHandle.open(row, 'close')
+          break
+        case 'view':
+          this.handleView(row)
+          break
+      }
+    },
+
+    /** 手动上报 */
+    handleReport() {
+      this.$refs.alarmReport.open()
+    },
+
+    /** 批量确认 */
+    handleBatchConfirm() {
+      if (this.alarmIds.length === 0) {
+        this.$modal.msgWarning('请选择要确认的告警')
+        return
+      }
+      this.$modal.confirm(`确认选中的 ${this.alarmIds.length} 条告警吗?`).then(() => {
+        return batchConfirmAlarm(this.alarmIds)
+      }).then(res => {
+        this.$modal.msgSuccess(res.msg || '批量确认成功')
+        this.handleSuccess()
+      }).catch(() => {})
+    },
+
+    /** 删除 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids
+      this.$modal.confirm(`确认删除选中的告警记录吗?`).then(() => {
+        return delAlarm(ids)
+      }).then(() => {
+        this.$modal.msgSuccess('删除成功')
+        this.getList()
+      }).catch(() => {})
+    },
+
+    /** 导出 */
+    handleExport() {
+      this.download('/ems/alarm/export', {
+        ...this.queryParams
+      }, `alarm_${new Date().getTime()}.xlsx`)
+    },
+
+    /** 处理成功回调 */
+    handleSuccess() {
+      this.getList()
+      this.loadStats()
+      this.loadUrgentAlarms()
+    },
+
+    /** 显示紧急告警列表 */
+    showUrgentList() {
+      this.urgentDialogVisible = true
+    },
+
+    /** 处置紧急告警 */
+    handleUrgentProcess(row) {
+      this.urgentDialogVisible = false
+      this.$refs.alarmHandle.open(row, 'handle')
+    },
+
+    /** 批量确认紧急告警 */
+    handleBatchConfirmUrgent() {
+      const ids = this.urgentAlarms.map(a => a.alarmId)
+      batchConfirmAlarm(ids).then(res => {
+        this.$modal.msgSuccess('批量确认成功')
+        this.urgentDialogVisible = false
+        this.handleSuccess()
+      })
+    },
+
+    /** 启动自动刷新 */
+    startAutoRefresh() {
+      this.refreshTimer = setInterval(() => {
+        this.loadStats()
+        this.loadUrgentAlarms()
+      }, 30000) // 30秒刷新一次
+    },
+
+    /** 停止自动刷新 */
+    stopAutoRefresh() {
+      if (this.refreshTimer) {
+        clearInterval(this.refreshTimer)
+        this.refreshTimer = null
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alarm-container {
+  padding: 20px;
+  background: #f5f7fa;
+  min-height: calc(100vh - 84px);
+}
+
+// 紧急告警横幅
+.urgent-banner {
+  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
+  border-radius: 8px;
+  padding: 12px 20px;
+  margin-bottom: 16px;
+  display: flex;
+  align-items: center;
+  color: #fff;
+  box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
+  animation: pulse 2s infinite;
+
+  .urgent-icon {
+    font-size: 24px;
+    margin-right: 12px;
+    animation: shake 0.5s infinite;
+  }
+
+  .urgent-content {
+    flex: 1;
+    overflow: hidden;
+
+    ::v-deep .el-carousel__item {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  .urgent-text {
+    cursor: pointer;
+    font-size: 14px;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    .urgent-time {
+      margin-left: 16px;
+      opacity: 0.8;
+      font-size: 12px;
+    }
+  }
+
+  .urgent-btn {
+    color: #fff;
+    font-weight: 600;
+
+    &:hover {
+      color: #ffeb3b;
+    }
+  }
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.9; }
+}
+
+@keyframes shake {
+  0%, 100% { transform: rotate(0deg); }
+  25% { transform: rotate(-5deg); }
+  75% { transform: rotate(5deg); }
+}
+
+// 统计卡片
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.stat-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  cursor: pointer;
+  transition: all 0.3s;
+  position: relative;
+  overflow: hidden;
+
+  &:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.1);
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 4px;
+    height: 100%;
+  }
+
+  &.active::before { background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); }
+  &.danger::before { background: linear-gradient(180deg, #f56c6c 0%, #e6a23c 100%); }
+  &.warning::before { background: linear-gradient(180deg, #e6a23c 0%, #f56c6c 100%); }
+  &.success::before { background: linear-gradient(180deg, #67c23a 0%, #409eff 100%); }
+}
+
+.stat-icon {
+  width: 56px;
+  height: 56px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16px;
+
+  i {
+    font-size: 28px;
+    color: #fff;
+  }
+
+  .active & { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
+  .danger & { background: linear-gradient(135deg, #f56c6c 0%, #e6a23c 100%); }
+  .warning & { background: linear-gradient(135deg, #e6a23c 0%, #f56c6c 100%); }
+  .success & { background: linear-gradient(135deg, #67c23a 0%, #409eff 100%); }
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #303133;
+  line-height: 1.2;
+}
+
+.stat-label {
+  font-size: 14px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.stat-trend {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  font-size: 12px;
+  color: #67c23a;
+  background: rgba(103, 194, 58, 0.1);
+  padding: 2px 8px;
+  border-radius: 10px;
+}
+
+.stat-badge {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  width: 10px;
+  height: 10px;
+  background: #f56c6c;
+  border-radius: 50%;
+  animation: blink 1s infinite;
+}
+
+.stat-progress {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 0 20px 10px;
+
+  ::v-deep .el-progress-bar__outer {
+    background: #ebeef5;
+  }
+
+  ::v-deep .el-progress-bar__inner {
+    background: linear-gradient(90deg, #67c23a 0%, #409eff 100%);
+  }
+}
+
+@keyframes blink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.3; }
+}
+
+// 搜索卡片
+.search-card {
+  margin-bottom: 16px;
+  border-radius: 8px;
+
+  ::v-deep .el-card__body {
+    padding: 15px 20px 5px;
+  }
+}
+
+// 表格卡片
+.table-card {
+  border-radius: 8px;
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .card-title-area {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+  }
+
+  .card-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #f56c6c;
+    }
+  }
+
+  .status-badge {
+    margin-left: 4px;
+
+    ::v-deep .el-badge__content {
+      height: 16px;
+      line-height: 16px;
+      padding: 0 5px;
+    }
+  }
+
+  .card-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+// 目标信息
+.target-info {
+  display: flex;
+  align-items: center;
+
+  .target-icon {
+    font-size: 20px;
+    color: #409eff;
+    margin-right: 10px;
+  }
+
+  .target-detail {
+    display: flex;
+    flex-direction: column;
+  }
+
+  .target-name {
+    font-weight: 500;
+  }
+
+  .target-location {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 2px;
+  }
+}
+
+// 告警内容
+.alarm-content {
+  .alarm-msg {
+    display: block;
+    color: #303133;
+  }
+
+  .alarm-value {
+    display: block;
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+
+    code {
+      background: #f5f7fa;
+      padding: 1px 6px;
+      border-radius: 3px;
+      color: #e6a23c;
+      font-family: 'Monaco', 'Menlo', monospace;
+    }
+  }
+}
+
+// 时间信息
+.time-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .time-main {
+    color: #606266;
+  }
+
+  .time-duration {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 2px;
+  }
+
+  .repeat-count {
+    margin-top: 4px;
+  }
+}
+
+// 表格行样式
+::v-deep {
+  .urgent-row {
+    background: rgba(245, 108, 108, 0.08) !important;
+  }
+
+  .important-row {
+    background: rgba(230, 162, 60, 0.08) !important;
+  }
+
+  .active-row {
+    background: rgba(64, 158, 255, 0.05) !important;
+  }
+
+  .ended-row {
+    color: #c0c4cc;
+  }
+}
+
+// 闪烁标签
+.blink {
+  animation: tagBlink 1s infinite;
+}
+
+@keyframes tagBlink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+// 删除按钮
+.delete-btn {
+  color: #f56c6c !important;
+
+  &:hover {
+    color: #f78989 !important;
+  }
+}
+</style>

+ 201 - 0
ems-ui-cloud/src/views/alarm/rule/components/RuleDetail.vue

@@ -0,0 +1,201 @@
+<template>
+  <el-dialog title="告警规则详情" :visible.sync="visible" width="800px" append-to-body>
+    <div v-loading="loading" class="rule-detail">
+      <!-- 基本信息 -->
+      <el-descriptions title="基本信息" :column="2" border size="medium">
+        <el-descriptions-item label="规则代码">
+          <code class="code-text">{{ rule.ruleCode }}</code>
+        </el-descriptions-item>
+        <el-descriptions-item label="规则名称">{{ rule.ruleName }}</el-descriptions-item>
+        <el-descriptions-item label="告警级别">
+          <el-tag :type="getAlarmLevelInfo(rule.alarmLevel).type" size="small" effect="dark">
+            {{ getAlarmLevelInfo(rule.alarmLevel).label }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="告警代码">{{ rule.alarmCode || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="所属子系统">{{ rule.subsystemName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="启用状态">
+          <el-tag :type="rule.enabled === 1 ? 'success' : 'info'" size="small">
+            {{ rule.enabled === 1 ? '已启用' : '已禁用' }}
+          </el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 监测目标 -->
+      <div class="section-title">
+        <i class="el-icon-aim"></i> 监测目标
+      </div>
+      <el-descriptions :column="2" border size="medium">
+        <el-descriptions-item label="设备模型">
+          {{ rule.deviceModelName || '全部设备' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="适用区域">
+          {{ rule.areaName || '全局' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="监测属性">
+          <el-tag size="small" effect="plain">{{ rule.attrName || rule.attrKey }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="属性标识">
+          <code class="code-text">{{ rule.attrKey }}</code>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 检查规则 -->
+      <div class="section-title">
+        <i class="el-icon-set-up"></i> 检查规则
+      </div>
+      <el-descriptions :column="2" border size="medium">
+        <el-descriptions-item label="检查类型">
+          <el-tag size="small" effect="light">{{ getCheckTypeLabel(rule.checkType) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="操作符">{{ rule.operator || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="阈值配置" :span="2">
+          <span class="threshold-display">{{ formatThreshold(rule) }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="消息模板" :span="2">
+          <div class="template-text">{{ rule.alarmMsgTemplate || '未配置' }}</div>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 高级配置 -->
+      <div class="section-title">
+        <i class="el-icon-s-tools"></i> 高级配置
+      </div>
+      <el-descriptions :column="3" border size="medium">
+        <el-descriptions-item label="连续触发次数">{{ rule.triggerCount || 1 }} 次</el-descriptions-item>
+        <el-descriptions-item label="冷却时间">{{ rule.coolDown || 0 }} 秒</el-descriptions-item>
+        <el-descriptions-item label="规则优先级">{{ rule.ruleOrder || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="检查恢复">
+          <el-tag :type="rule.recoveryCheck === 1 ? 'success' : 'info'" size="mini">
+            {{ rule.recoveryCheck === 1 ? '是' : '否' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="用于实时监测">
+          <el-tag :type="rule.useForRealtime === 1 ? 'primary' : 'info'" size="mini">
+            {{ rule.useForRealtime === 1 ? '是' : '否' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="用于巡检">
+          <el-tag :type="rule.useForInspection === 1 ? 'success' : 'info'" size="mini">
+            {{ rule.useForInspection === 1 ? '是' : '否' }}
+          </el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 其他信息 -->
+      <div class="section-title">
+        <i class="el-icon-info"></i> 其他信息
+      </div>
+      <el-descriptions :column="2" border size="medium">
+        <el-descriptions-item label="创建人">{{ rule.createBy || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ rule.createTime }}</el-descriptions-item>
+        <el-descriptions-item label="更新人">{{ rule.updateBy || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间">{{ rule.updateTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="规则描述" :span="2">{{ rule.description || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </div>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="visible = false">关 闭</el-button>
+      <el-button type="primary" @click="handleEdit">编 辑</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { getAlarmRule } from '@/api/alarm/alarmRule'
+import { getAlarmLevelInfo, getCheckTypeLabel } from '@/enums/alarmEnum'
+
+export default {
+  name: 'RuleDetail',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      rule: {}
+    }
+  },
+  methods: {
+    getAlarmLevelInfo,
+    getCheckTypeLabel,
+
+    formatThreshold(rule) {
+      if (!rule.checkType) return '-'
+      switch (rule.checkType) {
+        case 1:
+          const min = rule.thresholdMin || '-∞'
+          const max = rule.thresholdMax || '+∞'
+          return `${min} ~ ${max}`
+        case 2:
+        case 4:
+          return `= ${rule.thresholdValue || ''}`
+        case 3:
+          return '非空检查'
+        default:
+          return rule.thresholdValue || '-'
+      }
+    },
+
+    async open(id) {
+      this.visible = true
+      this.loading = true
+      try {
+        const res = await getAlarmRule(id)
+        this.rule = res.data || {}
+      } catch (e) {
+        this.$modal.msgError('获取详情失败')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    handleEdit() {
+      this.visible = false
+      this.$parent.$refs.ruleForm.open(this.rule.id)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.rule-detail {
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+.section-title {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+  margin: 20px 0 10px 0;
+  padding-left: 10px;
+  border-left: 3px solid #409EFF;
+
+  i {
+    margin-right: 5px;
+    color: #409EFF;
+  }
+}
+
+.code-text {
+  background: #f5f7fa;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 13px;
+  color: #409EFF;
+}
+
+.threshold-display {
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 14px;
+  color: #E6A23C;
+  font-weight: 600;
+}
+
+.template-text {
+  font-size: 13px;
+  color: #606266;
+  word-break: break-all;
+}
+</style>

+ 501 - 0
ems-ui-cloud/src/views/alarm/rule/components/RuleForm.vue

@@ -0,0 +1,501 @@
+<template>
+  <el-dialog :title="title" :visible.sync="visible" width="900px" append-to-body
+             :close-on-click-modal="false" @close="handleClose">
+    <el-form ref="form" :model="form" :rules="rules" label-width="110px" v-loading="formLoading">
+      <!-- 基本信息 -->
+      <el-divider content-position="left"><span class="divider-title">基本信息</span></el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="规则名称" prop="ruleName">
+            <el-input v-model="form.ruleName" placeholder="请输入规则名称" maxlength="128" show-word-limit />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="告警级别" prop="alarmLevel">
+            <el-select v-model="form.alarmLevel" placeholder="请选择告警级别" style="width: 100%">
+              <el-option v-for="item in ALARM_LEVEL_OPTIONS" :key="item.value"
+                         :label="item.label" :value="item.value">
+                <span :style="{ color: item.color }">
+                  <i class="el-icon-warning-outline"></i> {{ item.label }}
+                </span>
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="告警代码" prop="alarmCode">
+            <el-input v-model="form.alarmCode" placeholder="告警分类代码,如DEVICE_OFFLINE" maxlength="64" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="子系统" prop="subsystemCode">
+            <el-select v-model="form.subsystemCode" placeholder="请选择所属子系统" clearable style="width: 100%">
+              <el-option v-for="item in subsystemOptions" :key="item.systemCode"
+                         :label="item.systemName" :value="item.systemCode" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 目标配置 -->
+      <el-divider content-position="left"><span class="divider-title">监测目标</span></el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="设备模型" prop="deviceModel">
+            <el-select v-model="form.deviceModel" placeholder="不限定则监测全部设备" clearable filterable
+                       style="width: 100%" @change="handleModelChange">
+              <el-option v-for="item in modelOptions" :key="item.modelCode"
+                         :label="item.modelName" :value="item.modelCode" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="监测属性" prop="attrKey">
+            <el-select v-model="form.attrKey" placeholder="请选择监测属性" filterable allow-create
+                       style="width: 100%" @change="handleAttrChange">
+              <el-option label="设备状态" value="deviceStatus" />
+              <el-option-group v-if="attrOptions.length > 0" label="模型属性">
+                <el-option v-for="item in attrOptions" :key="item.attrKey"
+                           :label="item.attrName" :value="item.attrKey" />
+              </el-option-group>
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="属性名称" prop="attrName">
+            <el-input v-model="form.attrName" placeholder="属性中文名称" maxlength="256" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="适用区域" prop="areaCode">
+            <el-select v-model="form.areaCode" placeholder="不限定则全局适用" clearable filterable style="width: 100%">
+              <el-option v-for="item in areaOptions" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 检查规则 -->
+      <el-divider content-position="left"><span class="divider-title">检查规则</span></el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检查类型" prop="checkType">
+            <el-select v-model="form.checkType" placeholder="请选择检查类型" style="width: 100%"
+                       @change="handleCheckTypeChange">
+              <el-option v-for="item in CHECK_TYPE_OPTIONS" :key="item.value"
+                         :label="item.label" :value="item.value">
+                <i :class="item.icon" style="margin-right: 8px"></i>{{ item.label }}
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="form.checkType === 1">
+          <el-form-item label="操作符">
+            <el-select v-model="form.operator" placeholder="请选择操作符" style="width: 100%">
+              <el-option v-for="item in rangeOperatorOptions" :key="item.value"
+                         :label="item.label" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 阈值配置 - 根据检查类型动态显示 -->
+      <el-row :gutter="20">
+        <!-- 范围检查 -->
+        <template v-if="form.checkType === 1">
+          <el-col :span="12">
+            <el-form-item label="最小阈值" prop="thresholdMin">
+              <el-input v-model="form.thresholdMin" placeholder="最小值(可选)">
+                <template slot="append" v-if="form.attrUnit">{{ form.attrUnit }}</template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="最大阈值" prop="thresholdMax">
+              <el-input v-model="form.thresholdMax" placeholder="最大值(可选)">
+                <template slot="append" v-if="form.attrUnit">{{ form.attrUnit }}</template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+        </template>
+        <!-- 等值检查 -->
+        <template v-else-if="form.checkType === 2">
+          <el-col :span="12">
+            <el-form-item label="操作符">
+              <el-select v-model="form.operator" style="width: 100%">
+                <el-option label="等于 (=)" value="EQ" />
+                <el-option label="不等于 (!=)" value="NE" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="期望值" prop="thresholdValue">
+              <el-input v-model="form.thresholdValue" placeholder="期望的属性值" />
+            </el-form-item>
+          </el-col>
+        </template>
+        <!-- 在线状态检查 -->
+        <template v-else-if="form.checkType === 4">
+          <el-col :span="12">
+            <el-form-item label="在线值" prop="thresholdValue">
+              <el-select v-model="form.thresholdValue" placeholder="请选择在线值" style="width: 100%">
+                <el-option label="在线 (1)" value="1" />
+                <el-option label="离线 (0)" value="0" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="">
+              <div class="check-tip">
+                <i class="el-icon-info"></i>
+                当设备状态不等于在线值时触发告警
+              </div>
+            </el-form-item>
+          </el-col>
+        </template>
+        <!-- 非空检查 -->
+        <template v-else-if="form.checkType === 3">
+          <el-col :span="24">
+            <el-form-item label="">
+              <div class="check-tip">
+                <i class="el-icon-info"></i>
+                当属性值为空或不存在时触发告警
+              </div>
+            </el-form-item>
+          </el-col>
+        </template>
+      </el-row>
+
+      <!-- 告警消息模板 -->
+      <el-form-item label="消息模板" prop="alarmMsgTemplate">
+        <el-input v-model="form.alarmMsgTemplate" type="textarea" :rows="2"
+                  placeholder="告警消息模板,支持变量:${targetName}、${attrValue}、${threshold}、${location}等"
+                  maxlength="512" show-word-limit />
+      </el-form-item>
+
+      <!-- 高级配置 -->
+      <el-divider content-position="left"><span class="divider-title">高级配置</span></el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="8">
+          <el-form-item label="连续触发次数">
+            <el-input-number v-model="form.triggerCount" :min="1" :max="100" controls-position="right" style="width: 100%" />
+            <div class="form-tip">连续满足N次才触发告警</div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="冷却时间(秒)">
+            <el-input-number v-model="form.coolDown" :min="0" :max="86400" :step="60" controls-position="right" style="width: 100%" />
+            <div class="form-tip">同规则同目标的告警间隔</div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="规则优先级">
+            <el-input-number v-model="form.ruleOrder" :min="0" :max="9999" controls-position="right" style="width: 100%" />
+            <div class="form-tip">数值越小优先级越高</div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="8">
+          <el-form-item label="检查恢复">
+            <el-switch v-model="form.recoveryCheck" :active-value="1" :inactive-value="0"
+                       active-text="检查" inactive-text="不检查" />
+            <div class="form-tip">是否检测告警自动恢复</div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="用于实时监测">
+            <el-switch v-model="form.useForRealtime" :active-value="1" :inactive-value="0"
+                       active-text="是" inactive-text="否" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="用于巡检">
+            <el-switch v-model="form.useForInspection" :active-value="1" :inactive-value="0"
+                       active-text="是" inactive-text="否" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-form-item label="规则描述">
+        <el-input v-model="form.description" type="textarea" :rows="2" placeholder="规则说明" maxlength="512" />
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button type="primary" @click="submitForm" :loading="submitting">确 定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { getAlarmRule, addAlarmRule, updateAlarmRule } from '@/api/alarm/alarmRule'
+import { listAttr } from '@/api/basecfg/objAttribute'
+import { listSubsystem } from '@/api/adapter/subsystem'
+import { areaTreeSelect } from '@/api/basecfg/area'
+import { ALARM_LEVEL_OPTIONS, CHECK_TYPE_OPTIONS, OPERATOR_OPTIONS } from '@/enums/alarmEnum'
+
+export default {
+  name: 'RuleForm',
+  props: {
+    modelOptions: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      visible: false,
+      formLoading: false,
+      submitting: false,
+      title: '',
+      form: this.initForm(),
+      rules: {
+        ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
+        alarmLevel: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
+        attrKey: [{ required: true, message: '请选择监测属性', trigger: 'change' }],
+        checkType: [{ required: true, message: '请选择检查类型', trigger: 'change' }]
+      },
+      ALARM_LEVEL_OPTIONS,
+      CHECK_TYPE_OPTIONS,
+      OPERATOR_OPTIONS,
+      subsystemOptions: [],
+      areaOptions: [],
+      attrOptions: [],
+      attrCache: {},
+      rangeOperatorOptions: [
+        { value: 'RANGE', label: '超出范围告警' },
+        { value: 'GT', label: '大于最大值告警' },
+        { value: 'GE', label: '大于等于最大值告警' },
+        { value: 'LT', label: '小于最小值告警' },
+        { value: 'LE', label: '小于等于最小值告警' }
+      ]
+    }
+  },
+  created() {
+    this.loadSubsystems()
+    this.loadAreas()
+  },
+  methods: {
+    initForm() {
+      return {
+        id: null,
+        ruleCode: null,
+        ruleName: '',
+        areaCode: null,
+        targetType: 2,
+        targetScope: null,
+        deviceModel: null,
+        attrKey: 'deviceStatus',
+        attrName: '设备状态',
+        attrUnit: '',
+        checkType: 4,
+        operator: 'EQ',
+        thresholdValue: '1',
+        thresholdMin: null,
+        thresholdMax: null,
+        alarmLevel: 1,
+        alarmCode: '',
+        alarmMsgTemplate: '',
+        recoveryCheck: 1,
+        recoveryThreshold: null,
+        triggerCount: 1,
+        triggerInterval: 0,
+        coolDown: 300,
+        enabled: 1,
+        useForInspection: 1,
+        useForRealtime: 1,
+        subsystemCode: null,
+        ruleOrder: 0,
+        description: ''
+      }
+    },
+
+    async loadSubsystems() {
+      try {
+        const res = await listSubsystem({})
+        this.subsystemOptions = res.rows || res.data || []
+      } catch (e) {
+        console.error('加载子系统失败', e)
+      }
+    },
+
+    async loadAreas() {
+      try {
+        const res = await areaTreeSelect('0', 2)
+        this.areaOptions = this.flattenTree(res.data || [])
+      } catch (e) {
+        console.error('加载区域失败', e)
+      }
+    },
+
+    flattenTree(tree, prefix = '') {
+      const result = []
+      tree.forEach(node => {
+        result.push({ id: node.id, label: prefix + node.label })
+        if (node.children && node.children.length > 0) {
+          result.push(...this.flattenTree(node.children, prefix + node.label + ' / '))
+        }
+      })
+      return result
+    },
+
+    async loadAttrsByModel(modelCode) {
+      if (!modelCode || this.attrCache[modelCode]) {
+        this.attrOptions = this.attrCache[modelCode] || []
+        return
+      }
+      try {
+        const res = await listAttr({ modelCode })
+        this.attrCache[modelCode] = res.rows || []
+        this.attrOptions = this.attrCache[modelCode]
+      } catch (e) {
+        console.error('加载模型属性失败', e)
+      }
+    },
+
+    async open(id) {
+      this.form = this.initForm()
+      this.title = id ? '编辑告警规则' : '新增告警规则'
+      this.visible = true
+
+      if (id) {
+        this.formLoading = true
+        try {
+          const res = await getAlarmRule(id)
+          this.form = { ...this.form, ...res.data }
+          if (this.form.deviceModel) {
+            await this.loadAttrsByModel(this.form.deviceModel)
+          }
+        } catch (e) {
+          this.$modal.msgError('获取规则详情失败')
+          this.visible = false
+        } finally {
+          this.formLoading = false
+        }
+      }
+    },
+
+    handleClose() {
+      this.visible = false
+      this.$refs.form && this.$refs.form.resetFields()
+      this.form = this.initForm()
+    },
+
+    handleModelChange(val) {
+      this.form.attrKey = 'deviceStatus'
+      this.form.attrName = '设备状态'
+      this.form.attrUnit = ''
+      if (val) {
+        this.loadAttrsByModel(val)
+      } else {
+        this.attrOptions = []
+      }
+    },
+
+    handleAttrChange(val) {
+      if (val === 'deviceStatus') {
+        this.form.attrName = '设备状态'
+        this.form.attrUnit = ''
+        this.form.checkType = 4
+        this.form.thresholdValue = '1'
+      } else {
+        const attr = this.attrOptions.find(a => a.attrKey === val)
+        if (attr) {
+          this.form.attrName = attr.attrName
+          this.form.attrUnit = attr.attrUnit || ''
+        }
+        if (this.form.checkType === 4) {
+          this.form.checkType = 1
+        }
+      }
+    },
+
+    handleCheckTypeChange(val) {
+      this.form.thresholdMin = null
+      this.form.thresholdMax = null
+      this.form.thresholdValue = null
+
+      switch (val) {
+        case 1: // 范围
+          this.form.operator = 'RANGE'
+          break
+        case 2: // 等值
+          this.form.operator = 'EQ'
+          break
+        case 4: // 在线状态
+          this.form.operator = 'EQ'
+          this.form.thresholdValue = '1'
+          break
+        default:
+          this.form.operator = null
+      }
+    },
+
+    submitForm() {
+      this.$refs.form.validate(async (valid) => {
+        if (!valid) return
+
+        // 校验阈值
+        if (this.form.checkType === 1) {
+          if (!this.form.thresholdMin && !this.form.thresholdMax) {
+            this.$modal.msgWarning('范围检查至少需要设置一个阈值')
+            return
+          }
+        }
+
+        this.submitting = true
+        try {
+          if (this.form.id) {
+            await updateAlarmRule(this.form)
+          } else {
+            await addAlarmRule(this.form)
+          }
+          this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
+          this.handleClose()
+          this.$emit('success')
+        } catch (err) {
+          this.$modal.msgError(err.message || '操作失败')
+        } finally {
+          this.submitting = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.divider-title {
+  font-weight: 600;
+  color: #409EFF;
+  font-size: 14px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.check-tip {
+  padding: 10px 15px;
+  background: #f0f9eb;
+  border-radius: 4px;
+  color: #67C23A;
+  font-size: 13px;
+
+  i {
+    margin-right: 5px;
+  }
+}
+</style>

+ 607 - 0
ems-ui-cloud/src/views/alarm/rule/index.vue

@@ -0,0 +1,607 @@
+<template>
+  <div class="app-container alarm-rule-container">
+    <!-- 页面头部统计卡片 -->
+    <div class="stats-cards">
+      <div class="stat-card">
+        <div class="stat-icon total">
+          <i class="el-icon-s-grid"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.total || 0 }}</div>
+          <div class="stat-label">规则总数</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon enabled">
+          <i class="el-icon-circle-check"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.enabled || 0 }}</div>
+          <div class="stat-label">已启用</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon realtime">
+          <i class="el-icon-data-line"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.realtime || 0 }}</div>
+          <div class="stat-label">实时监测</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon inspection">
+          <i class="el-icon-discover"></i>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ statsData.inspection || 0 }}</div>
+          <div class="stat-label">用于巡检</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 搜索区域 -->
+    <el-card class="search-card" shadow="hover">
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
+        <el-form-item label="规则名称" prop="ruleName">
+          <el-input v-model="queryParams.ruleName" placeholder="请输入规则名称" clearable style="width: 180px"
+                    @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="设备模型" prop="deviceModel">
+          <el-select v-model="queryParams.deviceModel" placeholder="请选择设备模型" clearable filterable style="width: 200px">
+            <el-option v-for="item in modelOptions" :key="item.modelCode" :label="item.modelName" :value="item.modelCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="告警级别" prop="alarmLevel">
+          <el-select v-model="queryParams.alarmLevel" placeholder="请选择级别" clearable style="width: 120px">
+            <el-option v-for="item in ALARM_LEVEL_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="检查类型" prop="checkType">
+          <el-select v-model="queryParams.checkType" placeholder="请选择类型" clearable style="width: 140px">
+            <el-option v-for="item in CHECK_TYPE_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="启用状态" prop="enabled">
+          <el-select v-model="queryParams.enabled" placeholder="全部" clearable style="width: 100px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 操作工具栏 -->
+    <el-card class="table-card" shadow="hover">
+      <div slot="header" class="card-header">
+        <span class="card-title">
+          <i class="el-icon-setting"></i> 告警规则配置
+        </span>
+        <div class="card-actions">
+          <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                     v-hasPermi="['ems:alarm-rule:add']">新增规则</el-button>
+          <el-button type="success" plain icon="el-icon-check" size="mini" :disabled="multiple"
+                     @click="handleBatchEnable(1)" v-hasPermi="['ems:alarm-rule:edit']">批量启用</el-button>
+          <el-button type="warning" plain icon="el-icon-close" size="mini" :disabled="multiple"
+                     @click="handleBatchEnable(0)" v-hasPermi="['ems:alarm-rule:edit']">批量禁用</el-button>
+          <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
+                     @click="handleDelete" v-hasPermi="['ems:alarm-rule:remove']">删除</el-button>
+          <el-button type="info" plain icon="el-icon-download" size="mini" @click="handleExport"
+                     v-hasPermi="['ems:alarm-rule:export']">导出</el-button>
+          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </div>
+      </div>
+
+      <!-- 规则表格 -->
+      <el-table v-loading="loading" :data="ruleList" @selection-change="handleSelectionChange"
+                border stripe highlight-current-row :row-class-name="tableRowClassName">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="规则代码" prop="ruleCode" width="160" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.ruleCode }}</el-link>
+          </template>
+        </el-table-column>
+        <el-table-column label="规则名称" prop="ruleName" min-width="160" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <span class="rule-name">
+              <i :class="getCheckTypeIcon(scope.row.checkType)" class="rule-icon"></i>
+              {{ scope.row.ruleName }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="设备模型" prop="deviceModelName" width="150" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <span v-if="scope.row.deviceModelName">{{ scope.row.deviceModelName }}</span>
+            <el-tag v-else size="mini" type="info">全部设备</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="监测属性" width="140">
+          <template slot-scope="scope">
+            <el-tooltip :content="scope.row.attrKey" placement="top">
+              <span>{{ scope.row.attrName || scope.row.attrKey }}</span>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column label="检查类型" align="center" width="100">
+          <template slot-scope="scope">
+            <el-tag size="small" effect="plain">{{ getCheckTypeLabel(scope.row.checkType) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="阈值配置" width="150">
+          <template slot-scope="scope">
+            <span class="threshold-value">{{ formatThreshold(scope.row) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="告警级别" align="center" width="90">
+          <template slot-scope="scope">
+            <el-tag :type="getAlarmLevelInfo(scope.row.alarmLevel).type" size="small" effect="dark">
+              {{ getAlarmLevelInfo(scope.row.alarmLevel).label }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="应用场景" align="center" width="130">
+          <template slot-scope="scope">
+            <div class="scene-tags">
+              <el-tag v-if="scope.row.useForRealtime === 1" size="mini" type="primary" effect="light">实时</el-tag>
+              <el-tag v-if="scope.row.useForInspection === 1" size="mini" type="success" effect="light">巡检</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="80">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row.enabled" :active-value="1" :inactive-value="0"
+                       @change="handleStatusChange(scope.row)" v-hasPermi="['ems:alarm-rule:edit']" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="180" fixed="right" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                       v-hasPermi="['ems:alarm-rule:edit']">编辑</el-button>
+            <el-button size="mini" type="text" icon="el-icon-document-copy" @click="handleCopy(scope.row)"
+                       v-hasPermi="['ems:alarm-rule:add']">复制</el-button>
+            <el-button size="mini" type="text" icon="el-icon-delete" class="delete-btn"
+                       @click="handleDelete(scope.row)" v-hasPermi="['ems:alarm-rule:remove']">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
+                  :limit.sync="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <rule-form ref="ruleForm" :model-options="modelOptions" @success="getList" />
+
+    <!-- 查看详情对话框 -->
+    <rule-detail ref="ruleDetail" />
+
+    <!-- 复制规则对话框 -->
+    <el-dialog title="复制规则" :visible.sync="copyDialogVisible" width="400px" append-to-body>
+      <el-form ref="copyForm" :model="copyForm" :rules="copyRules" label-width="100px">
+        <el-form-item label="源规则">
+          <el-input :value="copyForm.sourceRuleName" disabled />
+        </el-form-item>
+        <el-form-item label="新规则名称" prop="newRuleName">
+          <el-input v-model="copyForm.newRuleName" placeholder="请输入新规则名称" maxlength="128" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="copyDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitCopy" :loading="copyLoading">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listAlarmRule, delAlarmRule, updateRuleEnabled, batchUpdateRuleEnabled, copyAlarmRule, listAllAlarmRule } from '@/api/alarm/alarmRule'
+import { listAllModel } from '@/api/basecfg/objModel'
+import { ALARM_LEVEL_OPTIONS, CHECK_TYPE_OPTIONS, getAlarmLevelInfo, getCheckTypeLabel } from '@/enums/alarmEnum'
+import RuleForm from './components/RuleForm.vue'
+import RuleDetail from './components/RuleDetail.vue'
+
+export default {
+  name: 'AlarmRule',
+  components: { RuleForm: RuleForm, RuleDetail },
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      ruleList: [],
+      ids: [],
+      ruleCodes: [],
+      single: true,
+      multiple: true,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        ruleName: null,
+        deviceModel: null,
+        alarmLevel: null,
+        checkType: null,
+        enabled: null
+      },
+      modelOptions: [],
+      statsData: {
+        total: 0,
+        enabled: 0,
+        realtime: 0,
+        inspection: 0
+      },
+      ALARM_LEVEL_OPTIONS,
+      CHECK_TYPE_OPTIONS,
+      // 复制对话框
+      copyDialogVisible: false,
+      copyLoading: false,
+      copyForm: {
+        sourceRuleCode: '',
+        sourceRuleName: '',
+        newRuleName: ''
+      },
+      copyRules: {
+        newRuleName: [
+          { required: true, message: '请输入新规则名称', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.loadModelOptions()
+    this.getList()
+    this.loadStats()
+  },
+  methods: {
+    getAlarmLevelInfo,
+    getCheckTypeLabel,
+
+    /** 获取检查类型图标 */
+    getCheckTypeIcon(type) {
+      const iconMap = {
+        1: 'el-icon-data-analysis',
+        2: 'el-icon-finished',
+        3: 'el-icon-document',
+        4: 'el-icon-connection',
+        5: 'el-icon-sort'
+      }
+      return iconMap[type] || 'el-icon-setting'
+    },
+
+    /** 格式化阈值显示 */
+    formatThreshold(row) {
+      if (!row.checkType) return '-'
+      switch (row.checkType) {
+        case 1: // 范围
+          const min = row.thresholdMin || '-∞'
+          const max = row.thresholdMax || '+∞'
+          return `${min} ~ ${max}`
+        case 2: // 等值
+        case 4: // 在线状态
+          return `= ${row.thresholdValue || ''}`
+        case 3: // 非空
+          return '非空检查'
+        default:
+          return row.thresholdValue || '-'
+      }
+    },
+
+    /** 表格行样式 */
+    tableRowClassName({ row }) {
+      if (row.enabled === 0) {
+        return 'disabled-row'
+      }
+      return ''
+    },
+
+    /** 加载设备模型选项 */
+    async loadModelOptions() {
+      try {
+        const res = await listAllModel(2)
+        this.modelOptions = res.data || []
+      } catch (e) {
+        console.error('加载设备模型失败', e)
+      }
+    },
+
+    /** 加载统计数据 */
+    async loadStats() {
+      try {
+        const res = await listAllAlarmRule({})
+        const list = res.data || []
+        this.statsData = {
+          total: list.length,
+          enabled: list.filter(r => r.enabled === 1).length,
+          realtime: list.filter(r => r.useForRealtime === 1).length,
+          inspection: list.filter(r => r.useForInspection === 1).length
+        }
+      } catch (e) {
+        console.error('加载统计数据失败', e)
+      }
+    },
+
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      listAlarmRule(this.queryParams).then(res => {
+        this.ruleList = res.rows || []
+        this.total = res.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+
+    /** 多选框选中数据 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.ruleCodes = selection.map(item => item.ruleCode)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+
+    /** 新增 */
+    handleAdd() {
+      this.$refs.ruleForm.open()
+    },
+
+    /** 查看详情 */
+    handleView(row) {
+      this.$refs.ruleDetail.open(row.id)
+    },
+
+    /** 修改 */
+    handleUpdate(row) {
+      this.$refs.ruleForm.open(row.id)
+    },
+
+    /** 复制 */
+    handleCopy(row) {
+      this.copyForm = {
+        sourceRuleCode: row.ruleCode,
+        sourceRuleName: row.ruleName,
+        newRuleName: row.ruleName + '_副本'
+      }
+      this.copyDialogVisible = true
+    },
+
+    /** 提交复制 */
+    submitCopy() {
+      this.$refs.copyForm.validate(valid => {
+        if (!valid) return
+        this.copyLoading = true
+        copyAlarmRule(this.copyForm.sourceRuleCode, this.copyForm.newRuleName).then(res => {
+          this.$modal.msgSuccess('复制成功')
+          this.copyDialogVisible = false
+          this.getList()
+          this.loadStats()
+        }).catch(err => {
+          this.$modal.msgError(err.message || '复制失败')
+        }).finally(() => {
+          this.copyLoading = false
+        })
+      })
+    },
+
+    /** 状态开关变更 */
+    handleStatusChange(row) {
+      const text = row.enabled === 1 ? '启用' : '禁用'
+      updateRuleEnabled(row.ruleCode, row.enabled).then(() => {
+        this.$modal.msgSuccess(`${text}成功`)
+        this.loadStats()
+      }).catch(() => {
+        row.enabled = row.enabled === 1 ? 0 : 1
+      })
+    },
+
+    /** 批量启用/禁用 */
+    handleBatchEnable(enabled) {
+      const text = enabled === 1 ? '启用' : '禁用'
+      this.$modal.confirm(`确认${text}选中的 ${this.ruleCodes.length} 条规则吗?`).then(() => {
+        return batchUpdateRuleEnabled(this.ruleCodes, enabled)
+      }).then(() => {
+        this.$modal.msgSuccess(`批量${text}成功`)
+        this.getList()
+        this.loadStats()
+      }).catch(() => {})
+    },
+
+    /** 删除 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids
+      const names = row.ruleName || this.ruleList.filter(r => ids.includes(r.id)).map(r => r.ruleName).join('、')
+
+      this.$modal.confirm(`确认删除规则【${names}】吗?`).then(() => {
+        return delAlarmRule(ids)
+      }).then(() => {
+        this.$modal.msgSuccess('删除成功')
+        this.getList()
+        this.loadStats()
+      }).catch(() => {})
+    },
+
+    /** 导出 */
+    handleExport() {
+      this.download('/alarm/rule/export', {
+        ...this.queryParams
+      }, `alarm_rule_${new Date().getTime()}.xlsx`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.alarm-rule-container {
+  padding: 20px;
+  background: #f5f7fa;
+  min-height: calc(100vh - 84px);
+}
+
+// 统计卡片
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.stat-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
+  }
+}
+
+.stat-icon {
+  width: 56px;
+  height: 56px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16px;
+
+  i {
+    font-size: 28px;
+    color: #fff;
+  }
+
+  &.total {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  }
+
+  &.enabled {
+    background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+  }
+
+  &.realtime {
+    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  }
+
+  &.inspection {
+    background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
+  }
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-value {
+  font-size: 28px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.2;
+}
+
+.stat-label {
+  font-size: 14px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+// 搜索卡片
+.search-card {
+  margin-bottom: 16px;
+  border-radius: 8px;
+
+  ::v-deep .el-card__body {
+    padding: 15px 20px 5px;
+  }
+}
+
+// 表格卡片
+.table-card {
+  border-radius: 8px;
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .card-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #409EFF;
+    }
+  }
+
+  .card-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+// 规则名称样式
+.rule-name {
+  display: flex;
+  align-items: center;
+
+  .rule-icon {
+    margin-right: 8px;
+    color: #409EFF;
+    font-size: 16px;
+  }
+}
+
+// 阈值样式
+.threshold-value {
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 13px;
+  color: #606266;
+  background: #f5f7fa;
+  padding: 2px 8px;
+  border-radius: 4px;
+}
+
+// 场景标签
+.scene-tags {
+  display: flex;
+  gap: 4px;
+  justify-content: center;
+}
+
+// 禁用行样式
+::v-deep .disabled-row {
+  background-color: #fafafa;
+  color: #c0c4cc;
+}
+
+// 删除按钮
+.delete-btn {
+  color: #F56C6C !important;
+
+  &:hover {
+    color: #f78989 !important;
+  }
+}
+</style>

+ 1 - 1
ems-ui-cloud/src/views/largeScreen/device/right.vue

@@ -25,7 +25,7 @@ import CusModule from '../components/CusModule.vue';
 import BaseChart from '@/components/BaseChart/index.vue'
 import {mapState} from 'vuex';
 import {fetchCntDateAlarmType, listAlarmInfo} from "@/api/alarm/alarm-info";
-import {ALARM_STATE} from "@/enums/alarm";
+import {ALARM_STATE} from "@/enums/alarmEnum";
 import {ApiCode} from "@/api/apiEmums";
 
 const colorDic = {

+ 1 - 1
ems-ui-cloud/src/views/largeScreen/home/right.vue

@@ -105,7 +105,7 @@ import {DateTool} from "@/utils/DateTool";
 import {array2Map, numToStr} from "@/utils";
 import {listDeviceStatus, listDeviceType} from "@/api/device/device";
 import {listAlarmInfo} from "@/api/alarm/alarm-info";
-import {ALARM_STATE} from "@/enums/alarm";
+import {ALARM_STATE} from "@/enums/alarmEnum";
 import {ApiCode} from "@/api/apiEmums";
 import {listSumCaMeterD} from "@/api/ca/caMeterD";
 import {calcElecProdForecastDateRange} from "@/api/prediction/predictionProd";

+ 1 - 1
ems-ui-cloud/src/views/largeScreen/source/pv-road-right.vue

@@ -29,7 +29,7 @@ import {mapState} from 'vuex';
 import {DateTool} from "@/utils/DateTool";
 import {predictionProdDateRange} from "@/api/screen";
 import {listAlarmInfo} from "@/api/alarm/alarm-info";
-import {ALARM_STATE} from "@/enums/alarm";
+import {ALARM_STATE} from "@/enums/alarmEnum";
 import {ApiCode} from "@/api/apiEmums";
 
 export default {

+ 1 - 1
ems-ui-cloud/src/views/largeScreen/source/right.vue

@@ -42,7 +42,7 @@ import {mapState} from 'vuex';
 import {DateTool} from "@/utils/DateTool";
 import {calcElecProdForecastDateRange} from "@/api/prediction/predictionProd";
 import {listAlarmInfo} from "@/api/alarm/alarm-info";
-import {ALARM_STATE} from "@/enums/alarm";
+import {ALARM_STATE} from "@/enums/alarmEnum";
 import {ApiCode} from "@/api/apiEmums";
 // 新增:引入碳计量API
 import {listSumCaMeterD} from "@/api/ca/caMeterD";