Selaa lähdekoodia

refactor(manholeCover): 重构井盖报警列表页面数据处理逻辑

- 移除模拟数据生成,集成真实API接口
- 添加列表和详情加载状态指示器
- 实现实时报警数据处理和规范化逻辑
- 集成设备详情和报警列表API调用
- 优化搜索功能,支持设备名称搜索
- 移除已处置统计项和操作按钮
- 添加空状态提示组件
- 更新地图坐标显示逻辑,移除模拟定位信息
- 移除实时监测图表和处置记录等冗余组件
- 重构过滤和统计计算逻辑,简化状态管理
nahida 1 viikko sitten
vanhempi
commit
10574380ee
1 muutettua tiedostoa jossa 214 lisäystä ja 205 poistoa
  1. 214 205
      src/views/subSystem/manholeCover/alarm/alarmList.vue

+ 214 - 205
src/views/subSystem/manholeCover/alarm/alarmList.vue

@@ -18,17 +18,13 @@
           <div class="stat-num">{{ warningCount }}</div>
           <div class="stat-label">一般报警</div>
         </div>
-        <div class="stat-item resolved" @click="filterAlerts('resolved')">
-          <div class="stat-num">{{ resolvedCount }}</div>
-          <div class="stat-label">已处置</div>
-        </div>
       </div>
     </div>
 
     <!-- 主内容区域:左侧报警列表 + 右侧详情研判 -->
     <div class="main-content">
       <!-- 左侧报警列表 -->
-      <div class="alert-list-panel">
+      <div class="alert-list-panel" v-loading="listLoading">
         <div class="panel-header">
           <span>报警事件列表 <el-badge :value="unhandledCount" class="badge" /></span>
           <el-input v-model="searchText" placeholder="搜索位置/设备编号" prefix-icon="Search" clearable size="small" style="width: 180px" />
@@ -71,20 +67,16 @@
               <el-tag type="danger" size="small">待处理</el-tag>
             </div>
           </div>
+          <el-empty v-if="!filteredAlerts.length && !listLoading" description="暂无报警事件" :image-size="100" />
         </div>
       </div>
 
       <!-- 右侧报警详情与研判面板 -->
-      <div class="detail-panel" v-if="selectedAlert">
+      <div class="detail-panel" v-if="selectedAlert" v-loading="detailLoading">
         <!-- 报警基本信息 -->
         <div class="detail-section">
           <div class="section-title">
             <span><el-icon><Warning /></el-icon> 报警详情 - 辅助研判</span>
-            <div class="action-buttons">
-              <el-button v-if="selectedAlert.status === 'pending'" type="primary" size="small" @click="startProcess">开始处理</el-button>
-              <el-button v-if="selectedAlert.status !== 'resolved'" type="success" size="small" @click="resolveAsTrue">确认为真报警</el-button>
-              <el-button v-if="selectedAlert.status !== 'resolved'" type="info" size="small" @click="resolveAsFalse">误报警排除</el-button>
-            </div>
           </div>
           <el-descriptions :column="2" border>
             <el-descriptions-item label="报警位置">
@@ -110,67 +102,6 @@
             </el-descriptions-item>
           </el-descriptions>
         </div>
-
-        <!-- 实时监测曲线 (帮助分析异常趋势) -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon><TrendCharts /></el-icon> 实时监测数据趋势(近1小时)</span>
-            <span class="sub-hint">辅助综合研判,排除误报</span>
-          </div>
-          <div class="chart-container">
-            <v-chart :option="realtimeChartOption" autoresize />
-          </div>
-          <div class="analysis-tips">
-            <el-alert
-                :title="analysisSuggestion"
-                type="info"
-                show-icon
-                :closable="false"
-                description="结合历史数据和当前环境因素,辅助判断报警真实性。"
-            />
-          </div>
-        </div>
-
-        <!-- 处置记录与研判日志 -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon><Document /></el-icon> 处置记录 & 研判日志</span>
-            <el-button size="small" link @click="addNote">添加备注</el-button>
-          </div>
-          <el-timeline>
-            <el-timeline-item
-                v-for="log in selectedAlert.logs"
-                :key="log.id"
-                :timestamp="log.time"
-                :type="log.type"
-                :hollow="true"
-            >
-              {{ log.content }}
-            </el-timeline-item>
-            <el-timeline-item v-if="!selectedAlert.logs?.length" timestamp="暂无处置记录" type="info">
-              系统已记录报警,待处理
-            </el-timeline-item>
-          </el-timeline>
-        </div>
-
-        <!-- 相似历史报警参考 -->
-        <div class="detail-section">
-          <div class="section-title">
-            <span><el-icon></el-icon> 相似历史报警参考</span>
-          </div>
-          <div class="history-ref">
-            <el-table :data="similarAlerts" size="small" style="width: 100%">
-              <el-table-column prop="time" label="报警时间" width="140" />
-              <el-table-column prop="alertType" label="报警类型" />
-              <el-table-column prop="realData" label="实时数据" width="100" />
-              <el-table-column prop="result" label="研判结果" width="100">
-                <template #default="{ row }">
-                  <el-tag :type="row.result === '真报警' ? 'danger' : 'info'" size="small">{{ row.result }}</el-tag>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </div>
       </div>
 
       <!-- 未选中报警时的空状态 -->
@@ -179,169 +110,250 @@
       </div>
     </div>
 
-    <!-- 地图弹窗(模拟GIS位置) -->
+    <!-- 地图弹窗 -->
     <el-dialog v-model="mapDialogVisible" title="报警位置地图" width="600px">
       <div id="simpleMap" style="height: 400px; background: #e9f5e9; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
         <el-icon :size="48" color="#f56c6c"><Location /></el-icon>
         <div><strong>{{ selectedAlert?.location }}</strong></div>
-        <div>经度: {{ selectedAlert?.lng || '121.487' }}, 纬度: {{ selectedAlert?.lat || '31.249' }}</div>
-        <div style="margin-top: 20px; font-size: 12px; color: gray">(GIS模拟定位 - 报警点闪烁标记)</div>
+        <div>经度: {{ selectedAlert?.lng || '暂无数据' }}, 纬度: {{ selectedAlert?.lat || '暂无数据' }}</div>
+        <div style="margin-top: 20px; font-size: 12px; color: gray">当前仅展示接口返回的真实坐标信息</div>
       </div>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
-import { BellFilled, WarningFilled, InfoFilled, CircleCheck, Location, Warning, TrendCharts, Document, CircleClose, Search } from '@element-plus/icons-vue'
-import { use } from 'echarts/core'
-import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart } from 'echarts/charts'
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
-import { ElMessage } from 'element-plus'
+import {computed, onMounted, ref} from 'vue'
+import {CircleCheck, InfoFilled, Location, Warning, WarningFilled} from '@element-plus/icons-vue'
+import {ElMessage} from 'element-plus'
+import {getManholeAlarmDeviceDetail, getManholeAlarmDeviceList} from '@/api/pipeNetwork/basic'
 
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
-
-// 模拟报警数据
-const generateAlerts = () => [
-  {
-    id: 1, deviceId: 'MN-1001', deviceName: '人民路智能井盖', deviceType: '智能井盖监测仪',
-    location: '人民路与解放路口东50m', lng: 121.487, lat: 31.249,
-    alertType: '倾斜角度超限', alertParam: '倾斜角', realData: '12.5°', threshold: '8°', exceedValue: '12.5°',
-    level: 'critical', time: '2025-04-02 14:23:15', status: 'pending',
-    logs: [{ id: 1, time: '2025-04-02 14:23:20', content: '系统自动触发报警', type: 'danger' }]
-  },
-  {
-    id: 2, deviceId: 'MN-1002', deviceName: '滨江路防爆井盖', deviceType: '防爆井盖监测仪',
-    location: '滨江路化工园区南门', lng: 121.502, lat: 31.235,
-    alertType: '水位超限', alertParam: '水位深度', realData: '58cm', threshold: '40cm', exceedValue: '58cm',
-    level: 'critical', time: '2025-04-02 13:55:02', status: 'pending',
-    logs: [{ id: 1, time: '2025-04-02 13:55:10', content: '井下水位超阈值告警', type: 'danger' }]
-  },
-  {
-    id: 3, deviceId: 'MN-1003', deviceName: '古城街铸铁井盖', deviceType: '普通井盖监测仪',
-    location: '古城街北段树下', lng: 121.478, lat: 31.258,
-    alertType: '异常震动', alertParam: '震动强度', realData: '112mg', threshold: '80mg', exceedValue: '112mg',
-    level: 'warning', time: '2025-04-02 12:10:33', status: 'processing',
-    logs: [{ id: 1, time: '2025-04-02 12:15:00', content: '已派发巡检人员现场核实', type: 'warning' }]
-  },
-  {
-    id: 4, deviceId: 'MN-1005', deviceName: '南港路井盖', deviceType: '智能井盖监测仪',
-    location: '南港路旧车站段', lng: 121.495, lat: 31.228,
-    alertType: '井盖开启异常', alertParam: '开盖状态', realData: '开启', threshold: '关闭', exceedValue: '异常开启',
-    level: 'warning', time: '2025-04-02 11:45:20', status: 'pending',
-    logs: []
-  },
-  {
-    id: 5, deviceId: 'EX-3002', deviceName: '石化区防爆井盖', deviceType: '防爆井盖监测仪',
-    location: '石化大道南侧', lng: 121.524, lat: 31.239,
-    alertType: '气体浓度超标', alertParam: 'H₂S浓度', realData: '28ppm', threshold: '15ppm', exceedValue: '28ppm',
-    level: 'critical', time: '2025-04-02 10:30:45', status: 'resolved',
-    logs: [{ id: 1, time: '2025-04-02 11:00:00', content: '经现场确认,为真报警,已通风处置', type: 'success' }]
-  }
-]
-
-const alerts = ref(generateAlerts())
+const alerts = ref([])
 const searchText = ref('')
 const filterType = ref('all')
-const selectedAlert = ref(alerts.value[0])
+const selectedAlert = ref(null)
 const mapDialogVisible = ref(false)
+const listLoading = ref(false)
+const detailLoading = ref(false)
 
-const filteredAlerts = computed(() => {
-  let list = alerts.value
-  if (filterType.value === 'critical') list = list.filter(a => a.level === 'critical' && a.status !== 'resolved')
-  else if (filterType.value === 'warning') list = list.filter(a => a.level === 'warning' && a.status !== 'resolved')
-  else if (filterType.value === 'resolved') list = list.filter(a => a.status === 'resolved')
-  else list = list.filter(a => a.status !== 'resolved' || filterType.value === 'resolved' ? true : a.status !== 'resolved')
+function toNumber(value) {
+  const num = Number(value)
+  return Number.isFinite(num) ? num : null
+}
 
-  if (searchText.value) {
-    const kw = searchText.value.toLowerCase()
-    list = list.filter(a => a.location.toLowerCase().includes(kw) || a.deviceId.toLowerCase().includes(kw))
+function formatDateTime(value) {
+  if (!value) return '—'
+  return String(value).replace('T', ' ')
+}
+
+function getThresholdTextByCode(raw, warningCode) {
+  const list = Array.isArray(raw?.warningThresholdList) ? raw.warningThresholdList : []
+  const target = list.find(item => item.warningCode === warningCode)
+  if (!target) return '—'
+  const { minValue, maxValue } = target
+  if (minValue !== null && minValue !== undefined && maxValue !== null && maxValue !== undefined) {
+    return `${minValue} ~ ${maxValue}`
   }
-  return list
-})
+  if (minValue !== null && minValue !== undefined) return `<= ${minValue}`
+  if (maxValue !== null && maxValue !== undefined) return `>= ${maxValue}`
+  return '—'
+}
 
-const totalAlerts = computed(() => alerts.value.filter(a => a.status !== 'resolved').length)
-const criticalCount = computed(() => alerts.value.filter(a => a.level === 'critical' && a.status !== 'resolved').length)
-const warningCount = computed(() => alerts.value.filter(a => a.level === 'warning' && a.status !== 'resolved').length)
-const resolvedCount = computed(() => alerts.value.filter(a => a.status === 'resolved').length)
-const unhandledCount = computed(() => alerts.value.filter(a => a.status === 'pending').length)
+function buildAlertInfo(raw) {
+  const status = raw?.equipmentStatus || {}
+  const manholeData = raw?.manholeData || {}
+  const battery = toNumber(manholeData.batteryLevel)
+  const signal = toNumber(manholeData.signalStrength)
+  const tiltAngle = toNumber(manholeData.tiltAngle)
 
-const filterAlerts = (type) => { filterType.value = type }
+  if (`${manholeData.waterLevelAlarmStatus}` === '1') {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: '水位报警',
+      alertParam: '水位',
+      realData: manholeData.waterLevel !== undefined && manholeData.waterLevel !== null ? `${manholeData.waterLevel}cm` : '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-LEVEL')
+    }
+  }
 
-const selectAlert = (alert) => { selectedAlert.value = alert }
+  if (`${manholeData.waterInfiltrationAlarmStatus}` === '1') {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: '水浸报警',
+      alertParam: '水浸状态',
+      realData: '报警',
+      threshold: getThresholdTextByCode(raw, 'WARN-WATER-INFILTRATION')
+    }
+  }
 
-// 模拟实时数据曲线(基于报警类型生成模拟趋势数据)
-const realtimeChartOption = computed(() => {
-  if (!selectedAlert.value) return {}
-  const param = selectedAlert.value.alertParam || '数值'
-  const unit = selectedAlert.value.alertType.includes('角度') ? '°' : selectedAlert.value.alertType.includes('水位') ? 'cm' : selectedAlert.value.alertType.includes('震动') ? 'mg' : ''
-  // 模拟最近12个时间点的数据,最后一点为报警值突出
-  const baseValue = parseFloat(selectedAlert.value.realData) || 20
-  const thresholdVal = parseFloat(selectedAlert.value.threshold) || 10
-  const mockData = Array.from({ length: 12 }, (_, i) => {
-    if (i === 11) return baseValue
-    return baseValue - (Math.random() * (baseValue - thresholdVal) * 0.6 + 2)
-  })
-  const times = Array.from({ length: 12 }, (_, i) => `${i * 5}分钟前`).reverse()
-  return {
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: times },
-    yAxis: { type: 'value', name: `${param} (${unit})` },
-    series: [
-      { name: '实时监测值', type: 'line', data: mockData, smooth: true, lineStyle: { color: '#f56c6c', width: 2 }, areaStyle: { opacity: 0.2 }, markLine: { data: [{ yAxis: thresholdVal, name: '阈值线', lineStyle: { color: '#e6a23c', type: 'dashed' } }] } }
-    ]
+  if (Number(status.alarmStatus) === 1) {
+    return {
+      hasAlert: true,
+      level: 'critical',
+      levelText: '严重报警',
+      alertType: tiltAngle !== null ? '倾斜报警' : '设备报警',
+      alertParam: tiltAngle !== null ? '倾斜角' : '设备状态',
+      realData: tiltAngle !== null ? `${tiltAngle}°` : '报警',
+      threshold: tiltAngle !== null ? getThresholdTextByCode(raw, 'WARN-TILT') : '—'
+    }
   }
-})
 
-const analysisSuggestion = computed(() => {
-  if (!selectedAlert.value) return ''
-  const alert = selectedAlert.value
-  if (alert.alertType.includes('倾斜')) return '当前倾斜角度超阈值12.5°,超过设定值50%以上,且近期有道路施工记录,建议现场核实是否为施工影响或井盖破损。'
-  if (alert.alertType.includes('水位')) return '水位突增至58cm,查看过去2小时降雨量数据无雨情,可能为管道堵塞,建议立即排水检修,排除误报。'
-  if (alert.alertType.includes('震动')) return '震动峰值112mg,周边无大型车辆记录,可能由于井盖松动或人为敲击,建议巡检。'
-  return '结合历史报警记录,该测点一周内无相似报警,当前数据超出阈值明显,真报警可能性较高,建议优先处置。'
-})
+  if (battery !== null && battery <= 20) {
+    return {
+      hasAlert: true,
+      level: 'warning',
+      levelText: '一般报警',
+      alertType: '低电量预警',
+      alertParam: '电池电量',
+      realData: `${battery}%`,
+      threshold: '20%'
+    }
+  }
 
-const similarAlerts = computed(() => {
-  if (!selectedAlert.value) return []
-  return alerts.value.filter(a => a.alertType === selectedAlert.value.alertType && a.id !== selectedAlert.value.id && a.status === 'resolved').slice(0, 3).map(a => ({
-    time: a.time, alertType: a.alertType, realData: a.realData, result: a.result || '真报警'
-  }))
-})
+  if (signal !== null && signal <= 20) {
+    return {
+      hasAlert: true,
+      level: 'warning',
+      levelText: '一般报警',
+      alertType: '弱信号预警',
+      alertParam: '信号强度',
+      realData: `${signal}%`,
+      threshold: '20%'
+    }
+  }
+
+  return {
+    hasAlert: false,
+    level: '',
+    levelText: '',
+    alertType: '',
+    alertParam: '',
+    realData: '',
+    threshold: '—'
+  }
+}
 
-const startProcess = () => {
-  if (selectedAlert.value.status === 'pending') {
-    selectedAlert.value.status = 'processing'
-    selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '值班人员开始处理报警', type: 'warning' })
-    ElMessage.info('已开始处理该报警事件')
+function normalizeAlert(raw, baseAlert = null) {
+  const alertInfo = buildAlertInfo(raw)
+  if (!alertInfo.hasAlert) return null
+  return {
+    id: raw?.equipmentId || raw?.equipmentCode,
+    equipmentId: raw?.equipmentId || '',
+    deviceId: raw?.equipmentCode || raw?.equipmentId || '—',
+    deviceName: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
+    deviceType: raw?.equipmentTypeName || '井盖监测设备',
+    location: raw?.equipmentLocation || '暂无位置信息',
+    lng: raw?.longitude || baseAlert?.lng || '',
+    lat: raw?.latitude || baseAlert?.lat || '',
+    alertType: alertInfo.alertType,
+    alertParam: alertInfo.alertParam,
+    realData: alertInfo.realData,
+    threshold: alertInfo.threshold,
+    exceedValue: alertInfo.realData || '报警',
+    level: alertInfo.level,
+    levelText: alertInfo.levelText,
+    time: formatDateTime(raw?.equipmentStatus?.statusUpdateTime || raw?.updateTime || raw?.installTime),
+    status: raw?.equipmentStatus?.alarmStatus === 1 ? 'pending' : 'resolved',
+    statusText: raw?.equipmentStatus?.alarmStatus === 1 ? '待处理' : '已恢复',
+    raw
   }
 }
 
-const resolveAsTrue = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  selectedAlert.value.status = 'resolved'
-  selectedAlert.value.result = '真报警'
-  selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '经研判确认为真报警,已派发维修工单', type: 'success' })
-  ElMessage.success('已确认真报警,处置流程已启动')
+async function loadAlerts() {
+  listLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceList({
+      alarmStatus: 1,
+      isQueryManholeData: true
+    })
+    const list = Array.isArray(res?.data) ? res.data : []
+    alerts.value = list
+      .map(item => normalizeAlert(item))
+      .filter(Boolean)
+    if (!alerts.value.length) {
+      selectedAlert.value = null
+      return
+    }
+    const currentId = selectedAlert.value?.id
+    selectedAlert.value = alerts.value.find(item => item.id === currentId) || alerts.value[0]
+    if (selectedAlert.value?.equipmentId) {
+      await loadAlertDetail(selectedAlert.value)
+    }
+  } catch (error) {
+    alerts.value = []
+    selectedAlert.value = null
+    ElMessage.error('报警设备加载失败')
+  } finally {
+    listLoading.value = false
+  }
 }
 
-const resolveAsFalse = () => {
-  if (selectedAlert.value.status === 'resolved') return
-  selectedAlert.value.status = 'resolved'
-  selectedAlert.value.result = '误报警'
-  selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: '经现场核实,该报警为误报(环境干扰/传感器波动),已排除', type: 'info' })
-  ElMessage.info('已标记为误报警,事件关闭')
+async function loadAlertDetail(alert) {
+  if (!alert?.equipmentId) return
+  detailLoading.value = true
+  try {
+    const res = await getManholeAlarmDeviceDetail(alert.equipmentId)
+    if (res?.data) {
+      const merged = normalizeAlert(res.data, alert)
+      if (merged) {
+        const idx = alerts.value.findIndex(item => item.id === merged.id)
+        if (idx > -1) {
+          alerts.value[idx] = merged
+        }
+        selectedAlert.value = merged
+      }
+    }
+  } catch (error) {
+    ElMessage.error('报警详情加载失败')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
-const viewOnMap = () => { mapDialogVisible.value = true }
-const addNote = () => {
-  const note = prompt('请输入研判备注信息')
-  if (note && selectedAlert.value) {
-    selectedAlert.value.logs.push({ id: Date.now(), time: new Date().toLocaleString(), content: `备注: ${note}`, type: 'info' })
-    ElMessage.success('备注已添加')
+const filteredAlerts = computed(() => {
+  let list = alerts.value
+  if (filterType.value === 'critical') {
+    list = list.filter(a => a.level === 'critical')
+  } else if (filterType.value === 'warning') {
+    list = list.filter(a => a.level === 'warning')
+  }
+
+  if (searchText.value) {
+    const kw = searchText.value.toLowerCase()
+    list = list.filter(a =>
+      `${a.location || ''}`.toLowerCase().includes(kw) ||
+      `${a.deviceId || ''}`.toLowerCase().includes(kw) ||
+      `${a.deviceName || ''}`.toLowerCase().includes(kw)
+    )
   }
+  return list
+})
+
+const totalAlerts = computed(() => alerts.value.length)
+const criticalCount = computed(() => alerts.value.filter(a => a.level === 'critical').length)
+const warningCount = computed(() => alerts.value.filter(a => a.level === 'warning').length)
+const unhandledCount = computed(() => alerts.value.length)
+
+const filterAlerts = (type) => {
+  filterType.value = type
+}
+
+const selectAlert = async (alert) => {
+  selectedAlert.value = alert
+  await loadAlertDetail(alert)
 }
+
+const viewOnMap = () => {
+  mapDialogVisible.value = true
+}
+
+onMounted(async () => {
+  await loadAlerts()
+})
 </script>
 
 <style scoped>
@@ -433,9 +445,6 @@ const addNote = () => {
 }
 .real-time-data { background: #fff9f0; padding: 8px; border-radius: 8px; }
 .warning-value { color: #f56c6c; font-size: 18px; margin: 0 4px; }
-.chart-container { height: 220px; }
-.analysis-tips { margin-top: 12px; }
 .action-buttons { display: flex; gap: 8px; }
 .empty-detail { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
-.history-ref { margin-top: 8px; }
-</style>
+</style>