Przeglądaj źródła

重构-窨井子系统-首页统计

zengy 1 tydzień temu
rodzic
commit
71078b62ee
1 zmienionych plików z 733 dodań i 736 usunięć
  1. 733 736
      src/views/subSystem/manholeCover/home/index.vue

+ 733 - 736
src/views/subSystem/manholeCover/home/index.vue

@@ -1,198 +1,540 @@
-<!--窨井盖监测首页大屏-->
+<!--窨井盖监测首页大屏:中间地图按 equipmentGIS.vue 的 GIS 一张图规范实现-->
 <template>
   <div class="monitor-container">
-    <!-- 左侧栏 -->
-    <div class="left-col">
-      <!-- 待办工单 -->
-      <div class="panel">
-        <h3 class="panel-title">待办工单 <span class="title-count">{{ workOrders.length }} 条</span></h3>
-        <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.work">
-          <el-table-column prop="orderNo" label="工单编号" min-width="120" show-overflow-tooltip />
-          <el-table-column prop="deviceCode" label="设备" min-width="100" show-overflow-tooltip />
-          <el-table-column label="优先级" width="70">
-            <template #default="{ row }">
-              <span :class="['priority-tag', priorityClass(row.orderLevel)]">{{ priorityText(row.orderLevel) }}</span>
-            </template>
-          </el-table-column>
-        </el-table>
+    <!-- 顶部:标题 + 设备状态总览(全域健康度带) -->
+    <header class="top-bar">
+      <div class="brand">
+        <div class="brand-title">窨井盖监测中心</div>
       </div>
 
-      <!-- 告警类型统计(15天内) -->
-      <div class="panel">
-        <h3 class="panel-title">
-          告警类型统计(15天内)
-          <span class="title-count">{{ alarmRangeText }}</span>
-        </h3>
-        <div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
+      <div class="health-band">
+        <div class="hb-rate">
+          <b :class="{ low: onlineRate < 60 }">{{ onlineRate }}%</b>
+          <span>设备在线率</span>
+        </div>
+        <div class="hb-bar">
+          <i class="on" :style="{ width: onlineRate + '%' }"></i>
+          <i class="off" :style="{ width: offlineRate + '%' }"></i>
+          <i class="al" :style="{ width: alarmRate + '%' }"></i>
+        </div>
+        <div class="hb-items">
+          <div><b>{{ stats.total }}</b><span>设备总数</span></div>
+          <div><b class="text-green">{{ stats.online }}</b><span>在线</span></div>
+          <div><b class="text-gray">{{ stats.offline }}</b><span>离线</span></div>
+          <div><b class="text-red">{{ stats.alarm }}</b><span>报警</span></div>
+        </div>
       </div>
-
-      <!-- 设备区域信息 -->
-      <div class="panel">
-        <h3 class="panel-title">设备区域信息</h3>
-        <div ref="areaChartRef" class="chart" v-loading="loading.device" />
+    </header>
+
+    <!-- 主体三列 -->
+    <div class="main-grid">
+      <!-- 左:实时报警 + 报警类型分布 -->
+      <div class="col left-col">
+        <section class="panel alarm-panel">
+          <h3 class="panel-title">
+            实时报警
+            <span class="badge" :class="{ zero: !activeAlarms.length }">{{ activeAlarms.length }}</span>
+          </h3>
+          <div class="alarm-list" v-loading="loading.alarm">
+            <div v-for="a in activeAlarms" :key="a.id || a.alarmNo" class="alarm-item">
+              <div class="alarm-top">
+                <span class="alarm-dev">
+                  {{ a.deviceName || a.deviceCode || '未知设备' }}
+                  <span v-if="a.deviceCode" class="alarm-code">{{ a.deviceCode }}</span>
+                </span>
+                <span class="alarm-level" :style="{ color: levelColor(a.alarmLevel) }">{{ levelName(a.alarmLevel) }}</span>
+              </div>
+              <div class="alarm-time">
+                <span v-if="a.alarmNo" class="alarm-no">{{ a.alarmNo }}</span>
+                <span>{{ a.createTime || '—' }}</span>
+              </div>
+              <div class="alarm-desc">
+                {{ a.alarmContent || a.warningCode || '—' }}
+                <span v-if="a.actualValue != null">实测 {{ a.actualValue }}</span>
+              </div>
+            </div>
+            <div v-if="!activeAlarms.length" class="empty">当前无未处理报警</div>
+          </div>
+        </section>
+
+        <section class="panel dist-panel">
+          <h3 class="panel-title">
+            报警类型分布
+            <span class="title-count">近 {{ ALARM_RANGE_DAYS }} 天 · {{ recentAlarms.length }} 条</span>
+          </h3>
+          <div class="type-dist" v-loading="loading.alarm">
+            <div v-for="t in alarmTypeDist" :key="t.name" class="td-row">
+              <span class="td-name" :title="t.name">{{ t.name }}</span>
+              <div class="td-track"><i :style="{ width: t.rate + '%' }"></i></div>
+              <span class="td-num">{{ t.count }} 条</span>
+            </div>
+            <div v-if="!alarmTypeDist.length" class="empty">近 {{ ALARM_RANGE_DAYS }} 天暂无报警</div>
+          </div>
+        </section>
       </div>
-    </div>
 
-    <!-- 中间地图区域 -->
-    <div class="center-col">
-      <div class="panel map-panel">
-        <div class="map-header">
-          <span>窨井设备分布</span>
-          <div class="map-legend">
-            <span><i class="legend-dot online"></i> 在线 {{ stats.online }}</span>
-            <span><i class="legend-dot alert"></i> 告警 {{ stats.alarm }}</span>
-            <span><i class="legend-dot offline"></i> 离线 {{ stats.offline }}</span>
+      <!-- 中:监测设备 GIS 一张图(与 equipmentGIS.vue 同规范) -->
+      <div class="col center-col">
+        <section class="panel map-panel">
+          <div class="map-header">
+            <span>窨井设备分布</span>
+            <el-button :icon="Refresh" size="small" :loading="gisLoading" @click="loadGisData">刷新</el-button>
           </div>
-        </div>
-        <div id="homeMap" class="bmap-container"></div>
+          <div class="map-wrap">
+            <GisMapCanvas
+              ref="gisMapRef"
+              :points="mapPoints"
+              cluster-label="设备集合"
+              :center="mapCenter"
+              :zoom="15"
+              @point-click="openDetail($event.equipmentId)"
+              @error="mapError = $event"
+            />
+            <div class="map-legend" @mousedown.stop @click.stop @dblclick.stop>
+              <div class="legend-title">点击切换显示</div>
+              <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.online }" @click="toggleLegend('online')">
+                <i class="legend-dot online"></i>在线
+              </button>
+              <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.offline }" @click="toggleLegend('offline')">
+                <i class="legend-dot offline"></i>离线
+              </button>
+              <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.alarm }" @click="toggleLegend('alarm')">
+                <i class="legend-dot alarm"></i>报警(在线/离线时叠加)
+              </button>
+            </div>
+            <el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError">
+              <el-button link type="primary" @click="loadGisData">重试</el-button>
+            </el-alert>
+            <el-empty
+              v-if="!gisLoading && !mapError && !mapPoints.length"
+              class="map-empty"
+              description="当前没有可显示的监测设备"
+            />
+          </div>
+        </section>
       </div>
 
-      <!-- 设备分类 -->
-      <div class="panel chart-panel">
-        <h3 class="panel-title">设备分类</h3>
-        <div ref="deviceTypeChartRef" class="chart" v-loading="loading.device" />
+      <!-- 右:待办工单 + 设备状态总览(片区点阵) -->
+      <div class="col right-col">
+        <section class="panel work-panel">
+          <h3 class="panel-title">待办工单 <span class="title-count">{{ workOrders.length }} 条</span></h3>
+          <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.work">
+            <el-table-column prop="orderNo" label="工单编号" min-width="110" show-overflow-tooltip />
+            <el-table-column prop="orderDesc" label="工单描述" min-width="150" show-overflow-tooltip :formatter="(row) => row.orderDesc || '-'" />
+            <el-table-column prop="deviceName" label="设备" min-width="110" show-overflow-tooltip :formatter="(row) => row.deviceName || row.deviceCode || '-'" />
+            <el-table-column label="优先级" width="70">
+              <template #default="{ row }">
+                <span :class="['priority-tag', priorityClass(row.orderLevel)]">{{ priorityText(row.orderLevel) }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </section>
+
+        <section class="panel area-panel">
+          <h3 class="panel-title">
+            设备状态总览
+            <span class="title-count">按片区 · {{ areaRank.length }} 个片区</span>
+          </h3>
+          <div class="area-cards" v-loading="loading.device">
+            <div v-for="a in areaRank" :key="a.district" class="acard" :class="{ 'has-alarm': a.alarm > 0 }">
+              <div class="ac-head">
+                <div>
+                  <div class="ac-name">{{ a.district }}</div>
+                  <div class="ac-sub">共 {{ a.total }} 台设备</div>
+                </div>
+                <div class="ac-rate">
+                  <b :class="{ 'text-red': a.alarm > 0 }">{{ areaOnlineRate(a) }}%</b>
+                  <span>在线率</span>
+                </div>
+              </div>
+              <div class="ac-bar">
+                <i class="on" :style="{ width: areaOnlineRate(a) + '%' }"></i>
+                <i class="off" :style="{ width: areaOfflineRate(a) + '%' }"></i>
+                <i class="al" :style="{ width: areaAlarmRate(a) + '%' }"></i>
+              </div>
+              <div class="dot-matrix">
+                <div
+                  v-for="d in devicesByDistrict(a.district)"
+                  :key="d.id || d.code"
+                  class="dot"
+                  :class="dotClass(d)"
+                  :title="`${d.name}(${d.code})· ${statusText(d.status)}`"
+                ></div>
+                <span v-if="!devicesByDistrict(a.district).length" class="ac-tip">暂无设备</span>
+              </div>
+              <div class="ac-metrics">
+                <span>在线 <b>{{ a.online }}</b></span>
+                <span>离线 <b>{{ a.offline }}</b></span>
+                <span>报警 <b class="text-red">{{ a.alarm }}</b></span>
+              </div>
+            </div>
+          </div>
+        </section>
       </div>
     </div>
 
-    <!-- 右侧栏 -->
-    <div class="right-col">
-      <!-- 设备列表 -->
-      <div class="panel">
-        <h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
-        <div class="filter-bar">
-          <el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
-            <el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-        </div>
-        <el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
-          <el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
-          <el-table-column label="类型" width="70">
-            <template #default="{ row }">{{ typeText(row.typeId, row.typeName) }}</template>
-          </el-table-column>
-          <el-table-column label="电量" width="60">
-            <template #default="{ row }">
-              <el-tag :type="batteryTag(row.battery)" size="small">{{ row.battery ?? '-' }}%</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="状态" width="60">
-            <template #default="{ row }">
-              <span :class="['status-dot', row.status]"></span>
-              <span class="status-text">{{ statusText(row.status) }}</span>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
+    <!-- 设备详情(与 equipmentGIS.vue 一致) -->
+    <el-dialog title="设备详情" v-model="detailVisible" width="800px" append-to-body class="detail-dialog">
+      <el-alert v-if="detailError" type="error" show-icon :closable="false" title="设备详情加载失败,请关闭后重试" />
+      <div v-loading="detailLoading">
+        <template v-if="selectedDetail?.equipmentInfo">
+          <div class="dlg-summary">
+            <div class="summary-name">{{ selectedDetail.equipmentInfo.equipmentName || '未命名设备' }}</div>
+            <div class="summary-code">{{ selectedDetail.equipmentInfo.equipmentCode }}</div>
+            <span class="summary-tag">{{ selectedDetail.typeInfo?.typeName || '未分类' }}</span>
+            <div class="status-group">
+              <span class="st-badge" :class="selectedDetail.statusInfo?.onlineStatus === 1 ? 'online' : 'offline'">
+                <span class="badge-dot"></span>
+                <span class="badge-text">{{ selectedDetail.statusInfo?.onlineStatus === 1 ? '在线' : '离线' }}</span>
+              </span>
+              <span v-if="selectedDetail.statusInfo?.alarmStatus === 1" class="st-badge alarm">
+                <el-icon class="badge-icon"><WarningFilled /></el-icon>
+                <span class="badge-text">报警</span>
+              </span>
+            </div>
+          </div>
 
-      <!-- 动态信息 -->
-      <div class="panel">
-        <h3 class="panel-title">动态信息</h3>
-        <el-table :data="dynamicLogs" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.alarm">
-          <el-table-column prop="time" label="时间" width="140" />
-          <el-table-column prop="event" label="事件" min-width="120" show-overflow-tooltip />
-          <el-table-column label="状态" width="70">
-            <template #default="{ row }">
-              <span :class="['status-tag', row.alarmStatus === 1 ? 'done' : 'processing']">{{ row.alarmStatus === 1 ? '已处理' : '未处理' }}</span>
-            </template>
-          </el-table-column>
-        </el-table>
+          <div class="form-section-title detail-section-title">基础信息</div>
+          <el-descriptions :column="2" border size="small" class="dlg-grid">
+            <el-descriptions-item label="设备编码">{{ selectedDetail.equipmentInfo.equipmentCode || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备名称">{{ selectedDetail.equipmentInfo.equipmentName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">{{ selectedDetail.typeInfo?.typeName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="所属片区">{{ districtLabel(selectedDetail.equipmentInfo.district) }}</el-descriptions-item>
+            <el-descriptions-item label="设备型号">{{ selectedDetail.equipmentInfo.equipmentModel || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="制造商">{{ selectedDetail.equipmentInfo.manufacturer || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="生产日期">{{ selectedDetail.equipmentInfo.productionDate || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="规格参数">{{ selectedDetail.equipmentInfo.equipmentSpec || '-' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div class="form-section-title detail-section-title">资产信息</div>
+          <el-descriptions :column="2" border size="small" class="dlg-grid">
+            <el-descriptions-item label="资产状态">{{ currentStatusMeta(selectedDetail.statusInfo?.currentStatus).text }}</el-descriptions-item>
+            <el-descriptions-item label="资产价值(元)">{{ selectedDetail.equipmentInfo.assetValue ?? '-' }}</el-descriptions-item>
+            <el-descriptions-item label="使用年限(年)">{{ selectedDetail.equipmentInfo.useLife ?? '-' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div class="form-section-title detail-section-title">部署运维</div>
+          <el-descriptions :column="2" border size="small" class="dlg-grid">
+            <el-descriptions-item label="安装位置" :span="2">{{ selectedDetail.equipmentInfo.equipmentLocation || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="经度">{{ selectedDetail.equipmentInfo.longitude ?? '-' }}</el-descriptions-item>
+            <el-descriptions-item label="纬度">{{ selectedDetail.equipmentInfo.latitude ?? '-' }}</el-descriptions-item>
+            <el-descriptions-item label="安装时间">{{ selectedDetail.equipmentInfo.installTime || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="维护人员">{{ selectedDetail.equipmentInfo.maintainer || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="联系电话" :span="2">{{ selectedDetail.equipmentInfo.maintainerPhone || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="备注" :span="2">{{ selectedDetail.equipmentInfo.remark || '-' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div v-if="selectedDetail.equipmentInfo.equipmentImage" class="detail-image">
+            <div class="field-label">设备图片</div>
+            <el-image
+              :src="resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)"
+              :preview-src-list="[resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)]"
+              preview-teleported
+              fit="cover"
+              class="field-media"
+            />
+          </div>
+        </template>
+        <el-empty v-else-if="!detailLoading" description="未选中设备" />
       </div>
-    </div>
+      <template #footer>
+        <div class="dlg-footer">
+          <el-button @click="detailVisible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
-import * as echarts from 'echarts'
-import { getManholeLayerData } from '@/api/pipeNetwork/basic'
+import { ref, reactive, onMounted, nextTick, computed } from 'vue'
+import { Refresh, WarningFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
+import { getManholeLayerData, getEquipmentGisPoints } from '@/api/pipeNetwork/basic'
+import { getEquipmentDetail } from '@/api/pipeNetwork/pdeviceInform'
 import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
 import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
 import { getAlarmDataPage } from '@/api/drainage'
-import { ElMessage } from 'element-plus'
-import locationIcon from '@/assets/images/location.png'
-// 台账存 WGS84(设备表单选点经 bd09ToWgs84 回填),百度地图必须转 BD-09 再画
-import { convertCoord } from '@/utils/coordTransform'
+import { gisPointStatusIcons } from '@/utils/gisMapColors'
+import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
 
 // ==================== 状态 ====================
 const loading = reactive({ work: false, alarm: false, device: false })
-const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
-
 const workOrders = ref([])
 const allDevices = ref([])
-const dynamicLogs = ref([])
-const deviceTypeFilter = ref('')
-const alarmRangeText = ref(formatAlarmRange())
-
-/** 告警统计区间文案,空数据时也展示,避免面板看不出统计范围 */
-function formatAlarmRange() {
-  const end = new Date()
-  const start = new Date(end.getTime() - ALARM_RANGE_DAYS * 24 * 3600 * 1000)
-  const label = date => `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`
-  return `${label(start)} ~ ${label(end)}`
-}
-
-// 图表引用
-const alarmChartRef = ref(null)
-const areaChartRef = ref(null)
-const deviceTypeChartRef = ref(null)
-let alarmChart = null, areaChart = null, deviceTypeChart = null
+const recentAlarms = ref([])
 
 // ==================== 常量映射 ====================
-/** 历史遗留窨井类型编码,仅用于后端未返回 typeName 时兜底展示 */
-const LEGACY_TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
-
-// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
+/** 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常) */
 const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
-function districtLabel(code) {
-  return code == null ? '' : (DISTRICT_MAP[code] || String(code))
-}
-const tableHeaderStyle = { background: 'rgba(0,100,200,0.3)', color: '#fff', borderColor: '#2b4b8c' }
-
-const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
-const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
-/** 待办工单口径:待派单/待接单/已接单/处理中/延期中(枚举派生,见 workOrderDict.js) */
+/** 无设备数据时仍展示的区域维度,取自 DistrictEnum */
+const DEFAULT_DISTRICTS = ['城南', '城北', '太常']
 const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
 /** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
 const DEVICE_TYPE_PARENT_ID = '4'
-/** 窨井设备大类根ID(EquipmentTypeEnum.ROOT_MANHOLE_DEVICE),报警查询按大类隔离 */
+/** 窨井设备大类根ID(EquipmentTypeEnum.ROOT_MANHOLE_DEVICE) */
 const MANHOLE_ROOT_TYPE_ID = 4
 /** 报警查询后端限制 pageSize 不超过 100 */
 const ALARM_PAGE_SIZE = 100
 const ALARM_MAX_PAGES = 3
-/** 告警统计窗口(天) */
+/** 报警统计窗口(天) */
 const ALARM_RANGE_DAYS = 15
-/** 无设备数据时仍展示的区域维度,取自 DistrictEnum(0-城南 1-城北 2-太常) */
-const DEFAULT_DISTRICTS = ['城南', '城北', '太常']
-const typeText = (id, name) => name || LEGACY_TYPE_MAP[id] || (id ? `类型${id}` : '-')
+
+const tableHeaderStyle = { background: '#f7f9fc', color: '#5e6d82', borderColor: '#e3e9f2' }
+const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
+const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
 const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
-const batteryTag = (b) => {
-  if (b == null) return 'info'
-  if (Number(b) <= 20) return 'danger'
-  if (Number(b) <= 50) return 'warning'
-  return 'success'
+function districtLabel(code) {
+  return code == null ? '-' : (DISTRICT_MAP[code] ?? String(code))
+}
+/** 报警类型展示:优先报警内容,其次报警编码 */
+function alarmTypeText(record) {
+  return record.alarmContent || record.warningCode || '其他告警'
+}
+/** 报警等级配色/名称:与系统 alarm_level 字典一致(1红 2橙 3蓝 4绿) */
+function levelColor(level) {
+  return ({ 1: '#f56c6c', 2: '#e6a23c', 3: '#409eff', 4: '#67c23a' }[Number(level)] || '#9ba8c0')
+}
+function levelName(level) {
+  return ({ 1: '一级报警', 2: '二级报警', 3: '三级报警', 4: '四级报警' }[Number(level)] || (level != null ? level + '级' : '—'))
+}
+
+const pad2 = value => String(value).padStart(2, '0')
+function formatDateTime(date) {
+  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
 }
 
-// ==================== 筛选后设备列表 ====================
-/** 设备类型下拉项:跟着台账实际类型走,不再写死 5/9/10 */
-const deviceTypeOptions = computed(() => {
+// ==================== 顶部健康度带 ====================
+const stats = computed(() => {
+  const list = allDevices.value
+  return {
+    total: list.length,
+    online: list.filter(d => d.status === 'normal').length,
+    offline: list.filter(d => d.status === 'offline').length,
+    alarm: list.filter(d => d.status === 'alert').length
+  }
+})
+const onlineRate = computed(() => stats.value.total ? Math.round(stats.value.online / stats.value.total * 100) : 0)
+const offlineRate = computed(() => stats.value.total ? Math.round(stats.value.offline / stats.value.total * 100) : 0)
+const alarmRate = computed(() => stats.value.total ? Math.round(stats.value.alarm / stats.value.total * 100) : 0)
+
+// ==================== 片区设备总览(点阵) ====================
+function areaKey(d) {
+  return d.district || d.location || '未分配区域'
+}
+const areaRank = computed(() => {
   const map = new Map()
   allDevices.value.forEach(d => {
-    const value = String(d.typeId ?? '')
-    if (!value || map.has(value)) return
-    map.set(value, { value, label: typeText(d.typeId, d.typeName) })
+    const name = areaKey(d)
+    if (!map.has(name)) map.set(name, { district: name, total: 0, online: 0, offline: 0, alarm: 0 })
+    const a = map.get(name)
+    a.total += 1
+    if (d.status === 'normal') a.online += 1
+    else if (d.status === 'offline') a.offline += 1
+    else if (d.status === 'alert') a.alarm += 1
+  })
+  const list = Array.from(map.values()).sort((x, y) => y.total - x.total)
+  return list.length ? list : DEFAULT_DISTRICTS.map(name => ({ district: name, total: 0, online: 0, offline: 0, alarm: 0 }))
+})
+function devicesByDistrict(name) {
+  return allDevices.value.filter(d => areaKey(d) === name)
+}
+function dotClass(d) {
+  if (d.status === 'alert') return 'al'
+  return d.status === 'normal' ? 'on' : 'off'
+}
+function areaOnlineRate(a) { return a.total ? Math.round(a.online / a.total * 100) : 0 }
+function areaOfflineRate(a) { return a.total ? Math.round(a.offline / a.total * 100) : 0 }
+function areaAlarmRate(a) { return a.total ? Math.round(a.alarm / a.total * 100) : 0 }
+
+// ==================== 实时报警 / 报警类型分布 ====================
+/** 实时报警:近 N 天未处理(alarmStatus!=1)的报警,按时间倒序 */
+const activeAlarms = computed(() => recentAlarms.value
+  .filter(r => Number(r.alarmStatus) !== 1)
+  .slice()
+  .sort((a, b) => String(b.createTime || '').localeCompare(String(a.createTime || '')))
+  .slice(0, 20))
+/** 报警类型分布:按类型聚合条数,横条按最大值归一化 */
+const alarmTypeDist = computed(() => {
+  const map = new Map()
+  recentAlarms.value.forEach(r => {
+    const name = alarmTypeText(r)
+    map.set(name, (map.get(name) || 0) + 1)
   })
-  return Array.from(map.values())
+  const list = Array.from(map, ([name, count]) => ({ name, count }))
+    .sort((a, b) => b.count - a.count)
+    .slice(0, 8)
+  const max = Math.max(1, ...list.map(x => x.count))
+  return list.map(x => ({ ...x, rate: Math.round(x.count / max * 100) }))
 })
 
-const filteredDevices = computed(() => {
-  if (!deviceTypeFilter.value) return allDevices.value
-  return allDevices.value.filter(d => String(d.typeId) === deviceTypeFilter.value)
+// ==================== GIS 一张图(与 equipmentGIS.vue 同规范) ====================
+const mapCenter = { lng: 110.3965, lat: 28.4638 }
+const gisDevices = ref([])
+const gisLoading = ref(false)
+const mapError = ref('')
+const gisMapRef = ref(null)
+
+/** 标记图预加载:在线=绿、离线=灰,报警时在其右上角叠加红色标记 */
+const iconsReady = ref(false)
+const markerImages = {
+  green: typeof Image !== 'undefined' ? new Image() : null,
+  grey: typeof Image !== 'undefined' ? new Image() : null,
+  red: typeof Image !== 'undefined' ? new Image() : null
+}
+const iconCache = new Map()
+
+function loadMarkerImages() {
+  const entries = [
+    ['green', gisPointStatusIcons.normal],
+    ['grey', gisPointStatusIcons.offline],
+    ['red', gisPointStatusIcons.alarm]
+  ]
+  let pending = entries.length
+  if (!pending) { iconsReady.value = true; return }
+  const done = () => {
+    pending -= 1
+    if (pending <= 0) iconsReady.value = true
+  }
+  entries.forEach(([key, url]) => {
+    const img = markerImages[key]
+    if (!img) return done()
+    img.onload = done
+    img.onerror = done
+    img.src = url
+  })
+}
+
+function composeAlarmIcon(online) {
+  const base = online ? markerImages.green : markerImages.grey
+  const red = markerImages.red
+  if (!base || !red || !base.complete || !red.complete) {
+    return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
+  }
+  const size = 32
+  const canvas = document.createElement('canvas')
+  canvas.width = size
+  canvas.height = size
+  const ctx = canvas.getContext('2d')
+  ctx.drawImage(base, 0, 0, size, size)
+  const badge = Math.round(size * 0.5)
+  ctx.drawImage(red, size - badge + 2, 1, badge, badge)
+  return canvas.toDataURL()
+}
+
+function getDeviceIcon(device) {
+  const online = Number(device.onlineStatus) === 1
+  const alarm = Number(device.alarmStatus) === 1
+  if (!alarm) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
+  if (!iconsReady.value) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
+  const key = online ? 'online' : 'offline'
+  if (!iconCache.has(key)) iconCache.set(key, composeAlarmIcon(online))
+  return iconCache.get(key)
+}
+
+/** 图例状态筛选:隐藏后对应状态的设备在地图上不显示 */
+const legendVisible = reactive({ online: true, offline: true, alarm: true })
+function toggleLegend(key) {
+  legendVisible[key] = !legendVisible[key]
+}
+function isDeviceVisible(device) {
+  const online = Number(device.onlineStatus) === 1
+  const alarm = Number(device.alarmStatus) === 1
+  if (online && !legendVisible.online) return false
+  if (!online && !legendVisible.offline) return false
+  if (alarm && !legendVisible.alarm) return false
+  return true
+}
+
+const mapPoints = computed(() => {
+  // 依赖 iconsReady 与图例筛选状态,图片就绪或切换筛选后重算
+  void iconsReady.value
+  void legendVisible.online
+  void legendVisible.offline
+  void legendVisible.alarm
+  return gisDevices.value
+    .filter(isDeviceVisible)
+    .map(device => ({
+      ...device,
+      lng: device._longitude,
+      lat: device._latitude,
+      name: device.equipmentName || device.equipmentCode,
+      iconUrl: getDeviceIcon(device)
+    }))
 })
 
+/** 将 EquipmentBaseVO(统一设备台账)映射为地图点位设备对象 */
+function normalizeEquipment(row) {
+  return {
+    equipmentId: row.equipmentId,
+    equipmentCode: row.equipmentCode,
+    equipmentName: row.equipmentName,
+    equipmentTypeId: row.equipmentTypeId,
+    equipmentTypeName: row.equipmentTypeName,
+    equipmentImage: row.equipmentImage,
+    onlineStatus: row.onlineStatus,
+    alarmStatus: row.alarmStatus,
+    currentStatus: row.currentStatus,
+    _longitude: row.longitude != null ? Number(row.longitude) : null,
+    _latitude: row.latitude != null ? Number(row.latitude) : null
+  }
+}
+
+async function loadGisData() {
+  gisLoading.value = true
+  mapError.value = ''
+  try {
+    // rootTypeId=4 只取窨井设备,与 equipmentGIS 一致
+    const res = await getEquipmentGisPoints({ rootTypeId: MANHOLE_ROOT_TYPE_ID })
+    const list = res?.data ?? res
+    gisDevices.value = (Array.isArray(list) ? list : []).map(normalizeEquipment)
+    await nextTick()
+    gisMapRef.value?.fitBounds()
+  } catch (e) {
+    mapError.value = '监测设备数据加载失败,请点击右上角“刷新”重试'
+  } finally {
+    gisLoading.value = false
+  }
+}
+
+// ==================== 设备详情 ====================
+const detailVisible = ref(false)
+const detailLoading = ref(false)
+const detailError = ref('')
+const selectedDetail = ref(null)
+
+/** 资产状态:0-在用,1-维修,2-报废 */
+function currentStatusMeta(value) {
+  const map = {
+    0: { text: '在用', type: 'success' },
+    1: { text: '维修', type: 'warning' },
+    2: { text: '报废', type: 'danger' }
+  }
+  return map[value] || { text: '未知', type: 'info' }
+}
+function resolveImageUrl(url) {
+  return buildMinioAttachmentUrl(url)
+}
+async function openDetail(equipmentId) {
+  if (!equipmentId) return
+  detailVisible.value = true
+  detailLoading.value = true
+  selectedDetail.value = null
+  detailError.value = ''
+  try {
+    const detail = await getEquipmentDetail(equipmentId)
+    selectedDetail.value = detail?.data || detail
+  } catch (e) {
+    detailError.value = '设备详情加载失败,请关闭后重试'
+  } finally {
+    detailLoading.value = false
+  }
+}
+
 // ==================== 数据加载 ====================
 async function loadAll() {
-  await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData()])
-  await nextTick()
-  handleResize()
+  await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData(), loadGisData()])
 }
 
 async function loadDevices() {
@@ -212,27 +554,13 @@ async function loadDevices() {
       else if (onlineStatus === 1) status = 'normal'
       return {
         id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
-        typeId: d.equipmentTypeId, typeName: d.equipmentTypeName,
-        location: d.equipmentLocation, district: districtLabel(d.district),
-        lng: d.longitude != null ? Number(d.longitude) : null,
-        lat: d.latitude != null ? Number(d.latitude) : null,
-        battery: d.manholeData?.batteryLevel ?? null,
+        location: d.equipmentLocation,
+        district: d.district != null ? districtLabel(d.district) : '',
         status
       }
     })
-    const statistics = res.data?.statistics || {}
-    stats.total = statistics.total || allDevices.value.length
-    stats.online = statistics.online || 0
-    stats.offline = statistics.offline || 0
-    stats.alarm = statistics.alarm || 0
-    renderAreaChart()
-    renderDeviceTypeChart()
-    refreshMarkers()
   } catch (e) {
     ElMessage.error('设备数据加载失败')
-    // 加载失败也铺出空态骨架,面板不留白
-    renderAreaChart()
-    renderDeviceTypeChart()
   } finally {
     loading.device = false
   }
@@ -255,31 +583,20 @@ async function loadWorkOrders() {
   }
 }
 
-function formatDateTime(date) {
-  const pad = value => String(value).padStart(2, '0')
-  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
-}
-
-/** 报警类型展示:优先报警内容,其次报警编码 */
-function alarmTypeText(record) {
-  return record.alarmContent || record.warningCode || '其他告警'
-}
-
 /**
- * 拉取近 15 天的窨井报警
- *
- * rootTypeId 是报警查询的必填项(后端校验「设备大类不能为空」),
- * pageSize 上限 100,因此按页累加,最多取 3 页用于首页统计。
+ * 拉取近 N 天的窨井报警。
+ * rootTypeId 是报警查询必填项(后端校验「设备大类不能为空」),pageSize 上限 100,按页累加。
  */
 async function fetchRecentAlarms() {
   const params = {
     rootTypeId: MANHOLE_ROOT_TYPE_ID,
-    startTime: formatDateTime(new Date(Date.now() - 15 * 24 * 3600 * 1000))
+    startTime: formatDateTime(new Date(Date.now() - ALARM_RANGE_DAYS * 24 * 3600 * 1000))
   }
   const records = []
   for (let page = 1; page <= ALARM_MAX_PAGES; page++) {
     const res = await getAlarmDataPage(page, ALARM_PAGE_SIZE, params)
-    const pageRecords = res.rows || res.data?.records || res.data || []
+    const raw = res.rows || res.data?.records || res.data || []
+    const pageRecords = Array.isArray(raw) ? raw : []
     records.push(...pageRecords)
     if (pageRecords.length < ALARM_PAGE_SIZE) break
   }
@@ -288,607 +605,287 @@ async function fetchRecentAlarms() {
 
 async function loadAlarmData() {
   loading.alarm = true
-  alarmRangeText.value = formatAlarmRange()
   try {
     const records = await fetchRecentAlarms()
-    const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 3600 * 1000)
-    const recentAlarms = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= fifteenDaysAgo)
-
-    const typeMap = {}
-    recentAlarms.forEach(r => {
-      const type = alarmTypeText(r)
-      typeMap[type] = (typeMap[type] || 0) + 1
-    })
-    renderAlarmChart(typeMap)
-
-    // 动态信息(最新10条)
-    dynamicLogs.value = records.slice(0, 10).map(r => ({
-      time: r.createTime || '',
-      event: `${alarmTypeText(r)} - ${r.deviceName || r.deviceCode || ''}`,
-      alarmStatus: r.alarmStatus ?? 0
-    }))
+    const rangeStart = new Date(Date.now() - ALARM_RANGE_DAYS * 24 * 3600 * 1000)
+    recentAlarms.value = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= rangeStart)
   } catch (e) {
     console.error(e)
     ElMessage.error('告警数据加载失败')
-    // 加载失败也铺出空态骨架(含统计区间),面板不留白
-    renderAlarmChart({})
+    recentAlarms.value = []
   } finally {
     loading.alarm = false
   }
 }
 
-// ==================== ECharts ====================
-function renderAlarmChart(typeMap) {
-  if (!alarmChartRef.value) return
-  if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
-  const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
-  const data = Object.entries(typeMap || {}).map(([name, value], i) => ({
-    name, value, itemStyle: { color: colors[i % colors.length] }
-  }))
-  const hasData = data.length > 0
-  alarmChart.setOption({
-    backgroundColor: 'transparent',
-    // 无告警时保留统计区间与说明,避免面板看起来是空的
-    title: hasData ? { show: false } : {
-      text: '近 15 天暂无告警',
-      subtext: `统计区间 ${alarmRangeText.value}`,
-      left: 'center',
-      top: 'middle',
-      textStyle: { color: '#9ba8c0', fontSize: 12, fontWeight: 'normal' },
-      subtextStyle: { color: '#6f7d95', fontSize: 11 }
-    },
-    tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
-    legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 }, show: hasData },
-    // 空数据时不挂饼图系列,否则会残留一个灰色的空圆环
-    series: hasData ? [{
-      type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
-      label: { color: '#ccc', fontSize: 10 },
-      data
-    }] : []
-  }, true)
-}
-
-function renderAreaChart() {
-  if (!areaChartRef.value) return
-  if (!areaChart) areaChart = echarts.init(areaChartRef.value)
-  const areaMap = {}
-  allDevices.value.forEach(d => {
-    const area = d.district || d.location || '未分配区域'
-    // 取区域名(截取前4个字)
-    const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
-    areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
-  })
-  let entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
-  const hasData = entries.length > 0
-  // 无设备时仍按默认区域维度铺出骨架,面板不会空成一片
-  if (!hasData) {
-    entries = DEFAULT_DISTRICTS.map(name => [name, 0])
-  }
-  areaChart.setOption({
-    backgroundColor: 'transparent',
-    title: hasData ? { show: false } : {
-      text: '暂无设备,按默认区域维度展示',
-      left: 'center',
-      top: 0,
-      textStyle: { color: '#6f7d95', fontSize: 11, fontWeight: 'normal' }
-    },
-    tooltip: { trigger: 'axis' },
-    grid: { left: '12%', right: '8%', bottom: '18%', top: '18%' },
-    xAxis: {
-      type: 'category', data: entries.map(e => e[0]),
-      axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
-    },
-    yAxis: {
-      type: 'value', minInterval: 1,
-      axisLabel: { color: '#ccc' },
-      splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
-    },
-    series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' }, barWidth: '40%' }]
-  }, true)
-}
-
-function renderDeviceTypeChart() {
-  if (!deviceTypeChartRef.value) return
-  if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
-
-  // 按台账实际存在的设备类型聚合,避免把不存在的旧类型当成固定三分类
-  const countMap = new Map()
-  allDevices.value.forEach(d => {
-    const label = typeText(d.typeId, d.typeName)
-    countMap.set(label, (countMap.get(label) || 0) + 1)
-  })
-  // 有设备但类型名为空时,至少给出一个占位分类,面板不留白
-  if (!countMap.size && allDevices.value.length) {
-    countMap.set('未分类', allDevices.value.length)
-  }
-
-  const labels = Array.from(countMap.keys())
-  const values = Array.from(countMap.values())
-  const palette = ['#3399ff', '#67c23a', '#e6a23c', '#f56c6c', '#9b59b6']
-
-  deviceTypeChart.setOption({
-    backgroundColor: 'transparent',
-    tooltip: { trigger: 'axis' },
-    grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
-    title: labels.length ? undefined : {
-      text: '暂无窨井设备',
-      subtext: '设备台账中未查到窨井类设备',
-      left: 'center',
-      top: 'middle',
-      textStyle: { color: '#ccc', fontSize: 12, fontWeight: 'normal' },
-      subtextStyle: { color: '#888', fontSize: 11 }
-    },
-    xAxis: {
-      type: 'category', data: labels,
-      axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
-    },
-    yAxis: {
-      type: 'value', minInterval: 1,
-      axisLabel: { color: '#ccc' },
-      splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
-    },
-    series: labels.length ? [{
-      type: 'bar',
-      data: values.map((v, i) => ({ value: v, itemStyle: { color: palette[i % palette.length] } })),
-      barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
-    }] : []
-  }, true)
-}
-
-// ==================== 百度地图 ====================
-let mapInstance = null
-let mapFitted = false
-const markerMap = new Map()
-/** 兜底中心(沅陵县城),WGS84 口径,与设备台账一致 */
-const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
-
-function initMap() {
-  const container = document.getElementById('homeMap')
-  if (!container || typeof BMapGL === 'undefined') return
-  try {
-    mapInstance = new BMapGL.Map('homeMap')
-    const [centerLng, centerLat] = convertCoord(YUANLING_CENTER.lng, YUANLING_CENTER.lat)
-    mapInstance.centerAndZoom(new BMapGL.Point(centerLng, centerLat), 15)
-    mapInstance.enableScrollWheelZoom(true)
-  } catch (e) { console.error('地图初始化失败', e) }
-}
-
-/** 有设备时按设备范围取视野;点太近时限制放大层级,避免只剩一栋楼 */
-function fitMapToDevices(points) {
-  if (!mapInstance || !points.length) return
-  if (points.length === 1) {
-    mapInstance.centerAndZoom(points[0], 16)
-    return
-  }
-  const lngs = points.map(p => p.lng)
-  const lats = points.map(p => p.lat)
-  const span = Math.max(Math.max(...lngs) - Math.min(...lngs), Math.max(...lats) - Math.min(...lats))
-  if (span < 0.002) {
-    const mid = new BMapGL.Point((Math.max(...lngs) + Math.min(...lngs)) / 2, (Math.max(...lats) + Math.min(...lats)) / 2)
-    mapInstance.centerAndZoom(mid, 16)
-  } else {
-    mapInstance.setViewport(points, { margins: [50, 50, 50, 50] })
-  }
-}
-
-function getStatusColor(status) {
-  if (status === 'alert') return '#F56C6C'
-  if (status === 'offline') return '#909399'
-  return '#67C23A'
-}
-
-function refreshMarkers() {
-  if (!mapInstance) return
-  markerMap.forEach(label => mapInstance.removeOverlay(label))
-  markerMap.clear()
-  const devicePoints = []
-  allDevices.value.forEach(d => {
-    if (d.lng == null || d.lat == null) return
-    // 关键:台账是 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
-    const [bdLng, bdLat] = convertCoord(Number(d.lng), Number(d.lat))
-    const point = new BMapGL.Point(bdLng, bdLat)
-    devicePoints.push(point)
-    const color = getStatusColor(d.status)
-    const isAttention = d.status === 'alert' || d.status === 'offline'
-    const attentionText = d.status === 'alert' ? '告警' : '离线'
-    const html = isAttention
-      ? `<div class="home-map-attention-marker home-map-${d.status}" title="${attentionText}设备:${d.name}">
-          <div class="home-map-attention-badge">${attentionText} - ${d.name}</div>
-          <div class="home-map-attention-pin">
-            <i class="home-map-attention-ring"></i>
-            <img src="${locationIcon}" alt="" />
-          </div>
-        </div>`
-      : `<div style="text-align:center;cursor:pointer;">
-          <div style="color:#fff;background:${color};border-radius:3px;padding:1px 6px;font-size:10px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 3px rgba(0,0,0,0.3);">${d.name}</div>
-          <div><img src="${locationIcon}" alt="" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
-        </div>`
-    const label = new BMapGL.Label(html, {
-      position: point,
-      offset: new BMapGL.Size(isAttention ? -62 : -25, isAttention ? -68 : -40)
-    })
-    label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: isAttention ? '20' : '10' })
-    mapInstance.addOverlay(label)
-    markerMap.set(d.id, label)
-  })
-  // 只在首次拿到设备时校准视野,后续刷新不打断用户手动缩放
-  if (!mapFitted && devicePoints.length) {
-    fitMapToDevices(devicePoints)
-    mapFitted = true
-  }
-}
-
 // ==================== 生命周期 ====================
-const handleResize = () => {
-  alarmChart && alarmChart.resize()
-  areaChart && areaChart.resize()
-  deviceTypeChart && deviceTypeChart.resize()
-  mapInstance && mapInstance.resize && mapInstance.resize()
-}
-
-onMounted(async () => {
-  await nextTick()
-  initMap()
-  window.addEventListener('resize', handleResize)
+onMounted(() => {
+  loadMarkerImages()
   loadAll()
 })
-
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', handleResize)
-  alarmChart && alarmChart.dispose()
-  areaChart && areaChart.dispose()
-  deviceTypeChart && deviceTypeChart.dispose()
-  if (mapInstance) {
-    mapInstance.clearOverlays && mapInstance.clearOverlays()
-    mapInstance = null
-  }
-  markerMap.clear()
-})
 </script>
 
-<!-- ==================== 全局样式(非 scoped):el-table 深色主题覆盖 ==================== -->
-<!-- 用 .monitor-container 限定范围,不影响其他页面 -->
+<!-- ==================== 全局样式(非 scoped):el-table 浅色主题(与 realTimeShow.vue 一致) ==================== -->
 <style>
 .monitor-container .el-table {
-  --el-table-bg-color: transparent;
-  --el-table-tr-bg-color: transparent;
-  --el-table-header-bg-color: transparent;
-  --el-table-current-row-bg-color: rgba(0, 100, 200, 0.25);
-  --el-table-row-hover-bg-color: rgba(0, 100, 200, 0.15);
-  --el-table-text-color: #dce3ee;
-  --el-table-header-text-color: #fff;
-  --el-table-border-color: rgba(43, 75, 140, 0.35);
-  --el-table-border: 1px solid rgba(43, 75, 140, 0.35);
-  --el-fill-color-lighter: rgba(30, 40, 90, 0.25);
-  --el-fill-color-light: rgba(30, 40, 90, 0.25);
-  --el-fill-color: rgba(30, 40, 90, 0.25);
-  --el-fill-color-blank: transparent;
-  --el-text-color-regular: #dce3ee;
-  --el-text-color-secondary: #9ba8c0;
-  --el-border-color: rgba(43, 75, 140, 0.35);
-  --el-border-color-lighter: rgba(43, 75, 140, 0.2);
-  background: transparent !important;
+  --el-table-bg-color: #fff;
+  --el-table-tr-bg-color: #fff;
+  --el-table-header-bg-color: #f7f9fc;
+  --el-table-current-row-bg-color: #f4f9ff;
+  --el-table-row-hover-bg-color: #f8fbff;
+  --el-table-text-color: #1f2937;
+  --el-table-header-text-color: #5e6d82;
+  --el-table-border-color: #eef2f7;
+  --el-table-border: 1px solid #eef2f7;
+  --el-fill-color-lighter: #fff;
+  --el-fill-color-light: #f7f9fc;
+  --el-fill-color: #f7f9fc;
+  --el-fill-color-blank: #fff;
+  --el-text-color-regular: #1f2937;
+  --el-text-color-secondary: #97a3b7;
+  --el-border-color: #e3e9f2;
+  --el-border-color-lighter: #eef2f7;
+  background: #fff !important;
   border: none !important;
 }
-/* 隐藏 EP 表格自带的底部/右侧边框伪元素 */
 .monitor-container .el-table::before,
 .monitor-container .el-table::after,
 .monitor-container .el-table .el-table__inner-wrapper::before,
-.monitor-container .el-table .el-table__inner-wrapper::after {
-  display: none !important;
-}
-/* 内部容器全透明 */
+.monitor-container .el-table .el-table__inner-wrapper::after { display: none !important; }
 .monitor-container .el-table .el-table__inner-wrapper,
 .monitor-container .el-table .el-table__body-wrapper,
 .monitor-container .el-table .el-table__header-wrapper,
 .monitor-container .el-table .el-table__body,
-.monitor-container .el-table .el-table__header {
-  background: transparent !important;
-}
-/* 表头单元格 — EP 2.x 使用 th.el-table__cell */
+.monitor-container .el-table .el-table__header { background: #fff !important; }
 .monitor-container .el-table th.el-table__cell {
-  background: rgba(0, 100, 200, 0.35) !important;
-  color: #fff !important;
-  font-weight: 600;
-  border-bottom: 1px solid #2b4b8c !important;
+  background: #f7f9fc !important;
+  color: #5e6d82 !important; font-weight: 600;
+  border-bottom: 1px solid #eef2f7 !important;
 }
-/* 内容单元格 */
 .monitor-container .el-table td.el-table__cell {
-  background: transparent !important;
-  border-bottom: 1px solid rgba(43, 75, 140, 0.25) !important;
+  background: #fff !important;
+  border-bottom: 1px solid #f1f5f9 !important;
   padding: 7px 0 !important;
 }
-/* 行 */
-.monitor-container .el-table .el-table__row {
-  background: transparent !important;
-}
-.monitor-container .el-table .el-table__row:hover > td.el-table__cell {
-  background: rgba(0, 100, 200, 0.15) !important;
-}
-/* 空数据 */
-.monitor-container .el-table .el-table__empty-block {
-  background: transparent !important;
-}
-.monitor-container .el-table .el-table__empty-text {
-  color: #8090a8;
-}
-/* 滚动条 */
-.monitor-container .el-table .el-scrollbar__bar {
-  opacity: 0.4;
-}
-/* el-select 输入框深色适配 */
-.monitor-container .el-select .el-input__wrapper {
-  background: rgba(30, 40, 90, 0.6);
-  box-shadow: 0 0 0 1px rgba(43, 75, 140, 0.5) inset;
-}
-.monitor-container .el-select .el-input__inner {
-  color: #dce3ee;
-}
-/* el-table 在 flex 容器中撑满高度(无 max-height 时) */
-.monitor-container .el-table {
-  height: 100% !important;
-  display: flex;
-  flex-direction: column;
-}
-.monitor-container .el-table .el-table__inner-wrapper {
-  flex: 1;
-  min-height: 0;
-  display: flex;
-  flex-direction: column;
-}
-.monitor-container .el-table .el-table__body-wrapper {
-  flex: 1;
-  min-height: 0;
-}
-
-/* 告警和离线设备地图标记:通过扩散圈和上下位移动画强化定位提示 */
-.home-map-attention-marker {
-  position: relative;
-  width: 124px;
-  height: 70px;
-  cursor: pointer;
-  text-align: center;
-}
-.home-map-attention-badge {
-  position: relative;
-  z-index: 2;
-  display: inline-block;
-  max-width: 118px;
-  overflow: hidden;
-  padding: 2px 7px;
-  border: 1px solid rgba(255, 255, 255, 0.72);
-  border-radius: 4px;
-  color: #fff;
-  box-shadow: 0 2px 8px rgba(152, 23, 23, 0.55);
-  font-size: 10px;
-  line-height: 16px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.home-map-attention-pin {
-  position: absolute;
-  left: 50%;
-  top: 27px;
-  width: 28px;
-  height: 34px;
-  transform: translateX(-50%);
-  animation: homeMapAttentionBounce 1.1s ease-in-out infinite;
-}
-.home-map-attention-pin img {
-  position: relative;
-  z-index: 2;
-  display: block;
-  width: 28px;
-  height: 28px;
-}
-.home-map-attention-ring {
-  position: absolute;
-  z-index: 1;
-  left: 50%;
-  top: 12px;
-  width: 18px;
-  height: 18px;
-  border-radius: 50%;
-  transform: translate(-50%, -50%);
-  animation: homeMapAttentionPulse 1.5s ease-out infinite;
-}
-.home-map-alert .home-map-attention-badge { background: #e64545; }
-.home-map-alert .home-map-attention-ring { border: 2px solid rgba(255, 75, 75, 0.95); }
-.home-map-offline .home-map-attention-badge { background: #e67e22; }
-.home-map-offline .home-map-attention-ring { border: 2px solid rgba(255, 166, 77, 0.95); }
-@keyframes homeMapAttentionBounce {
-  0%, 100% { transform: translate(-50%, 0); }
-  50% { transform: translate(-50%, -8px); }
-}
-@keyframes homeMapAttentionPulse {
-  0% { opacity: 0.9; transform: translate(-50%, -50%) scale(0.7); }
-  70%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(2.2); }
-}
+.monitor-container .el-table .el-table__row { background: #fff !important; }
+.monitor-container .el-table .el-table__row:hover > td.el-table__cell { background: #f8fbff !important; }
+.monitor-container .el-table .el-table__empty-block { background: #fff !important; }
+.monitor-container .el-table .el-table__empty-text { color: #97a3b7; }
+.monitor-container .el-table .el-scrollbar__bar { opacity: 0.4; }
+.monitor-container .el-table { height: 100% !important; display: flex; flex-direction: column; }
+.monitor-container .el-table .el-table__inner-wrapper { flex: 1; min-height: 0; display: flex; flex-direction: column; }
+.monitor-container .el-table .el-table__body-wrapper { flex: 1; min-height: 0; }
 </style>
 
-<!-- ==================== 页面布局样式(scoped) ==================== -->
+<!-- ==================== 页面布局(scoped) ==================== -->
 <style scoped>
 .monitor-container {
-  display: flex;
-  width: 100%;
-  height: 100vh;
-  min-height: 600px;
-  background: #1a1a4a;
-  color: #fff;
-  gap: 16px;
-  padding: 16px;
-  box-sizing: border-box;
-  overflow: hidden;
-}
-.left-col {
-  flex: 0 0 25%;
-  min-width: 280px;
-  max-width: 420px;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  min-height: 0;
-}
-.right-col {
-  flex: 0 0 25%;
-  min-width: 280px;
-  max-width: 420px;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  min-height: 0;
-}
-.center-col {
-  flex: 1;
-  min-width: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  min-height: 0;
-}
+  display: flex; flex-direction: column;
+  width: 100%; height: calc(100vh - 84px); min-height: 620px;
+  background: #f0f3f8;
+  color: #1f2937; gap: 12px; padding: 14px 16px 16px;
+  box-sizing: border-box; overflow: hidden;
+}
+
+/* ===== 顶部健康度带 ===== */
+.top-bar {
+  display: flex; align-items: center; gap: 22px; flex: none;
+  padding: 12px 18px; border-radius: 10px;
+  background: #fff;
+  border: 1px solid #e3e9f2;
+  box-shadow: 0 2px 8px rgba(15, 34, 67, 0.06);
+}
+.brand { flex: none; min-width: 210px; }
+.brand-title { font-size: 20px; font-weight: 700; letter-spacing: 2px; color: #0a1628; }
+.health-band { flex: 1; display: flex; align-items: center; gap: 24px; min-width: 0; }
+.hb-rate { flex: none; min-width: 108px; display: flex; flex-direction: column; gap: 3px; }
+.hb-rate b { font-size: 38px; line-height: 1; font-weight: 700; color: #16a34a; font-variant-numeric: tabular-nums; }
+.hb-rate b.low { color: #ef4444; }
+.hb-rate span { font-size: 11px; color: #97a3b7; }
+.hb-bar { flex: 1; min-width: 150px; height: 12px; border-radius: 6px; overflow: hidden; display: flex; background: #eef2f7; }
+.hb-bar i { display: block; height: 100%; transition: width 0.4s; }
+.hb-bar i.on { background: #16a34a; }
+.hb-bar i.off { background: #94a3b8; }
+.hb-bar i.al { background: #ef4444; }
+.hb-items { display: flex; gap: 22px; flex: none; }
+.hb-items > div { display: flex; flex-direction: column; gap: 3px; }
+.hb-items b { font-size: 22px; font-weight: 700; line-height: 1; color: #0a1628; font-variant-numeric: tabular-nums; }
+.hb-items span { font-size: 11px; color: #97a3b7; }
+.text-green { color: #16a34a !important; }
+.text-gray { color: #64748b !important; }
+.text-red { color: #ef4444 !important; }
+/* ===== 主体三列 ===== */
+.main-grid {
+  flex: 1; display: grid;
+  grid-template-columns: minmax(280px, 24%) minmax(0, 1fr) minmax(300px, 26%);
+  gap: 12px; min-height: 0;
+}
+.col { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
 .panel {
-  flex: 1;
-  background: rgba(30, 40, 90, 0.8);
-  border: 1px solid #2b4b8c;
-  border-radius: 8px;
-  padding: 14px;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
-}
-/* 设备分类固定高度,不参与均分 */
-.chart-panel {
-  flex: 0 0 190px;
+  background: #fff;
+  border: 1px solid #e3e9f2; border-radius: 10px;
+  padding: 12px 14px; box-sizing: border-box;
+  display: flex; flex-direction: column; min-height: 0;
+  box-shadow: 0 2px 8px rgba(15, 34, 67, 0.06);
 }
 .panel-title {
-  margin: 0 0 10px 0;
-  font-size: 14px;
-  color: #66ccff;
-  border-left: 3px solid #66ccff;
-  padding-left: 8px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-shrink: 0;
-}
-.title-count {
-  font-size: 12px;
-  color: #aaa;
-  font-weight: normal;
-}
-.panel-table {
-  width: 100%;
-  flex: 1;
-  min-height: 0;
-  overflow: hidden;
-}
-.chart {
-  width: 100%;
-  flex: 1;
-  min-height: 100px;
-}
-.map-panel {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
-}
+  margin: 0 0 10px; font-size: 14px; font-weight: 600; color: #1f2937;
+  border-left: 3px solid #1677ff; padding-left: 9px;
+  display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
+}
+.title-count { font-size: 11px; color: #97a3b7; font-weight: 400; }
+.empty { padding: 28px 0; text-align: center; color: #97a3b7; font-size: 12px; }
+
+/* 实时报警 */
+.alarm-panel { flex: 1.1; }
+.badge {
+  display: inline-flex; min-width: 20px; height: 20px; padding: 0 6px;
+  border-radius: 10px; background: #ef4444; color: #fff; font-size: 12px;
+  align-items: center; justify-content: center; font-weight: 600;
+}
+.badge.zero { background: #cbd5e1; }
+.alarm-list { flex: 1; overflow-y: auto; min-height: 0; padding-right: 2px; }
+.alarm-item {
+  padding: 9px 10px; margin-bottom: 8px; border-radius: 6px;
+  border-left: 3px solid #ef4444; background: #fffafa;
+  border-bottom: 1px solid #eef2f7;
+  transition: background 0.15s;
+}
+.alarm-item:hover { background: #f8fbff; }
+.alarm-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
+.alarm-dev { font-size: 13px; font-weight: 600; color: #1f2937; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.alarm-code {
+  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; color: #5e6d82;
+  background: #f1f5f9; border: 1px solid #e3e9f2;
+  border-radius: 4px; padding: 1px 5px; margin-left: 6px;
+}
+.alarm-level { font-size: 11px; font-weight: 600; flex: none; }
+.alarm-time { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; color: #97a3b7; margin: 5px 0 3px; font-variant-numeric: tabular-nums; }
+.alarm-no { font-family: ui-monospace, Menlo, Consolas, monospace; color: #8a96a8; }
+.alarm-desc { font-size: 12px; color: #5e6d82; line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.work-panel { flex: 1; }
+.panel-table { width: 100%; flex: 1; min-height: 0; overflow: hidden; }
+.priority-tag { padding: 1px 6px; border-radius: 3px; font-size: 11px; color: #fff; }
+.priority-tag.high { background: #ef4444; }
+.priority-tag.medium { background: #f59e0b; }
+.priority-tag.low { background: #16a34a; }
+
+/* ===== 中间 GIS 一张图 ===== */
+.map-panel { flex: 1; }
 .map-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 8px;
-  font-size: 14px;
-  color: #66ccff;
-  flex-shrink: 0;
+  display: flex; justify-content: space-between; align-items: center;
+  margin-bottom: 10px; font-size: 14px; color: #1f2937; flex-shrink: 0;
 }
+.map-header :deep(.el-button) {
+  background: #f4f9ff; border-color: #d6e6ff; color: #1677ff;
+}
+.map-wrap { position: relative; flex: 1; min-height: 0; border-radius: 8px; overflow: hidden; border: 1px solid #e3e9f2; }
+/* GisMapCanvas 自带 min-height 420px,大屏内需由父容器高度决定 */
+.map-wrap :deep(.gis-map-canvas) { height: 100% !important; min-height: 0 !important; }
 .map-legend {
-  display: flex;
-  gap: 12px;
-  font-size: 11px;
-}
-.legend-dot {
-  display: inline-block;
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-  margin-right: 3px;
-}
-.legend-dot.online { background: #67c23a; }
-.legend-dot.alert { background: #f56c6c; }
-.legend-dot.offline { background: #909399; }
-.bmap-container {
-  width: 100%;
-  flex: 1;
-  min-height: 200px;
-  border-radius: 4px;
-  overflow: hidden;
-}
-.filter-bar {
-  margin-bottom: 10px;
-  flex-shrink: 0;
-}
-.priority-tag {
-  padding: 1px 6px;
-  border-radius: 3px;
-  font-size: 11px;
-  color: #fff;
-}
-.priority-tag.high { background: #ff3333; }
-.priority-tag.medium { background: #ffb333; color: #333; }
-.priority-tag.low { background: #33cc33; }
-.status-tag {
-  padding: 1px 6px;
-  border-radius: 3px;
-  font-size: 11px;
-  color: #fff;
-}
-.status-tag.processing { background: #3399ff; }
-.status-tag.done { background: #33cc33; }
-.status-dot {
-  display: inline-block;
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-  margin-right: 3px;
-}
-.status-dot.normal { background: #67c23a; }
-.status-dot.alert { background: #f56c6c; }
-.status-dot.offline { background: #909399; }
-.status-text {
-  font-size: 11px;
-  color: #dce3ee;
-}
-
-/* === 响应式 === */
-@media (max-width: 1600px) {
-  .left-col, .right-col { flex: 0 0 24%; min-width: 260px; max-width: 380px; }
-  .monitor-container { gap: 12px; padding: 12px; }
-  .panel { padding: 12px; }
-  .chart-panel { flex-basis: 170px; }
-}
-@media (max-width: 1366px) {
-  .left-col, .right-col { flex: 0 0 27%; min-width: 240px; max-width: 340px; }
-  .monitor-container { gap: 10px; padding: 10px; }
-  .panel { padding: 10px; }
-  .chart-panel { flex-basis: 150px; }
+  position: absolute; top: 12px; left: 12px; z-index: 9999;
+  padding: 10px 12px; border-radius: 8px;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e3e9f2;
+  box-shadow: 0 2px 8px rgba(15, 34, 67, 0.1);
+  pointer-events: auto;
+}
+.legend-title { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: #97a3b7; }
+.legend-row {
+  display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
+  padding: 0; border: none; background: transparent; font: inherit;
+  font-size: 13px; color: #5e6d82; cursor: pointer;
+  transition: opacity 0.15s, color 0.15s;
+}
+.legend-row:last-child { margin-bottom: 0; }
+.legend-row:hover { color: #1677ff; }
+.legend-row.is-off { opacity: 0.4; text-decoration: line-through; }
+.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
+.legend-dot.online { background: #16a34a; }
+.legend-dot.offline { background: #94a3b8; }
+.legend-dot.alarm { background: #ef4444; }
+.map-error { position: absolute; left: 16px; bottom: 16px; width: 360px; z-index: 2; }
+.map-empty { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; }
+
+/* ===== 报警类型分布(横条) ===== */
+.dist-panel { flex: 0 0 36%; min-height: 0; }
+.type-dist { flex: 1; overflow-y: auto; min-height: 0; padding: 2px; }
+.td-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid #f1f5f9; font-size: 12px; }
+.td-row:last-child { border-bottom: none; }
+.td-name { width: 96px; flex: none; color: #5e6d82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.td-track { flex: 1; height: 10px; border-radius: 5px; background: #f1f5f9; overflow: hidden; min-width: 50px; }
+.td-track i { display: block; height: 100%; border-radius: 5px; background: #1677ff; transition: width 0.4s; }
+.td-num { width: 44px; text-align: right; color: #97a3b7; flex: none; font-variant-numeric: tabular-nums; }
+
+/* ===== 设备状态总览(片区点阵卡) ===== */
+.area-panel { flex: 1; }
+.area-cards { flex: 1; overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 10px; padding: 2px; }
+.acard { flex: none; border: 1px solid #e3e9f2; border-radius: 8px; padding: 10px 12px; background: #fff; transition: 0.18s; }
+.acard:hover { border-color: #1677ff; background: #f8fbff; }
+.acard.has-alarm { border-color: #fca5a5; background: linear-gradient(180deg, #fffafa 0%, #fff 42%); }
+.ac-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
+.ac-name { font-size: 15px; font-weight: 700; color: #0a1628; }
+.ac-sub { font-size: 10px; color: #97a3b7; margin-top: 3px; }
+.ac-rate { text-align: right; flex: none; line-height: 1.1; }
+.ac-rate b { display: block; font-size: 20px; font-weight: 700; color: #16a34a; font-variant-numeric: tabular-nums; }
+.ac-rate span { font-size: 10px; color: #97a3b7; }
+.ac-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: #eef2f7; margin: 9px 0; }
+.ac-bar i { display: block; height: 100%; transition: width 0.4s; }
+.ac-bar i.on { background: #16a34a; }
+.ac-bar i.off { background: #94a3b8; }
+.ac-bar i.al { background: #ef4444; }
+.dot-matrix { display: flex; flex-wrap: wrap; gap: 4px; align-content: flex-start; min-height: 9px; }
+.dot { width: 9px; height: 9px; border-radius: 2px; background: #16a34a; }
+.dot.off { background: #cbd5e1; }
+.dot.al { background: #ef4444; animation: dotFlash 1s steps(1, end) infinite; }
+.ac-tip { font-size: 11px; color: #1677ff; }
+.ac-metrics { display: flex; gap: 16px; font-size: 10px; color: #97a3b7; margin-top: 9px; }
+.ac-metrics b { color: #0a1628; font-size: 12px; }
+@keyframes dotFlash { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.3; } }
+
+/* ===== 设备详情弹窗(与 equipmentGIS.vue 一致) ===== */
+.dlg-summary { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 14px; border-radius: 8px; background: #f5f9ff; }
+.summary-name { font-size: 15px; font-weight: 500; color: #014db2; }
+.summary-code { font-size: 12px; color: #6b7280; }
+.summary-tag { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; background: #fff; font-size: 11px; font-weight: 500; color: #014db2; }
+.status-group { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
+.st-badge { display: flex; align-items: center; gap: 5px; }
+.badge-dot { width: 6px; height: 6px; border-radius: 3px; flex-shrink: 0; }
+.badge-text { font-size: 12px; font-weight: 500; }
+.st-badge.online .badge-dot { background: #10b981; }
+.st-badge.online .badge-text { color: #10b981; }
+.st-badge.offline .badge-dot { background: #f59e0b; }
+.st-badge.offline .badge-text { color: #f59e0b; }
+.st-badge.alarm .badge-text { color: #ef4444; }
+.badge-icon { font-size: 13px; color: #ef4444; }
+.form-section-title { margin: 8px 0 12px; padding-left: 10px; border-left: 3px solid #014db2; color: #1f2937; font-size: 14px; font-weight: 600; }
+.detail-section-title { margin-top: 18px; }
+.dlg-grid { margin-top: 16px; }
+.detail-image { margin-top: 16px; }
+.field-label { margin-bottom: 6px; font-size: 12px; color: #6b7280; }
+.field-media { width: 320px; max-width: 100%; height: 180px; border-radius: 6px; border: 1px solid #e6e6eb; }
+.dlg-footer { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
+
+/* ===== 响应式 ===== */
+@media (max-width: 1500px) {
+  .main-grid { grid-template-columns: minmax(250px, 23%) minmax(0, 1fr) minmax(265px, 25%); }
+  .top-bar { gap: 14px; }
+  .hb-items { gap: 14px; }
+  .hb-rate b { font-size: 32px; }
+}
+@media (max-width: 1280px) {
+  .brand { min-width: 170px; }
+  .brand-title { font-size: 17px; }
+  .hb-items b { font-size: 18px; }
+  .hb-items { gap: 12px; }
 }
 @media (max-width: 1024px) {
-  .monitor-container {
-    flex-direction: column;
-    height: auto;
-    min-height: 100vh;
-    overflow: auto;
-  }
-  .left-col, .right-col {
-    flex: 1 1 auto;
-    min-width: 100%;
-    max-width: 100%;
-  }
-  .center-col {
-    flex: 1 1 auto;
-    min-width: 100%;
-  }
+  .monitor-container { height: auto; min-height: 100vh; overflow: auto; }
+  .main-grid { display: flex; flex-direction: column; }
+  .col { min-height: auto; }
+  .panel { min-height: 240px; }
+  .map-panel { min-height: 480px; }
+  .health-band { flex-wrap: wrap; }
+  .dist-panel { flex: 0 0 auto; }
 }
 </style>