Przeglądaj źródła

重构-窨井子系统-数据统计

zengy 1 tydzień temu
rodzic
commit
d7828f8faf

+ 5 - 4
src/api/pipeNetwork/basic.js

@@ -100,10 +100,11 @@ export function getManholeDataPage(params) {
 }
 
 
-// ============ 运维数据统计 ============
-// 运维看板统计数据:一次返回KPI指标/异常终端趋势/处置率延期率趋势/问题类型分布/区域排行/异常终端列表
-export function getManholeMaintenanceDashboard(data = {}) {
-  return request({ url: '/manhole/maintenanceDataStat/getDashboardStat', method: 'post', data })
+// ============ 报警运维看板 ============
+// 一次返回:今日新增/较昨日、已转/未转工单、处置率、平均响应/处置时长、
+// 分级卡片(固定4项)、近7日趋势(固定7项)、异常报警列表
+export function getAlarmDashboard() {
+  return request({ url: '/manhole/alarmDashboard/getDashboardStat', method: 'post' })
 }
 
 // ============ 维修记录 ============

+ 290 - 522
src/views/subSystem/manholeCover/operation/mOdataStatistics.vue

@@ -1,363 +1,198 @@
-<!--数据统计 - 井盖设备智能终端运维看板-->
+<!-- 数据统计 - 井盖设备智能终端运维看板(报警运维看板) -->
 <template>
-  <div class="terminal-operation-dashboard">
-    <!-- 页面头部 -->
-    <div class="page-header">
+  <div class="alarm-dashboard">
+    <header class="page-header">
       <div class="header-left">
         <el-icon><Monitor /></el-icon>
         <span class="title">井盖设备智能终端运维看板</span>
-        <el-tag type="danger" effect="dark">异常终端 {{ kpi.abnormal }}</el-tag>
       </div>
-      <div class="header-right">
-        <el-date-picker
-          v-model="dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          style="width: 260px"
-          size="default"
-          @change="loadAll"
-        />
-        <el-button type="primary" :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
-      </div>
-    </div>
+      <el-button :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
+    </header>
 
-    <!-- KPI 指标卡片 -->
-    <div class="kpi-cards">
+    <!-- 第一行指标 -->
+    <div class="kpi-grid">
       <div class="kpi-card">
-        <div class="kpi-value">{{ kpi.total }}</div>
-        <div class="kpi-label">终端总数</div>
-      </div>
-      <div class="kpi-card warning">
-        <div class="kpi-value">{{ kpi.abnormal }}</div>
-        <div class="kpi-label">异常终端</div>
-        <div class="kpi-trend">占比 {{ kpi.abnormalRate }}%</div>
-      </div>
-      <div class="kpi-card success">
-        <div class="kpi-value">{{ kpi.solvedRate }}%</div>
-        <div class="kpi-label">问题解决率</div>
-        <div class="kpi-trend">已派发 {{ kpi.submittedCount }} / 异常 {{ kpi.abnormal }}</div>
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><Calendar /></el-icon></span>今日新增报警</div>
+        <div class="kpi-value green">{{ kpi.todayCount }}</div>
+        <div class="kpi-foot">较昨日 <span :class="diffClass">{{ diffText }}</span></div>
       </div>
       <div class="kpi-card">
-        <div class="kpi-value">{{ kpi.disposalRate }}%</div>
-        <div class="kpi-label">案件处置率</div>
-        <div class="kpi-trend">工单总数 {{ kpi.totalCase }}</div>
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><Document /></el-icon></span>已转工单</div>
+        <div class="kpi-value">{{ kpi.transferredCount }}</div>
+        <div class="kpi-foot">已派发处置的报警</div>
       </div>
       <div class="kpi-card">
-        <div class="kpi-value">{{ kpi.delayRate }}%</div>
-        <div class="kpi-label">延期率</div>
-        <div class="kpi-trend" :class="{ 'trend-down': Number(kpi.delayRate) > 0 }">统计范围内</div>
-      </div>
-      <div class="kpi-card">
-        <div class="kpi-value">{{ kpi.avgResponseTime }}</div>
-        <div class="kpi-label">平均响应时长(小时)</div>
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><Warning /></el-icon></span>未转工单</div>
+        <div class="kpi-value orange">{{ kpi.untransferredCount }}</div>
+        <div class="kpi-foot">待核实转派的报警</div>
       </div>
     </div>
 
-    <!-- 图表区域 -->
-    <div class="charts-row">
-      <div class="chart-card">
-        <div class="chart-header">
-          <span>异常终端数量趋势(月度)</span>
-        </div>
-        <div ref="trendChartRef" class="chart-container" v-loading="loading"></div>
-      </div>
-      <div class="chart-card">
-        <div class="chart-header">
-          <span>问题类型分布</span>
-        </div>
-        <div ref="problemTypeChartRef" class="chart-container" v-loading="loading"></div>
+    <!-- 第二行指标 -->
+    <div class="kpi-grid">
+      <div class="kpi-card">
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><CircleCheck /></el-icon></span>报警处置率</div>
+        <div class="kpi-value green">{{ kpi.disposalRate }}<small>%</small></div>
+        <div class="kpi-foot">已处置异常占比</div>
       </div>
-    </div>
-
-    <div class="charts-row">
-      <div class="chart-card">
-        <div class="chart-header">
-          <span>处置率 vs 延期率趋势</span>
-        </div>
-        <div ref="disposalTrendChartRef" class="chart-container" v-loading="loading"></div>
+      <div class="kpi-card">
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><Clock /></el-icon></span>平均响应时长</div>
+        <div class="kpi-value">{{ formatDuration(kpi.avgResponseHours) }}</div>
+        <div class="kpi-foot">报警处置响应</div>
       </div>
-      <div class="chart-card">
-        <div class="chart-header">
-          <span>区域运维情况排行</span>
-        </div>
-        <div ref="regionRankChartRef" class="chart-container" v-loading="loading"></div>
+      <div class="kpi-card">
+        <div class="kpi-head"><span class="kpi-icon"><el-icon><Timer /></el-icon></span>平均处置时长</div>
+        <div class="kpi-value">{{ formatDuration(kpi.avgDisposalHours) }}</div>
+        <div class="kpi-foot">报警处置完成</div>
       </div>
     </div>
 
-    <!-- 异常终端列表 -->
-    <div class="device-list-container">
-      <div class="list-header">
-        <span><el-icon><List /></el-icon> 异常终端列表(共 {{ filteredAbnormalDevices.length }} 台)</span>
-        <div class="list-filters">
-          <el-input
-            v-model="searchKeyword"
-            placeholder="搜索设备编号/名称/位置"
-            clearable
-            :prefix-icon="Search"
-            style="width: 200px"
-            size="small"
-          />
-          <el-select v-model="faultTypeFilter" placeholder="故障类型" clearable size="small" style="width: 130px">
-            <el-option label="倾斜超标" value="tilt" />
-            <el-option label="水位超限" value="waterLevel" />
-            <el-option label="水浸报警" value="waterInfiltration" />
-            <el-option label="通讯故障" value="offline" />
-            <el-option label="电池低电量" value="battery" />
-            <el-option label="信号弱" value="signal" />
-          </el-select>
-          <el-button type="primary" size="small" @click="currentPage = 1">查询</el-button>
-        </div>
-      </div>
-      <el-table :data="paginatedAbnormalDevices" stripe border style="width: 100%" v-loading="loading">
-        <el-table-column type="index" label="序号" width="55" align="center" />
-        <el-table-column prop="code" label="设备编号" width="140" sortable />
-        <el-table-column prop="name" label="设备名称" width="140" show-overflow-tooltip />
-        <el-table-column prop="deviceTypeName" label="设备类型" width="100" />
-        <el-table-column label="故障类型" width="110">
-          <template #default="{ row }">
-            <el-tag v-for="ft in row.faultTypes" :key="ft.type" :type="getFaultTagType(ft.type)" size="small" style="margin: 1px">{{ ft.name }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="faultDesc" label="故障描述" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="locationShort" label="所在位置" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="abnormalTime" label="故障时间" width="150" sortable />
-        <el-table-column label="处理状态" width="100" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.workOrderSubmitted ? 'success' : 'danger'" size="small">
-              {{ row.workOrderSubmitted ? '已派发' : '待派发' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="elapsedHours" label="已耗时(h)" width="100" sortable align="center">
-          <template #default="{ row }">
-            <span :class="{ 'over-time': row.elapsedHours > 48 }">{{ row.elapsedHours }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="100" fixed="right" align="center">
-          <template #default="{ row }">
-            <el-button link type="primary" size="small" @click="viewDetail(row)">详情</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <div class="pagination-wrapper">
-        <el-pagination
-          background
-          layout="total, sizes, prev, pager, next"
-          :total="filteredAbnormalDevices.length"
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
-          :page-sizes="[10, 20, 50]"
-        />
+    <!-- 已核实报警 · 分级 -->
+    <div class="section-title"><span class="title-bar"></span>已核实报警 · 分级</div>
+    <div class="level-grid">
+      <div v-for="(item, index) in levels" :key="item.level" class="level-card" :class="'level-' + (index + 1)">
+        <div class="level-value">{{ item.count ?? 0 }}</div>
+        <div class="level-name">{{ item.name }}</div>
       </div>
     </div>
 
-    <!-- 终端详情抽屉 -->
-    <el-drawer v-model="detailDrawerVisible" :title="`终端详情 - ${currentDevice?.code}`" size="450px" direction="rtl" class="detail-drawer">
-      <div v-if="currentDevice" class="device-detail">
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="设备编号">{{ currentDevice.code }}</el-descriptions-item>
-          <el-descriptions-item label="设备名称">{{ currentDevice.name }}</el-descriptions-item>
-          <el-descriptions-item label="设备类型">{{ currentDevice.deviceTypeName }}</el-descriptions-item>
-          <el-descriptions-item label="故障描述">{{ currentDevice.faultDesc }}</el-descriptions-item>
-          <el-descriptions-item label="故障时间">{{ currentDevice.abnormalTime }}</el-descriptions-item>
-          <el-descriptions-item label="详细地址">{{ currentDevice.location }}</el-descriptions-item>
-          <el-descriptions-item label="处理状态">
-            <el-tag :type="currentDevice.workOrderSubmitted ? 'success' : 'danger'">
-              {{ currentDevice.workOrderSubmitted ? '已派发' : '待派发' }}
-            </el-tag>
-          </el-descriptions-item>
-        </el-descriptions>
-
-        <!-- 监测数据 -->
-        <div class="sub-title">监测数据</div>
-        <div class="monitor-grid">
-          <div class="monitor-item" :class="{ alert: currentDevice.status.tilt }">
-            <div class="m-label">倾斜角度</div>
-            <div class="m-value">{{ currentDevice.tiltAngle }}°</div>
-            <div class="m-extra">阈值: {{ currentDevice.angleThreshold }}°</div>
-          </div>
-          <div class="monitor-item" :class="{ alert: currentDevice.status.waterInfiltration }">
-            <div class="m-label">水浸</div>
-            <div class="m-value">{{ currentDevice.status.waterInfiltration ? '报警' : '正常' }}</div>
-          </div>
-          <div class="monitor-item" :class="{ alert: currentDevice.status.waterLevel }">
-            <div class="m-label">水位</div>
-            <div class="m-value">{{ currentDevice.status.waterLevel ? '超标' : '正常' }}</div>
-          </div>
-          <div class="monitor-item" :class="{ alert: currentDevice.status.lowBattery }">
-            <div class="m-label">电量</div>
-            <div class="m-value">{{ currentDevice.battery }}%</div>
-          </div>
-          <div class="monitor-item" :class="{ alert: currentDevice.status.lowSignal }">
-            <div class="m-label">信号</div>
-            <div class="m-value">{{ currentDevice.signal }}%</div>
-          </div>
-          <div class="monitor-item">
-            <div class="m-label">温度</div>
-            <div class="m-value">{{ currentDevice.temperature || '—' }}°C</div>
-          </div>
-        </div>
-      </div>
-    </el-drawer>
+    <!-- 近 7 日报警数 -->
+    <div class="section-title"><span class="title-bar"></span>近 7 日报警数</div>
+    <div ref="trendRef" class="trend-chart" v-loading="loading"></div>
+
+    <!-- 实时故障列表 -->
+    <div class="section-title"><span class="title-bar"></span>实时故障列表<span class="title-count">共 {{ abnormalAlarms.length }} 条</span></div>
+    <el-table :data="pagedAlarms" class="alarm-table" size="small" v-loading="loading">
+      <el-table-column prop="deviceCode" label="设备编码" min-width="150" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.deviceCode || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.deviceName || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="installLocation" label="设备安装位置" min-width="180" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.installLocation || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="faultDesc" label="故障描述" min-width="200" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.faultDesc || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="faultTime" label="故障发生时间" min-width="160" show-overflow-tooltip>
+        <template #default="{ row }">{{ row.faultTime || '-' }}</template>
+      </el-table-column>
+    </el-table>
+    <div class="pagination-wrapper">
+      <el-pagination
+        background
+        layout="total, sizes, prev, pager, next"
+        :total="abnormalAlarms.length"
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50]"
+      />
+    </div>
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
-import { Monitor, Refresh, List, Search } from '@element-plus/icons-vue'
+import {
+  Monitor, Refresh, Calendar, Document, Warning, CircleCheck, Clock, Timer
+} from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import * as echarts from 'echarts'
-import {
-  getManholeMaintenanceDashboard
-} from '@/api/pipeNetwork/basic'
-
-// ==================== 常量 ====================
-const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
-
-// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
-const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
-function districtLabel(code) {
-  return code == null ? '' : (DISTRICT_MAP[code] || String(code))
-}
-const LOW_BATTERY_THRESHOLD = 15
-const LOW_SIGNAL_THRESHOLD = 20
+import { getAlarmDashboard } from '@/api/pipeNetwork/basic'
 
 // ==================== 状态 ====================
 const loading = ref(false)
-const dateRange = ref([])
-const searchKeyword = ref('')
-const faultTypeFilter = ref('')
 const currentPage = ref(1)
 const pageSize = ref(10)
 
-const detailDrawerVisible = ref(false)
-const currentDevice = ref(null)
-
 const kpi = reactive({
-  total: 0, abnormal: 0, abnormalRate: '0.0',
-  solvedRate: 0, submittedCount: 0, totalCase: 0,
-  disposalRate: 0, delayRate: 0,
-  avgResponseTime: '—'
+  todayCount: 0,
+  yesterdayCount: 0,
+  todayDiff: 0,
+  transferredCount: 0,
+  untransferredCount: 0,
+  disposalRate: 0,
+  avgResponseHours: null,
+  avgDisposalHours: null
 })
 
-const abnormalDevices = ref([])
-
-// 图表引用
-const trendChartRef = ref(null)
-const problemTypeChartRef = ref(null)
-const disposalTrendChartRef = ref(null)
-const regionRankChartRef = ref(null)
-let trendChart = null, problemTypeChart = null, disposalTrendChart = null, regionRankChart = null
-
-// ==================== 日期工具 ====================
-function formatDate(date) {
-  const y = date.getFullYear()
-  const m = String(date.getMonth() + 1).padStart(2, '0')
-  const d = String(date.getDate()).padStart(2, '0')
-  return `${y}-${m}-${d}`
-}
-
-// ==================== 设备数据规范化 ====================
-function normalizeDevice(raw) {
-  const md = raw?.manholeData || {}
-  const st = raw?.equipmentStatus || {}
-  const typeId = raw?.equipmentTypeId || ''
-
-  const tiltAngle = parseFloat(md.tiltAngle) || 0
-  const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
-  const battery = parseFloat(md.batteryLevel) || 0
-  const signal = parseFloat(md.signalStrength) || 0
-  const temperature = md.temperatureValue ? parseFloat(md.temperatureValue).toFixed(1) : null
-  const isOnline = Number(st.onlineStatus) === 1
-
-  const tilt = isOnline && tiltAngle > angleThreshold
-  const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
-  const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
-  const lowBattery = battery > 0 && battery <= LOW_BATTERY_THRESHOLD
-  const lowSignal = signal > 0 && signal <= LOW_SIGNAL_THRESHOLD
-  const offline = !isOnline
-
-  const faultTypes = []
-  if (tilt) faultTypes.push({ type: 'tilt', name: '倾斜超标' })
-  if (waterLevel) faultTypes.push({ type: 'waterLevel', name: '水位超限' })
-  if (waterInfiltration) faultTypes.push({ type: 'waterInfiltration', name: '水浸报警' })
-  if (offline) faultTypes.push({ type: 'offline', name: '通讯故障' })
-  if (lowBattery) faultTypes.push({ type: 'battery', name: '电池低电量' })
-  if (lowSignal) faultTypes.push({ type: 'signal', name: '信号弱' })
-
-  const hasAlert = faultTypes.length > 0
-
-  const parts = []
-  if (tilt) parts.push(`倾斜角度${tiltAngle}°超过阈值${angleThreshold}°`)
-  if (waterInfiltration) parts.push('水浸报警')
-  if (waterLevel) parts.push('水位超标报警')
-  if (lowBattery) parts.push(`电量仅剩${battery}%`)
-  if (lowSignal) parts.push(`信号强度${signal}%`)
-  if (offline) parts.push('设备离线')
+// 四个等级卡片固定展示,接口无数据时全部为 0
+const FALLBACK_LEVELS = [
+  { level: '1', name: '一级报警', count: 0 },
+  { level: '2', name: '二级报警', count: 0 },
+  { level: '3', name: '三级报警', count: 0 },
+  { level: '4', name: '四级报警', count: 0 }
+]
+const levels = ref(FALLBACK_LEVELS.map(item => ({ ...item })))
+const trend = ref([])
+const abnormalAlarms = ref([])
+const trendRef = ref(null)
+let trendChart = null
+
+// ==================== 展示格式化 ====================
+const diffText = computed(() => {
+  const diff = Number(kpi.todayDiff) || 0
+  return diff > 0 ? `+${diff}` : String(diff)
+})
+const diffClass = computed(() => {
+  const diff = Number(kpi.todayDiff) || 0
+  return diff > 0 ? 'up' : diff < 0 ? 'down' : 'flat'
+})
 
-  let elapsedHours = 0
-  const faultTime = md.uploadTime || md.createTime
-  if (faultTime) {
-    const diff = Date.now() - new Date(faultTime).getTime()
-    elapsedHours = Math.max(0, Math.round(diff / (1000 * 60 * 60) * 10) / 10)
+// 后端返回小时:满 24 小时折算成天,否则展示小时;无有效样本显示占位符
+function formatDuration(hours) {
+  if (hours == null || hours === '') return '—'
+  const value = Number(hours)
+  if (!Number.isFinite(value) || value < 0) return '—'
+  if (value >= 24) {
+    const days = value / 24
+    return `${Number.isInteger(days) ? days : days.toFixed(1)}天`
   }
+  return `${value.toFixed(1)}小时`
+}
 
-  return {
-    id: raw?.equipmentId || raw?.equipmentCode || '',
-    code: raw?.equipmentCode || raw?.equipmentId || '',
-    equipmentId: raw?.equipmentId || '',
-    name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
-    typeId,
-    deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
-    location: raw?.equipmentLocation || '暂无位置信息',
-    locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
-    district: districtLabel(raw?.district),
-    battery, signal, temperature, tiltAngle, angleThreshold,
-    status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal, offline },
-    faultTypes, hasAlert,
-    faultDesc: parts.join(';') || '设备状态正常',
-    abnormalTime: faultTime || '',
-    elapsedHours,
-    workOrderSubmitted: !!raw?.workOrderSubmitted
+function fallbackTrend() {
+  const result = []
+  const today = new Date()
+  for (let i = 6; i >= 0; i--) {
+    const day = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i)
+    const mm = String(day.getMonth() + 1).padStart(2, '0')
+    const dd = String(day.getDate()).padStart(2, '0')
+    result.push({ label: `${mm}.${dd}`, count: 0 })
   }
+  return result
 }
 
+const pagedAlarms = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value
+  return abnormalAlarms.value.slice(start, start + pageSize.value)
+})
+
 // ==================== 数据加载 ====================
 async function loadAll() {
   loading.value = true
   try {
-    const today = new Date()
-    const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1)
-
-    // 日期范围:默认近6个月,后端统一计算全部统计数据
-    const startDate = dateRange.value?.[0] ? formatDate(new Date(dateRange.value[0])) : formatDate(sixMonthsAgo)
-    const endDate = dateRange.value?.[1] ? formatDate(new Date(dateRange.value[1])) : formatDate(today)
-
-    const { data } = await getManholeMaintenanceDashboard({ trendType: 2, startDate, endDate })
+    const { data } = await getAlarmDashboard()
+    const d = data || {}
+    kpi.todayCount = d.todayCount ?? 0
+    kpi.yesterdayCount = d.yesterdayCount ?? 0
+    kpi.todayDiff = d.todayDiff ?? 0
+    kpi.transferredCount = d.transferredCount ?? 0
+    kpi.untransferredCount = d.untransferredCount ?? 0
+    kpi.disposalRate = d.disposalRate ?? 0
+    kpi.avgResponseHours = d.avgResponseHours ?? null
+    kpi.avgDisposalHours = d.avgDisposalHours ?? null
+
+    levels.value = Array.isArray(d.levels) && d.levels.length
+      ? d.levels.map(item => ({ ...item }))
+      : FALLBACK_LEVELS.map(item => ({ ...item }))
+    trend.value = Array.isArray(d.trend) && d.trend.length ? d.trend : fallbackTrend()
+    abnormalAlarms.value = Array.isArray(d.abnormalAlarms) ? d.abnormalAlarms : []
+    currentPage.value = 1
 
-    // 1. KPI 指标
-    const stat = data?.kpi || {}
-    kpi.total = stat.totalTerminal || 0
-    kpi.abnormal = stat.abnormalTerminal || 0
-    kpi.abnormalRate = stat.abnormalRate ?? '0.0'
-    kpi.submittedCount = stat.submittedCount || 0
-    kpi.solvedRate = stat.solvedRate ?? 0
-    kpi.totalCase = stat.totalCase || 0
-    kpi.disposalRate = stat.disposalRate ?? 0
-    kpi.delayRate = stat.delayRate ?? 0
-    kpi.avgResponseTime = stat.avgDisposalTime ?? '—'
-
-    // 2. 异常终端列表
-    abnormalDevices.value = (data?.abnormalDevices || []).map(normalizeDevice)
-
-    // 3. 渲染图表
     await nextTick()
-    renderTrendChart(data?.abnormalTerminalTrend || [])
-    renderProblemTypeChart(data?.problemTypeDistribution || [])
-    renderDisposalTrendChart(data?.caseRateTrend || [])
-    renderRegionRankChart(data?.regionRank || [])
+    renderTrendChart()
   } catch (error) {
     const msg = error?.message || error?.msg || '未知错误'
     ElMessage.error('统计数据加载失败: ' + msg)
@@ -366,142 +201,52 @@ async function loadAll() {
   }
 }
 
-// ==================== ECharts 渲染 ====================
-function renderTrendChart(trendData) {
-  if (!trendChartRef.value) return
-  if (!trendChart) trendChart = echarts.init(trendChartRef.value)
-
-  const labels = (trendData || []).map(d => d.trendLabel || '')
-  const abnormalValues = (trendData || []).map(d => d.abnormalTerminal || 0)
-  const normalValues = (trendData || []).map(d => {
-    const abnormal = d.abnormalTerminal || 0
-    const total = d.totalTerminal || abnormal
-    return Math.max(0, total - abnormal)
-  })
+// ==================== 近 7 日趋势图 ====================
+function renderTrendChart() {
+  if (!trendRef.value) return
+  if (!trendChart) trendChart = echarts.init(trendRef.value)
+  const rows = trend.value.length ? trend.value : fallbackTrend()
 
   trendChart.setOption({
-    tooltip: { trigger: 'axis' },
-    legend: { data: ['异常终端数', '正常终端数'] },
-    grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
-    xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
-    yAxis: { type: 'value', name: '数量' },
-    series: [
-      { name: '异常终端数', type: 'line', data: abnormalValues, smooth: true, lineStyle: { color: '#f56c6c' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#f56c6c' } },
-      { name: '正常终端数', type: 'line', data: normalValues, smooth: true, lineStyle: { color: '#67c23a' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#67c23a' } }
-    ]
-  })
-}
-
-function renderProblemTypeChart(distribution) {
-  if (!problemTypeChartRef.value) return
-  if (!problemTypeChart) problemTypeChart = echarts.init(problemTypeChartRef.value)
-
-  const colors = ['#f56c6c', '#e6a23c', '#409eff', '#909399', '#67c23a', '#9966ff']
-  const data = (distribution || []).map((item, i) => ({
-    name: item.name || '其他',
-    value: item.value || 0,
-    itemStyle: { color: colors[i % colors.length] }
-  }))
-
-  problemTypeChart.setOption({
-    tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
-    legend: { orient: 'vertical', left: 'left', textStyle: { fontSize: 11 } },
-    series: [{
-      type: 'pie', radius: '55%', center: ['55%', '50%'],
-      data: data.length ? data : [{ name: '暂无数据', value: 0 }],
-      label: { show: true, formatter: '{b}: {d}%' }
-    }]
-  })
-}
-
-function renderDisposalTrendChart(trendData) {
-  if (!disposalTrendChartRef.value) return
-  if (!disposalTrendChart) disposalTrendChart = echarts.init(disposalTrendChartRef.value)
-
-  const labels = (trendData || []).map(d => d.trendLabel || '')
-  const disposalRates = (trendData || []).map(d => Number(d.caseDisposalRate) || 0)
-  const delayRates = (trendData || []).map(d => Number(d.caseDelayRate) || 0)
-
-  disposalTrendChart.setOption({
-    tooltip: { trigger: 'axis' },
-    legend: { data: ['处置率(%)', '延期率(%)'] },
-    grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
-    xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
-    yAxis: { type: 'value', name: '百分比(%)', max: 100 },
-    series: [
-      { name: '处置率(%)', type: 'line', data: disposalRates, smooth: true, itemStyle: { color: '#67c23a' } },
-      { name: '延期率(%)', type: 'line', data: delayRates, smooth: true, itemStyle: { color: '#e6a23c' } }
-    ]
-  })
-}
-
-function renderRegionRankChart(regionRank) {
-  if (!regionRankChartRef.value) return
-  if (!regionRankChart) regionRankChart = echarts.init(regionRankChartRef.value)
-
-  const entries = (regionRank || []).slice(0, 8)
-
-  regionRankChart.setOption({
-    tooltip: {
-      trigger: 'axis', axisPointer: { type: 'shadow' },
-      formatter: (params) => {
-        const p = params[0]
-        const item = entries[p.dataIndex]
-        return item ? `${p.name}: 异常 ${p.value} / 总数 ${item.total}` : p.name
-      }
+    backgroundColor: 'transparent',
+    grid: { left: 44, right: 24, top: 24, bottom: 32 },
+    tooltip: { trigger: 'axis', backgroundColor: '#ffffff', borderColor: '#dce3ed', textStyle: { color: '#303133' } },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: rows.map(item => item.label),
+      axisLine: { lineStyle: { color: '#c9d4e2' } },
+      axisTick: { show: false },
+      axisLabel: { color: '#7b8a9e', fontSize: 11 }
+    },
+    yAxis: {
+      type: 'value',
+      min: 0,
+      minInterval: 1,
+      splitLine: { lineStyle: { color: '#e8edf4', type: 'dashed' } },
+      axisLabel: { color: '#7b8a9e', fontSize: 11 }
     },
-    grid: { left: '15%', right: '8%', bottom: '8%', top: '8%' },
-    xAxis: { type: 'value', name: '异常数量' },
-    yAxis: { type: 'category', data: entries.map(e => e.name), axisLabel: { fontSize: 11 } },
     series: [{
-      name: '异常数量', type: 'bar',
-      data: entries.map(e => e.abnormal),
-      itemStyle: { color: '#409eff', borderRadius: [0, 4, 4, 0] },
-      label: { show: true, position: 'right', fontSize: 11 }
+      name: '报警数',
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 7,
+      data: rows.map(item => Number(item.count) || 0),
+      lineStyle: { width: 2, color: '#f59a23' },
+      itemStyle: { color: '#f59a23', borderColor: '#ffffff', borderWidth: 2 },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(255,159,67,.32)' },
+          { offset: 1, color: 'rgba(255,159,67,.02)' }
+        ])
+      }
     }]
-  })
-}
-
-// ==================== 计算属性 ====================
-const filteredAbnormalDevices = computed(() => {
-  let list = abnormalDevices.value
-  if (searchKeyword.value) {
-    const kw = searchKeyword.value.toLowerCase()
-    list = list.filter(d =>
-      d.code.toLowerCase().includes(kw) ||
-      d.name.toLowerCase().includes(kw) ||
-      d.location.toLowerCase().includes(kw)
-    )
-  }
-  if (faultTypeFilter.value) {
-    list = list.filter(d => d.faultTypes.some(ft => ft.type === faultTypeFilter.value))
-  }
-  return list
-})
-
-const paginatedAbnormalDevices = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value
-  return filteredAbnormalDevices.value.slice(start, start + pageSize.value)
-})
-
-// ==================== 辅助函数 ====================
-const getFaultTagType = (type) => {
-  const map = { tilt: 'danger', waterLevel: 'danger', waterInfiltration: 'danger', offline: 'info', battery: 'warning', signal: 'warning' }
-  return map[type] || 'info'
-}
-
-const viewDetail = (device) => {
-  currentDevice.value = device
-  detailDrawerVisible.value = true
+  }, true)
 }
 
 // ==================== 生命周期 ====================
-const handleResize = () => {
-  trendChart && trendChart.resize()
-  problemTypeChart && problemTypeChart.resize()
-  disposalTrendChart && disposalTrendChart.resize()
-  regionRankChart && regionRankChart.resize()
-}
+const handleResize = () => { trendChart && trendChart.resize() }
 
 onMounted(async () => {
   await nextTick()
@@ -512,120 +257,143 @@ onMounted(async () => {
 onBeforeUnmount(() => {
   window.removeEventListener('resize', handleResize)
   trendChart && trendChart.dispose()
-  problemTypeChart && problemTypeChart.dispose()
-  disposalTrendChart && disposalTrendChart.dispose()
-  regionRankChart && regionRankChart.dispose()
+  trendChart = null
 })
 </script>
 
 <style scoped>
-.terminal-operation-dashboard {
-  background: #f0f2f6;
+.alarm-dashboard {
   min-height: 100vh;
-  padding: 20px;
+  padding: 18px 22px 28px;
+  background: #f0f2f6;
+  color: #303133;
+  box-sizing: border-box;
 }
+
 .page-header {
-  background: white;
-  border-radius: 16px;
-  padding: 16px 24px;
-  margin-bottom: 20px;
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
+  justify-content: space-between;
+  margin-bottom: 16px;
 }
-.header-left { display: flex; align-items: center; gap: 12px; }
-.header-left .title { font-size: 20px; font-weight: 600; }
-.header-right { display: flex; gap: 12px; flex-wrap: wrap; }
+.header-left { display: flex; align-items: center; gap: 10px; color: #409eff; }
+.header-left .title { font-size: 18px; font-weight: 600; letter-spacing: 1px; color: #1f2d3d; }
 
-.kpi-cards {
+.kpi-grid {
   display: grid;
-  grid-template-columns: repeat(6, 1fr);
-  gap: 16px;
-  margin-bottom: 20px;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 14px;
+  margin-bottom: 14px;
 }
 .kpi-card {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
-  text-align: center;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+  position: relative;
+  padding: 14px 18px 13px;
+  border: 1px solid #e4e9f2;
+  border-radius: 8px;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
+  overflow: hidden;
 }
-.kpi-value { font-size: 32px; font-weight: bold; color: #1f2d3d; }
-.kpi-label { font-size: 13px; color: #909399; margin-top: 6px; }
-.kpi-trend { font-size: 11px; color: #67c23a; margin-top: 4px; }
-.kpi-trend.trend-down { color: #f56c6c; }
-.kpi-card.warning .kpi-value { color: #e6a23c; }
-.kpi-card.success .kpi-value { color: #67c23a; }
-
-.charts-row {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 20px;
-  margin-bottom: 20px;
+.kpi-card::before {
+  content: '';
+  position: absolute;
+  left: 0; top: 0; bottom: 0;
+  width: 3px;
+  background: linear-gradient(180deg, #7ec8ff, #2b7de9);
 }
-.chart-card {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
+.kpi-head { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #6b7a90; }
+.kpi-icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 20px; height: 20px;
+  border-radius: 4px;
+  background: #eaf3ff;
+  color: #409eff;
+  font-size: 13px;
+}
+.kpi-value {
+  margin-top: 8px;
+  font-size: 30px;
+  font-weight: 700;
+  line-height: 1.15;
+  color: #1f2d3d;
+  font-variant-numeric: tabular-nums;
 }
-.chart-header {
+.kpi-value small { font-size: 16px; margin-left: 2px; font-weight: 600; }
+.kpi-value.green { color: #18a058; }
+.kpi-value.orange { color: #e8890c; }
+.kpi-foot { margin-top: 5px; font-size: 11px; color: #909399; }
+.kpi-foot .up { color: #f56c6c; font-weight: 600; }
+.kpi-foot .down { color: #18a058; font-weight: 600; }
+.kpi-foot .flat { color: #909399; }
+
+.section-title {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  margin-bottom: 16px;
+  gap: 9px;
+  margin: 20px 0 12px;
+  font-size: 14px;
   font-weight: 600;
+  color: #303133;
 }
-.chart-container { height: 280px; }
+.title-bar {
+  width: 3px; height: 14px;
+  border-radius: 2px;
+  background: linear-gradient(180deg, #7ec8ff, #2b7de9);
+}
+.title-count { font-size: 12px; font-weight: 400; color: #909399; }
 
-.device-list-container {
-  background: white;
-  border-radius: 16px;
-  padding: 16px;
+.level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
+.level-card {
+  padding: 16px 10px 14px;
+  text-align: center;
+  border: 1px solid #e4e9f2;
+  border-radius: 8px;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
 }
-.list-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
+.level-value { font-size: 28px; font-weight: 700; line-height: 1.2; color: #1f2d3d; font-variant-numeric: tabular-nums; }
+.level-name { margin-top: 6px; font-size: 12px; color: #6b7a90; }
+.level-1 { border-color: #f7c4c4; }
+.level-1 .level-value { color: #f04c4c; }
+.level-2 { border-color: #f5d9b0; }
+.level-2 .level-value { color: #e8890c; }
+.level-3 { border-color: #ede0a8; }
+.level-3 .level-value { color: #c9a227; }
+.level-4 { border-color: #b9e2ee; }
+.level-4 .level-value { color: #1a9cb5; }
+
+.trend-chart {
+  height: 210px;
+  border: 1px solid #e4e9f2;
+  border-radius: 8px;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
+}
+
+.alarm-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
+}
+.alarm-table :deep(th.el-table__cell) {
+  background: #f5f7fa;
+  color: #606266;
   font-weight: 600;
 }
-.list-filters { display: flex; gap: 12px; align-items: center; }
+
 .pagination-wrapper {
   display: flex;
   justify-content: flex-end;
-  margin-top: 16px;
+  margin-top: 12px;
   padding-top: 12px;
-  border-top: 1px solid #e9ecef;
-}
-.device-detail { padding: 8px; }
-.sub-title { font-weight: 600; margin: 16px 0 12px 0; }
-.over-time { color: #f56c6c; font-weight: 600; }
-
-.monitor-grid {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 10px;
-}
-.monitor-item {
-  background: #f5f7fa;
-  border-radius: 8px;
-  padding: 10px;
-  text-align: center;
-  border: 1px solid #e9ecef;
-}
-.monitor-item.alert {
-  background: #fef0f0;
-  border-color: #f56c6c;
+  border-top: 1px solid #ebeef5;
 }
-.m-label { font-size: 12px; color: #909399; }
-.m-value { font-size: 16px; font-weight: 600; color: #303133; margin: 4px 0; }
-.monitor-item.alert .m-value { color: #f56c6c; }
-.m-extra { font-size: 11px; color: #aaa; }
 
-@media (max-width: 1200px) {
-  .kpi-cards { grid-template-columns: repeat(3, 1fr); }
-  .charts-row { grid-template-columns: 1fr; }
+@media (max-width: 1100px) {
+  .kpi-grid { grid-template-columns: repeat(2, 1fr); }
+  .level-grid { grid-template-columns: repeat(2, 1fr); }
 }
 </style>