Bläddra i källkod

重构供水,排水,燃气,窨井首页并复用

zengy 1 vecka sedan
förälder
incheckning
5f21ba511d

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "test": "node --test tests/waterHomeRouteResolver.test.mjs tests/waterSupplyMonitoring.test.mjs tests/waterSupplyModel.test.mjs tests/equipmentTypeQuickFilter.test.mjs",
+    "test": "node --test tests/homeScope.test.mjs tests/waterSupplyMonitoring.test.mjs tests/waterSupplyModel.test.mjs tests/equipmentTypeQuickFilter.test.mjs",
     "build:prod": "vite build",
     "build:stage": "vite build --mode staging",
     "preview": "vite preview"

+ 0 - 8
src/api/pipeNetwork/basic.js

@@ -71,11 +71,6 @@ export function getEquipmentGisPoints(params) {
   return request({ url: '/equipment-gis/points', method: 'get', params })
 }
 
-// 态势图层 - 获取设备列表和统计(窨井监测一张图等共用,alarmSanalysis 已下线)
-export function getManholeLayerData(data = {}) {
-  return request({ url: '/manhole/layer/getManholeLayerData', method: 'post', data })
-}
-
 
 // ============ 报警 / 工单(案件流转:复用现有 alarm、ops/workOrder 模块) ============
 // 报警列表(AlarmQuery 过滤:keyword/warningCode/alarmLevel/auditStatus/displayStatus/alarmNo/deviceCode/startTime/endTime/rootTypeId)
@@ -225,9 +220,6 @@ export function getEquipmentTypeList() {
 export function getEquipmentByTopLevelType(typeName) {
   return request({ url: '/equipment-base/top-level-type', method: 'get', params: { typeName } })
 }
-export function getGasDashboardSummary() {
-  return request({ url: '/api/gas-dashboard/summary', method: 'get' })
-}
 // ============ 管点-设备关联 ============
 export function getPipePointDevices(pointId) {
   return request({ url: '/api/gas-pipe-point/device/list/' + pointId, method: 'get' })

+ 56 - 0
src/api/pipeNetwork/home.js

@@ -0,0 +1,56 @@
+import request from '@/utils/request'
+import { normalizeRootTypeId } from '@/utils/homeScope'
+
+/**
+ * 供水 / 排水 / 燃气 / 窨井四个子系统共用的首页接口。
+ *
+ * 数据范围由菜单 sys_menu.query 下发的 rootTypeId 决定(与系统既有约定一致):
+ *   1-供水设备  2-排水设备  3-燃气设备  4-窨井设备
+ * 前端从 useRoute().query.rootTypeId 读取后显式传给每个接口;请求拦截器不会自动附加,
+ * 后端 HomeController 会再次校验该用户是否拥有对应 rootTypeId 的首页菜单。
+ *
+ * 这里做最后一道防线:缺失或非法的 rootTypeId 直接报错,绝不退化成某个默认子系统。
+ */
+const BASE = '/api/home'
+
+function withScope(params) {
+  const rootTypeId = normalizeRootTypeId(params?.rootTypeId)
+  if (!rootTypeId) {
+    throw new Error('首页数据范围缺失:请检查 sys_menu.query.rootTypeId(1供水 / 2排水 / 3燃气 / 4窨井)')
+  }
+  return { ...params, rootTypeId }
+}
+
+/** 设备清单(首页顶部健康度带、片区点阵) */
+export function getHomeDevices(rootTypeId) {
+  return request({ url: `${BASE}/devices`, method: 'get', params: withScope({ rootTypeId }) })
+}
+
+/** 待办工单(orderStatus 支持逗号分隔多值) */
+export function getHomeWorkOrders(params) {
+  return request({ url: `${BASE}/work-orders`, method: 'get', params: withScope(params) })
+}
+
+/** 报警清单(AlarmQuery:rootTypeId / pageNum / pageSize / startTime ...) */
+export function getHomeAlarms(params) {
+  return request({ url: `${BASE}/alarms`, method: 'get', params: withScope(params) })
+}
+
+/** 设备 GIS 点位 */
+export function getHomeGisPoints(rootTypeId) {
+  return request({ url: `${BASE}/gis/points`, method: 'get', params: withScope({ rootTypeId }) })
+}
+
+/** 管网 GeoJSON(FeatureCollection,LineString 起终点坐标);rootTypeId=4 不区分管网类型,返回全部管线 */
+export function getHomePipeLines(rootTypeId) {
+  return request({ url: `${BASE}/gis/pipe-lines`, method: 'get', params: withScope({ rootTypeId }) })
+}
+
+/** 设备详情弹窗 */
+export function getHomeEquipmentDetail(equipmentId, rootTypeId) {
+  return request({
+    url: `${BASE}/equipment/${encodeURIComponent(equipmentId)}/detail`,
+    method: 'get',
+    params: withScope({ rootTypeId })
+  })
+}

+ 1 - 5
src/api/pipeNetwork/waterSupply.js

@@ -20,16 +20,12 @@ export const exportSupplyPressureRecords = (params) => request({ url: '/waterSup
 export const exportHydrantPressureRecords = (params) => request({ url: '/waterSupply/monitoring/pressure/hydrant/export', method: 'get', params, responseType: 'blob' })
 export const exportLeakageRecords = (params) => request({ url: '/waterSupply/monitoring/leakage/export', method: 'get', params, responseType: 'blob' })
 
-export const getAlarmStats = () => get('/waterSupply/alarm/stats')
-export const listRealtimeAlarms = () => get('/waterSupply/alarm/realtime')
-
-// 供水设备 GIS 一张图(WaterDeviceGisMap / WaterSupplyHome 使用)
+// 供水设施 GIS 一张图(旧供水首页的 stats/realtime/allPoints 接口已随首页统一下线)
 export const getPipeNetworkDashboard = (params) => get('/waterSupply/pipeNetwork/statistics/dashboard', params)
 export const exportPipeNetworkStatistics = (params) => request({ url: '/waterSupply/pipeNetwork/statistics/export', method: 'get', params, responseType: 'blob' })
 export const getWaterFacilityFeatures = (params) => get('/waterSupply/gis/map/features', params)
 export const getWaterFacilityOverview = () => get('/waterSupply/gis/map/overview')
 export const getWaterFacilityDetail = (type, id) => get(`/waterSupply/gis/map/detail/${type}/${id}`)
-export const getDeviceGisAllPoints = () => get('/waterSupply/device/gis/allPoints')
 export const getDeviceGisAbnormalPoints = () => get('/waterSupply/device/gis/abnormalPoints')
 export const getDeviceGisStatistics = () => get('/waterSupply/device/gis/statistics')
 export const getDeviceGisDetail = (id) => get(`/waterSupply/device/gis/detail/${id}`)

+ 0 - 79
src/router/waterHomeRouteResolver.js

@@ -1,79 +0,0 @@
-const SCORE = {
-  SUFFIX_BASE: 1000,
-  WATER_PATH: 500,
-  ROOT_TYPE: 400,
-  SHARED_ALARM_PATH: 200,
-  TITLE: 100
-}
-
-const WATER_ALARM_ENTRY_ALIASES = {
-  '/monitorAlarm/index': {
-    title: '报警管理',
-    suffixes: ['/monitorAlarm/index', '/alarm/common/list', '/alarm/list', '/alarm/alarmList', '/alarm/index']
-  },
-  '/monitorAlarm/audit': {
-    title: '报警核实',
-    suffixes: ['/monitorAlarm/audit', '/alarm/common/audit', '/alarm/audit', '/alarm/alarmAudit']
-  }
-}
-
-function normalizePath(value) {
-  const path = String(value || '').trim()
-  if (!path) return ''
-  const normalized = path.startsWith('/') ? path : `/${path}`
-  return normalized.replace(/\/+/gu, '/').replace(/\/$/u, '') || '/'
-}
-
-function parseQuery(value) {
-  if (!value) return {}
-  if (typeof value === 'object') return value
-  try { return JSON.parse(value) || {} } catch { return {} }
-}
-
-function routeRootTypeId(route) {
-  const query = parseQuery(route?.query || route?.meta?.query)
-  return query.rootTypeId == null ? null : Number(query.rootTypeId)
-}
-
-function routeScore(route, candidates, title) {
-  const path = normalizePath(route?.path)
-  if (!path) return Number.NEGATIVE_INFINITY
-  const rootTypeId = routeRootTypeId(route)
-  if (rootTypeId != null && rootTypeId !== 1) return Number.NEGATIVE_INFINITY
-  if (title && !path.includes('/subSystem/waterSupply/') && !path.startsWith('/subSystem/alarm/')) {
-    return Number.NEGATIVE_INFINITY
-  }
-
-  const suffixIndex = candidates.findIndex(candidate => path.endsWith(normalizePath(candidate)))
-  const titleMatch = route?.meta?.title === title || (title === '报警核实' && route?.meta?.title === '报警审核')
-  if (suffixIndex < 0 && !titleMatch) return Number.NEGATIVE_INFINITY
-  let score = suffixIndex < 0 ? 0 : SCORE.SUFFIX_BASE - suffixIndex
-  if (titleMatch) score += SCORE.TITLE
-  if (path.includes('/subSystem/waterSupply/')) score += SCORE.WATER_PATH
-  if (rootTypeId === 1) score += SCORE.ROOT_TYPE
-  if (path.startsWith('/subSystem/alarm/')) score += SCORE.SHARED_ALARM_PATH
-  return score
-}
-
-/**
- * Resolve a semantic water-home entry against the routes currently registered by
- * the permission store. Shared alarm menus and dedicated water menus both occur
- * in deployed environments, so this boundary must not assume a database path.
- */
-export function resolveWaterHomeRoute(routes, target) {
-  const list = Array.isArray(routes) ? routes : []
-  const canonicalPath = typeof target === 'string' ? target : target?.path
-  const definition = WATER_ALARM_ENTRY_ALIASES[canonicalPath]
-  const candidates = definition?.suffixes || [canonicalPath]
-  const title = definition?.title
-  let best = null
-  let bestScore = Number.NEGATIVE_INFINITY
-  for (const route of list) {
-    const score = routeScore(route, candidates, title)
-    if (score > bestScore) {
-      best = route
-      bestScore = score
-    }
-  }
-  return bestScore === Number.NEGATIVE_INFINITY ? null : best
-}

+ 76 - 0
src/utils/homeScope.js

@@ -0,0 +1,76 @@
+/** 四大设备大类根ID,与后端 EquipmentTypeEnum 一致(1供水 2排水 3燃气 4窨井) */
+export const HOME_ROOT_TYPES = { 1: '供水', 2: '排水', 3: '燃气', 4: '窨井' }
+
+/** 统一首页组件,需与 sys_menu.component 保持一致 */
+export const HOME_COMPONENT = 'subSystem/pipeNetwork/home/index'
+
+/** 菜单 query 在 /getRouters 里是 JSON 字符串,部分页面/路由也可能已是对象 */
+export function parseMenuQuery(raw) {
+  if (!raw) return {}
+  if (typeof raw === 'object') return raw
+  try {
+    const parsed = JSON.parse(raw)
+    return parsed && typeof parsed === 'object' ? parsed : {}
+  } catch {
+    return {}
+  }
+}
+
+/** 只接受四个合法设备大类,非法值一律返回 undefined,绝不回退到某个固定子系统 */
+export function normalizeRootTypeId(value) {
+  if (value == null || value === '') return undefined
+  const id = Number(value)
+  return Object.prototype.hasOwnProperty.call(HOME_ROOT_TYPES, id) ? id : undefined
+}
+
+/**
+ * 递归展开菜单树,收集 path=home 且 query 带合法 rootTypeId 的已授权首页菜单。
+ * sidebarRouters 的 component 已被 filterAsyncRouter 替换为函数,因此按叶子 path 判定;
+ * 绝对路径拼法与 alarmOperationsModel.findAlarmMenu 一致。
+ */
+export function collectHomeScopes(menus = [], parentPath = '') {
+  const scopes = []
+  for (const menu of menus || []) {
+    const rawPath = String(menu?.path ?? '')
+    const path = rawPath.startsWith('/')
+      ? rawPath
+      : `${parentPath}/${rawPath}`.replace(/\/+/g, '/')
+    const query = parseMenuQuery(menu?.query ?? menu?.meta?.query)
+    const rootTypeId = normalizeRootTypeId(query.rootTypeId ?? menu?.meta?.rootTypeId)
+    const leaf = path.split('/').filter(Boolean).at(-1)
+    if (leaf === 'home' && rootTypeId) {
+      scopes.push({ path, name: menu?.name, rootTypeId })
+    }
+    if (Array.isArray(menu?.children)) {
+      scopes.push(...collectHomeScopes(menu.children, path))
+    }
+  }
+  return scopes
+}
+
+/**
+ * 解析当前首页的数据范围。优先级:
+ *   1. URL 的 query.rootTypeId(点击菜单时由 SidebarItem 写入)
+ *   2. URL 的 ?query={"rootTypeId":N}(兼容把菜单 query 原样透传的链接)
+ *   3. 当前路由路径唯一命中的首页菜单配置(刷新/直达/TagsView 丢了 URL 参数时兜底)
+ *   4. 当前账号只被授权了一个首页菜单时,用该菜单
+ * 全部失败返回 rootTypeId=undefined,由页面提示配置 sys_menu.query,不再默认查窨井数据。
+ */
+export function resolveHomeScope({ query = {}, path = '', menus = [] } = {}) {
+  const fromUrl = normalizeRootTypeId(query?.rootTypeId)
+    ?? normalizeRootTypeId(parseMenuQuery(query?.query).rootTypeId)
+  const scopes = collectHomeScopes(menus)
+  const matched = scopes.filter(scope => path && (
+    scope.path === path || path.endsWith('/' + String(scope.path).replace(/^\/+/, ''))
+  ))
+  const unique = list => new Set(list.map(item => item.rootTypeId)).size === 1 ? list[0].rootTypeId : undefined
+  const fromMenu = unique(matched)
+  const fromOnlyMenu = unique(scopes)
+  const rootTypeId = fromUrl ?? fromMenu ?? fromOnlyMenu
+  return {
+    rootTypeId,
+    source: fromUrl ? 'url' : (fromMenu ? 'menu' : (fromOnlyMenu ? 'only-menu' : undefined)),
+    scopes,
+    matched
+  }
+}

+ 0 - 1249
src/views/subSystem/drainage/home.vue

@@ -1,1249 +0,0 @@
-<template>
-  <div class="drainage-home-screen">
-    <!-- ==================== 顶部标题栏 ==================== -->
-    <div class="screen-header">
-      <div class="header-left">
-        <span class="header-time">{{ currentDateTime }}</span>
-      </div>
-      <div class="header-center">
-        <h1 class="header-title">排水管网安全运行监测系统</h1>
-      </div>
-      <div class="header-right"></div>
-    </div>
-
-    <!-- ==================== Tab 1: 雨水污水 ==================== -->
-    <div v-show="activeTab === 0" class="screen-body">
-      <div class="panel-left">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 天气预报</div>
-          <div class="weather-cards">
-            <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
-              <div class="weather-date">{{ w.date }}</div>
-              <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
-              <div class="weather-desc">{{ w.desc }}</div>
-              <div class="weather-temp">{{ w.tempRange }}</div>
-              <div class="weather-wind">{{ w.wind }}</div>
-            </div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 监测点概况</div>
-          <div class="overview-cards">
-            <div class="overview-card"><div class="ov-label">监测总点数</div><div class="ov-value">{{ rwOverview.total }}</div></div>
-            <div class="overview-card"><div class="ov-label">在线监测点</div><div class="ov-value online">{{ rwOverview.online }}</div></div>
-            <div class="overview-card"><div class="ov-label">离线监测点</div><div class="ov-value offline">{{ rwOverview.offline }}</div></div>
-          </div>
-          <div class="pie-chart-wrapper">
-            <div id="rwPieChart" class="chart-area"></div>
-            <div class="pie-legend">
-              <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ rwOnlineRate }}%</span></div>
-              <div class="legend-item"><span class="legend-dot" style="background:#f56c6c"></span><span class="legend-label">离线率</span><span class="legend-value">{{ 100 - rwOnlineRate }}%</span></div>
-            </div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 实时数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>监测点</th><th>时间</th><th>积水量</th><th>排水量</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in rwRealtimeData" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td>{{ row.waterlogging }}</td><td>{{ row.drainage }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-      <div class="panel-center">
-        <div id="rwMap" class="map-container"></div>
-        <div class="pipeline-map-status">
-          <span class="pipeline-status-dot"></span>
-          排水管网 {{ drainagePipelines.length }} 段
-        </div>
-        <div v-if="pipelineMapError" class="pipeline-map-error">{{ pipelineMapError }}</div>
-      </div>
-      <div class="panel-right">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 近日排水量</div>
-          <div id="rwDrainageBarChart" class="chart-area-sm"></div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 近日积水量</div>
-          <div id="rwWaterlogBarChart" class="chart-area-sm"></div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 预警信息</div>
-          <div class="warn-summary">
-            <div class="warn-card"><div class="warn-icon">✅</div><div class="warn-label">设备在线</div><div class="warn-value online">{{ rwWarnSummary.online }}</div></div>
-            <div class="warn-card"><div class="warn-icon">❌</div><div class="warn-label">设备离线</div><div class="warn-value offline">{{ rwWarnSummary.offline }}</div></div>
-          </div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>设备名称</th><th>时间</th><th>设备编号</th><th>设备类型</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in rwWarnData" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.deviceName">{{ row.deviceName }}</td><td>{{ row.time }}</td><td>{{ row.deviceCode }}</td><td>{{ row.deviceType }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- ==================== Tab 2: 设备管理 ==================== -->
-    <div v-show="activeTab === 1" class="screen-body">
-      <div class="panel-left">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 基础信息</div>
-          <div class="overview-cards">
-            <div class="overview-card"><div class="ov-label">设备总数</div><div class="ov-value">{{ deviceOverview.total }}</div></div>
-            <div class="overview-card"><div class="ov-label">在线点</div><div class="ov-value online">{{ deviceOverview.online }}</div></div>
-            <div class="overview-card"><div class="ov-label">离线点</div><div class="ov-value offline">{{ deviceOverview.offline }}</div></div>
-          </div>
-          <div class="pie-chart-wrapper">
-            <div id="devicePieChart" class="chart-area"></div>
-            <div class="pie-legend">
-              <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ deviceOnlineRate }}%</span></div>
-              <div class="legend-item"><span class="legend-dot" style="background:#f56c6c"></span><span class="legend-label">离线率</span><span class="legend-value">{{ 100 - deviceOnlineRate }}%</span></div>
-            </div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 维修记录</div>
-          <div id="deviceRepairChart" class="chart-area-sm"></div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 点位数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>设备名称</th><th>时间</th><th>安装位置</th><th>在线状态</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in devicePointData" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td :title="row.location">{{ row.location }}</td>
-                  <td><span :class="'level-tag level-' + row.statusClass">{{ row.status }}</span></td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-      <div class="panel-center">
-        <div id="deviceMap" class="map-container"></div>
-      </div>
-      <div class="panel-right">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 设备信息</div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>设备名称</th><th>设备编号</th><th>所在位置</th><th>运行状态</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in deviceList" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.code }}</td><td :title="row.location">{{ row.location }}</td>
-                  <td><span :class="'level-tag level-' + row.statusClass">{{ row.status }}</span></td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 警告信息</div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table warn-table">
-              <thead><tr><th>告警等级</th><th>设备编码</th><th>时间</th><th>报警内容</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in deviceWarnData" :key="i">
-                  <td><span :class="'level-tag level-' + row.levelClass">{{ row.levelText }}</span></td><td>{{ row.code }}</td><td>{{ row.time }}</td><td :title="row.content">{{ row.content }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 报警统计</div>
-          <div class="alarm-stat">
-            <div class="alarm-stat-item"><div class="alarm-icon">⚠️</div><div class="alarm-label">一般危险</div><div class="alarm-value yellow">{{ alarmStat.general }}</div></div>
-            <div class="alarm-stat-item"><div class="alarm-icon">🚨</div><div class="alarm-label">严重危险</div><div class="alarm-value red">{{ alarmStat.severe }}</div></div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- ==================== Tab 3: 积水管理 ==================== -->
-    <div v-show="activeTab === 2" class="screen-body">
-      <div class="panel-left">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 天气预报</div>
-          <div class="weather-cards">
-            <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
-              <div class="weather-date">{{ w.date }}</div>
-              <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
-              <div class="weather-desc">{{ w.desc }}</div>
-              <div class="weather-temp">{{ w.tempRange }}</div>
-              <div class="weather-wind">{{ w.wind }}</div>
-            </div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 监测点概况</div>
-          <div class="overview-cards">
-            <div class="overview-card"><div class="ov-label">在线</div><div class="ov-value online">{{ waterOverview.online }}</div></div>
-            <div class="overview-card"><div class="ov-label">离线</div><div class="ov-value offline">{{ waterOverview.offline }}</div></div>
-            <div class="overview-card"><div class="ov-label">在线率</div><div class="ov-value">{{ waterOnlineRate }}%</div></div>
-          </div>
-          <div class="pie-chart-wrapper">
-            <div id="waterPieChart" class="chart-area"></div>
-            <div class="pie-legend">
-              <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ waterOnlineRate }}%</span></div>
-              <div class="legend-item"><span class="legend-dot" style="background:#f56c6c"></span><span class="legend-label">离线率</span><span class="legend-value">{{ 100 - waterOnlineRate }}%</span></div>
-            </div>
-          </div>
-          <div class="level-counts">
-            <div class="level-count"><span class="dot green"></span>未积水: {{ waterLevelCount.none }}</div>
-            <div class="level-count"><span class="dot blue"></span>轻度积水: {{ waterLevelCount.light }}</div>
-            <div class="level-count"><span class="dot orange"></span>中度积水: {{ waterLevelCount.moderate }}</div>
-            <div class="level-count"><span class="dot red"></span>严重积水: {{ waterLevelCount.severe }}</div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 实时数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>监测点</th><th>时间</th><th>积水量</th><th>级别</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in waterRealtimeData" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td>{{ row.depth }}cm</td>
-                  <td><span :class="'level-tag level-' + row.levelClass">{{ row.level }}</span></td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-      <div class="panel-center">
-        <div id="waterMap" class="map-container"></div>
-      </div>
-      <div class="panel-right">
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 积水分析</div>
-          <div class="analysis-form">
-            <div class="form-row"><label>年份</label><select v-model="analysisYear"><option v-for="y in analysisYears" :key="y" :value="y">{{ y }}</option></select></div>
-            <div class="form-row"><label>积水等级</label><select v-model="analysisLevel"><option v-for="opt in analysisLevels" :key="opt.value" :value="opt.value">{{ opt.label }}</option></select></div>
-            <button class="analysis-btn" @click="doWaterAnalysis">开始分析</button>
-            <div class="analysis-result">{{ waterAnalysisResult }}</div>
-          </div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 积水统计</div>
-          <div class="stat-tabs">
-            <span :class="{active: waterStatType==='month'}" @click="waterStatType='month'">按月</span>
-            <span :class="{active: waterStatType==='year'}" @click="waterStatType='year'">按年</span>
-          </div>
-          <div id="waterStatChart" class="chart-area-sm"></div>
-        </div>
-        <div class="panel-box">
-          <div class="panel-title"><span class="title-icon">▶</span> 预警信息</div>
-          <div class="warn-summary">
-            <div class="warn-card"><div class="warn-icon">💧</div><div class="warn-label">积水报警</div><div class="warn-value">{{ waterWarnSummary.water }}</div></div>
-            <div class="warn-card"><div class="warn-icon">✅</div><div class="warn-label">设备在线</div><div class="warn-value online">{{ waterWarnSummary.online }}</div></div>
-            <div class="warn-card"><div class="warn-icon">❌</div><div class="warn-label">设备离线</div><div class="warn-value offline">{{ waterWarnSummary.offline }}</div></div>
-          </div>
-          <div class="realtime-table-wrap">
-            <table class="dark-table">
-              <thead><tr><th>序号</th><th>地址</th><th>时间</th><th>报警类型</th><th>实时数据</th></tr></thead>
-              <tbody>
-                <tr v-for="(row, i) in waterWarnData" :key="i">
-                  <td>{{ i + 1 }}</td><td :title="row.addr">{{ row.addr }}</td><td>{{ row.time }}</td><td>{{ row.type }}</td><td>{{ row.value }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- ==================== 底部 Tab 栏 ==================== -->
-    <div class="bottom-tabs">
-      <div v-for="tab in tabs" :key="tab.value" class="bottom-tab" :class="{ active: activeTab === tab.value }" @click="switchTab(tab.value)">
-        {{ tab.label }}
-      </div>
-    </div>
-
-    <!-- ==================== 设备详情弹窗 ==================== -->
-    <div v-if="deviceModalVisible" class="device-modal" @click.self="closeDeviceModal">
-      <div class="device-modal-content">
-        <div class="device-modal-header">
-          <span class="device-modal-title">设备详情</span>
-          <span class="device-modal-close" @click="closeDeviceModal">×</span>
-        </div>
-        <div class="device-info-list">
-          <div class="device-info-item"><span class="info-label">设备编号</span><span class="info-value">{{ selectedDevice.code }}</span></div>
-          <div class="device-info-item"><span class="info-label">设备名称</span><span class="info-value">{{ selectedDevice.name }}</span></div>
-          <div class="device-info-item"><span class="info-label">安装日期</span><span class="info-value">{{ selectedDevice.installDate }}</span></div>
-          <div class="device-info-item"><span class="info-label">运行状态</span><span class="info-value"><span :class="'level-tag level-' + selectedDevice.statusClass">{{ selectedDevice.status }}</span></span></div>
-          <div class="device-info-item"><span class="info-label">频率</span><span class="info-value">{{ selectedDevice.frequency }}</span></div>
-          <div class="device-info-item"><span class="info-label">地址</span><span class="info-value">{{ selectedDevice.address }}</span></div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup name="DrainageHome">
-import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
-import * as echarts from 'echarts'
-import locationIcon from '@/assets/images/location.png'
-import { getEquipmentPage, getWaterloggingPage, getPartitionPage, getEquipmentById, getAlarmDataPage } from '@/api/drainage'
-import { getAlarmPage } from '@/api/pipeNetwork/hazard'
-import { getGisPipeLines } from '@/api/pipeNetwork/basic'
-import { convertCoord } from '@/utils/coordTransform'
-
-const BAIDU_MAP_AK = 'mvSNSN4CPN6WJs2Xi6sQFNmWwCLRgU6T'
-const MAP_STYLE_ID = '7a6c3490bbc3d1e3e9d8602530301e10'
-
-// ==================== 公共:实时时间 ====================
-const currentDateTime = ref('')
-const refreshTime = ref('')
-let timer = null
-function updateTime() {
-  const now = new Date()
-  const pad = n => String(n).padStart(2, '0')
-  const weekDays = ['日', '一', '二', '三', '四', '五', '六']
-  currentDateTime.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())} 星期${weekDays[now.getDay()]}`
-}
-function updateRefreshTime() {
-  const now = new Date()
-  const pad = n => String(n).padStart(2, '0')
-  refreshTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
-}
-
-// ==================== 公共:Tab 切换 ====================
-const tabs = [
-  { label: '雨水污水', value: 0 },
-  { label: '设备管理', value: 1 },
-  { label: '积水管理', value: 2 }
-]
-const activeTab = ref(0)
-
-// ==================== 公共:天气 ====================
-const weatherList = ref([])
-function getWeatherIcon(text) {
-  if (!text) return '☀️'
-  const t = String(text)
-  if (t.includes('晴')) return '☀️'
-  if (t.includes('多云')) return '⛅'
-  if (t.includes('阴')) return '☁️'
-  if (t.includes('雨') && t.includes('雪')) return '🌨️'
-  if (t.includes('雪')) return '❄️'
-  if (t.includes('雷')) return '⛈️'
-  if (t.includes('雨')) return '🌧️'
-  if (t.includes('雾') || t.includes('霾')) return '🌫️'
-  return '☁️'
-}
-function jsonpRequest(url) {
-  return new Promise((resolve, reject) => {
-    const callbackName = 'drainageHomeWeatherCallback_' + Date.now()
-    const script = document.createElement('script')
-    script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
-    script.onerror = () => { document.head.removeChild(script); delete window[callbackName]; reject(new Error('天气接口加载失败')) }
-    window[callbackName] = (data) => { document.head.removeChild(script); delete window[callbackName]; resolve(data) }
-    document.head.appendChild(script)
-  })
-}
-async function loadWeatherData() {
-  try {
-    const url = `/baidu-weather/weather/v1/?district_id=431222&data_type=all&ak=${BAIDU_MAP_AK}&output=json`
-    const response = await fetch(url)
-    const res = await response.json()
-    if (res && (res.status === 0 || res.status === '0') && res.result && Array.isArray(res.result.forecasts)) {
-      weatherList.value = res.result.forecasts.slice(0, 3).map(day => {
-        const dateStr = day.date || ''
-        const monthDay = dateStr.length >= 10 ? `${dateStr.slice(5, 7)}/${dateStr.slice(8, 10)}` : dateStr
-        return { date: monthDay, text: day.text_day || day.text_night || '多云', desc: day.text_day || '多云', tempRange: `${day.low || '--'}°C ~ ${day.high || '--'}°C`, wind: `${day.wd_day || ''} ${day.wc_day || ''}` }
-      })
-    } else { useMockWeather() }
-  } catch (e) { console.error('加载天气数据失败:', e); useMockWeather() }
-}
-function useMockWeather() {
-  const now = new Date(); const pad = n => String(n).padStart(2, '0')
-  const formatDate = (offset) => { const d = new Date(now.getTime() + offset * 24 * 60 * 60 * 1000); return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}` }
-  weatherList.value = [
-    { date: formatDate(0), text: '多云', desc: '多云', tempRange: '18°C ~ 26°C', wind: '东南风 2级' },
-    { date: formatDate(1), text: '小雨', desc: '小雨', tempRange: '16°C ~ 23°C', wind: '东风 3级' },
-    { date: formatDate(2), text: '阴', desc: '阴', tempRange: '15°C ~ 22°C', wind: '北风 2级' }
-  ]
-}
-
-// ==================== Tab 1: 雨水污水 ====================
-const rwOverview = ref({ total: 0, online: 0, offline: 0 })
-const rwOnlineRate = computed(() => {
-  const total = Number(rwOverview.value.total || 0)
-  return total ? Math.round((Number(rwOverview.value.online || 0) / total) * 100) : 0
-})
-const rwRealtimeData = ref([])
-const rwWarnSummary = ref({ online: 0, offline: 0 })
-const rwWarnData = ref([])
-const rwMapPoints = ref([])
-const drainagePipelines = ref([])
-const pipelineMapError = ref('')
-
-// ==================== Tab 2: 设备管理 ====================
-const deviceOverview = ref({ total: 0, online: 0, offline: 0 })
-const deviceOnlineRate = computed(() => {
-  const total = Number(deviceOverview.value.total || 0)
-  return total ? Math.round((Number(deviceOverview.value.online || 0) / total) * 100) : 0
-})
-const devicePointData = ref([])
-const deviceList = ref([])
-const deviceWarnData = ref([])
-const alarmStat = ref({ general: 0, severe: 0 })
-const deviceMapPoints = ref([])
-const deviceModalVisible = ref(false)
-const selectedDevice = ref({ code: '', name: '', installDate: '', status: '', statusClass: '', frequency: '', address: '' })
-
-// ==================== Tab 3: 积水管理 ====================
-const waterOverview = ref({ online: 98, offline: 12 })
-const waterOnlineRate = computed(() => {
-  const online = Number(waterOverview.value.online || 0)
-  const offline = Number(waterOverview.value.offline || 0)
-  const total = online + offline
-  return total ? Math.round((online / total) * 100) : 0
-})
-const waterLevelCount = ref({ none: 45, light: 32, moderate: 18, severe: 15 })
-const waterRealtimeData = ref([])
-const waterWarnSummary = ref({ water: 8, online: 98, offline: 12 })
-const waterWarnData = ref([])
-const waterMapPoints = ref([])
-const analysisYear = ref(new Date().getFullYear())
-const analysisYears = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
-const analysisLevel = ref('all')
-const analysisLevels = [{ value: 'all', label: '全部等级' }, { value: 'light', label: '轻度' }, { value: 'moderate', label: '中度' }, { value: 'severe', label: '严重' }]
-const waterAnalysisResult = ref('点击“开始分析”生成积水趋势分析结果。')
-const waterStatType = ref('month')
-
-// ==================== 地图实例 ====================
-const rwMap = ref(null), deviceMap = ref(null), waterMap = ref(null)
-let bmapInitTimer = null
-
-function initBMapGL(containerId, mapRef, points, clickHandler) {
-  const container = document.getElementById(containerId)
-  if (!container || typeof BMapGL === 'undefined') return false
-  try {
-    const map = new BMapGL.Map(containerId)
-    map.enableScrollWheelZoom(true)
-    map.setMapStyleV2({ styleId: MAP_STYLE_ID })
-    mapRef.value = map
-    // 首页中间的雨水污水地图展示 pipe_network_base 中的排水管网;
-    // 设备、积水页签仍只展示各自的业务点位。
-    const viewPoints = containerId === 'rwMap' ? drawDrainagePipelines(map) : []
-    if (points.length > 0) {
-      points.forEach(p => addLabelMarker(map, p, clickHandler))
-      viewPoints.push(...points.map(p => {
-        const [lng, lat] = convertCoord(Number(p.lng), Number(p.lat))
-        return new BMapGL.Point(lng, lat)
-      }))
-    }
-    if (viewPoints.length > 0) {
-      map.setViewport(viewPoints)
-      if (containerId !== 'rwMap' && map.getZoom() < 13) map.setZoom(13)
-    } else {
-      map.centerAndZoom(new BMapGL.Point(110.393, 28.452), 14)
-    }
-    return true
-  } catch (e) {
-    console.error('百度地图初始化失败:', e)
-    if (containerId === 'rwMap') pipelineMapError.value = '排水管网地图初始化失败,请刷新页面重试。'
-    return false
-  }
-}
-
-function escapeMapText(value) {
-  return String(value ?? '')
-    .replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/"/g, '&quot;')
-    .replace(/'/g, '&#39;')
-}
-
-function getPipelineColor(level) {
-  const text = String(level || '').toLowerCase()
-  if (text.includes('主') || text.includes('一级') || text.includes('main')) return '#00e5ff'
-  if (text.includes('支') || text.includes('二级') || text.includes('branch')) return '#409eff'
-  return '#67c23a'
-}
-
-function drawDrainagePipelines(map) {
-  const viewportPoints = []
-  drainagePipelines.value.forEach(line => {
-    if (!Array.isArray(line.points) || line.points.length < 2) return
-    const bmapPoints = line.points.map(point => {
-      const [lng, lat] = convertCoord(point.lng, point.lat)
-      return new BMapGL.Point(lng, lat)
-    })
-    const polyline = new BMapGL.Polyline(bmapPoints, {
-      strokeColor: getPipelineColor(line.level),
-      strokeWeight: 5,
-      strokeOpacity: 0.9
-    })
-    polyline.addEventListener('click', event => {
-      event?.domEvent?.stopPropagation?.()
-      const content = `<div style="min-width:220px;color:#303133;line-height:1.8">
-        <div style="font-size:15px;font-weight:700;color:#1677ff;margin-bottom:6px">${escapeMapText(line.name || '排水管网')}</div>
-        <div>设施编号:${escapeMapText(line.code || '-')}</div>
-        <div>管网等级:${escapeMapText(line.level || '-')}</div>
-        <div>管径:${escapeMapText(line.diameter ?? '-')} mm</div>
-        <div>长度:${escapeMapText(line.length ?? '-')} m</div>
-        <div>材质:${escapeMapText(line.material || '-')}</div>
-        <div>位置:${escapeMapText(line.location || '-')}</div>
-      </div>`
-      map.openInfoWindow(new BMapGL.InfoWindow(content, { width: 280, enableMessage: false }), bmapPoints[0])
-    })
-    map.addOverlay(polyline)
-    viewportPoints.push(...bmapPoints)
-  })
-  return viewportPoints
-}
-
-function addLabelMarker(map, point, clickHandler) {
-  const [lng, lat] = convertCoord(Number(point.lng), Number(point.lat))
-  const bPoint = new BMapGL.Point(lng, lat)
-  const color = point.color || '#409eff'
-  const icon = point.icon || locationIcon
-  const html = `<div style="text-align:center;cursor:pointer;">
-    <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.name}</div>
-    ${point.subText ? `<div style="color:#fff;font-size:10px;background:rgba(0,0,0,0.6);border-radius:3px;padding:1px 4px;margin-bottom:2px;">${point.subText}</div>` : ''}
-    <div><img src="${icon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
-  </div>`
-  const label = new BMapGL.Label(html, { position: bPoint, offset: new BMapGL.Size(-30, -55) })
-  label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
-  map.addOverlay(label)
-  if (clickHandler) {
-    label.addEventListener('click', () => clickHandler(point))
-  }
-}
-
-// ==================== ECharts 实例 ====================
-const chartInstances = {}
-
-function initChart(domId, option) {
-  const dom = document.getElementById(domId)
-  if (!dom) return null
-  if (chartInstances[domId]) chartInstances[domId].dispose()
-  chartInstances[domId] = echarts.init(dom)
-  chartInstances[domId].setOption(option)
-  return chartInstances[domId]
-}
-
-function getPieOption(rate) {
-  return {
-    tooltip: { trigger: 'item', formatter: '{b}: {d}%' },
-    graphic: [
-      { type: 'text', left: 'center', top: '35%', style: { text: '在线率', fill: '#a0cfff', fontSize: 11, textAlign: 'center' } },
-      { type: 'text', left: 'center', top: '50%', style: { text: `${rate}%`, fill: '#fff', fontSize: 18, fontWeight: 'bold', textAlign: 'center' } }
-    ],
-    series: [{
-      type: 'pie', radius: ['40%', '65%'], center: ['50%', '50%'], label: { show: false }, labelLine: { show: false },
-      data: [
-        { name: '在线', value: rate, itemStyle: { color: '#67c23a' } },
-        { name: '离线', value: 100 - rate, itemStyle: { color: '#f56c6c' } }
-      ],
-      emphasis: { scale: true, scaleSize: 5 }
-    }]
-  }
-}
-
-function getBarOption(xData, yData, name = '数值', color = '#409eff') {
-  return {
-    tooltip: { trigger: 'axis' },
-    grid: { left: 40, right: 10, top: 10, bottom: 25 },
-    xAxis: { type: 'category', data: xData, axisLabel: { color: '#7ec8e3', fontSize: 9 }, axisLine: { lineStyle: { color: '#1a3a5c' } } },
-    yAxis: { type: 'value', name, nameTextStyle: { color: '#7ec8e3', fontSize: 10 }, axisLabel: { color: '#7ec8e3', fontSize: 10 }, splitLine: { lineStyle: { color: '#1a3a5c' } }, axisLine: { show: false } },
-    series: [{ type: 'bar', data: yData, barWidth: 10, itemStyle: { color, borderRadius: [2, 2, 0, 0] } }]
-  }
-}
-
-// ==================== 数据加载(mock + API 占位) ====================
-async function loadAllData() {
-  await Promise.all([
-    loadDrainagePipelineData(),
-    loadRwTabData(),
-    loadEquipmentData(),
-    loadWaterloggingData()
-  ])
-}
-
-async function loadDrainagePipelineData() {
-  pipelineMapError.value = ''
-  try {
-    const response = await getGisPipeLines({ networkType: '1' })
-    const features = Array.isArray(response?.data?.features) ? response.data.features : []
-    drainagePipelines.value = features.map(feature => {
-      const properties = feature?.properties || {}
-      const coordinates = feature?.geometry?.type === 'LineString' ? feature.geometry.coordinates : []
-      const points = coordinates
-        .map(item => ({ lng: Number(item?.[0]), lat: Number(item?.[1]) }))
-        .filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
-      return {
-        id: properties.networkId,
-        code: properties.networkCode,
-        name: properties.networkName,
-        level: properties.networkLevel,
-        diameter: properties.diameter,
-        length: properties.length,
-        material: properties.material,
-        location: properties.location,
-        points
-      }
-    }).filter(line => line.points.length >= 2)
-    if (!drainagePipelines.value.length) {
-      pipelineMapError.value = '暂无带完整起止坐标的排水管网数据'
-    }
-  } catch (error) {
-    drainagePipelines.value = []
-    pipelineMapError.value = '排水管网数据加载失败,请检查后端服务。'
-    console.error('加载排水管网 GIS 线路失败:', error)
-  }
-}
-
-async function loadEquipmentData() {
-  try {
-    const eqPageRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
-    const eqPageData = eqPageRes.code !== undefined ? eqPageRes.data : eqPageRes
-    const allEquipments = eqPageData?.records || eqPageData?.rows || []
-    console.log('[设备管理调试] 设备列表总数:', allEquipments.length)
-    if (allEquipments.length > 0) console.log('[设备管理调试] 设备示例:', JSON.stringify(allEquipments[0]))
-
-    // 统计在线/离线
-    const online = allEquipments.filter(e => e.onlineStatus === 1).length
-    const offline = allEquipments.filter(e => e.onlineStatus !== 1).length
-    deviceOverview.value = { total: allEquipments.length, online, offline }
-
-    // 设备列表
-    deviceList.value = allEquipments.map(e => ({
-      name: e.equipmentName || '',
-      code: e.equipmentCode || '',
-      location: e.equipmentLocation || '',
-      status: e.onlineStatus === 1 ? '在线' : '离线',
-      statusClass: e.onlineStatus === 1 ? 'none' : 'severe'
-    }))
-
-    // 点位数据
-    devicePointData.value = allEquipments.map(e => ({
-      name: e.equipmentName || '',
-      time: e.createTime || '',
-      location: e.equipmentLocation || '',
-      status: e.onlineStatus === 1 ? '在线' : '离线',
-      statusClass: e.onlineStatus === 1 ? 'none' : 'severe'
-    }))
-
-    // 地图标记
-    deviceMapPoints.value = allEquipments
-      .filter(e => e.longitude && e.latitude)
-      .map(e => ({
-        name: e.equipmentName || '',
-        lng: Number(e.longitude),
-        lat: Number(e.latitude),
-        color: e.onlineStatus === 1 ? '#67c23a' : '#f56c6c',
-        icon: locationIcon,
-        subText: e.onlineStatus === 1 ? '在线' : '离线'
-      }))
-
-    // 查询alarm_data报警数据(只查排水设备)
-    try {
-      const alarmDataRes = await getAlarmDataPage(1, 50, { deviceType: 'drainage' })
-      const alarmPageData = alarmDataRes.code !== undefined ? alarmDataRes.data : alarmDataRes
-      const alarmDataList = alarmPageData?.records || alarmPageData?.rows || []
-      console.log('[大屏调试] alarm_data返回:', alarmDataList.length, alarmDataList.length > 0 ? JSON.stringify(alarmDataList[0]) : '')
-      deviceWarnData.value = alarmDataList.slice(0, 10).map(a => {
-        const raw = a.alarmLevel != null ? a.alarmLevel : (a.alarm_level != null ? a.alarm_level : null)
-        // alarmLevel: 1-最高 4-最低(字典编码字符串),统一转数值后比较
-        const lvl = raw == null || raw === '' ? null : Number(raw)
-        let levelText = '--'
-        let levelClass = 'yellow'
-        if (lvl != null && Number.isFinite(lvl) && lvl > 0) {
-          levelText = lvl === 1 ? '一级' : lvl === 2 ? '二级' : lvl === 3 ? '三级' : '四级'
-          levelClass = lvl <= 2 ? 'red' : lvl === 3 ? 'orange' : 'yellow'
-        } else {
-          levelText = '预警'
-          levelClass = 'orange'
-        }
-        return {
-          level: lvl != null && Number.isFinite(lvl) ? lvl : 3,
-          levelText,
-          levelClass,
-          code: a.deviceCode || a.device_code || '',
-          time: a.createTime || '',
-          content: a.warningType || a.warning_type || a.remark || ''
-        }
-      })
-      // 报警统计(等级为字典编码字符串,需转数值后比较)
-      const levelOf = a => {
-        const raw = a.alarmLevel != null ? a.alarmLevel : a.alarm_level
-        const n = raw == null || raw === '' ? NaN : Number(raw)
-        return Number.isFinite(n) ? n : 0
-      }
-      const general = alarmDataList.filter(a => levelOf(a) >= 3).length
-      const severe = alarmDataList.filter(a => levelOf(a) >= 1 && levelOf(a) <= 2).length
-      alarmStat.value = { general, severe }
-    } catch (e) {
-      deviceWarnData.value = []
-    }
-  } catch (e) { console.error('加载设备数据失败:', e) }
-}
-
-async function loadWaterloggingData() {
-  try {
-    const res = await getWaterloggingPage(1, 999, {})
-    const pageData = res.code !== undefined ? res.data : res
-    const list = pageData?.records || pageData?.rows || []
-    console.log('[大屏调试] 积水数据:', list.length, list.length > 0 ? JSON.stringify(list[0]) : '')
-
-    // 积水等级判断(用waterloggingLevel字段:0=未积水, 1=轻度, 2=中度, 3=严重)
-    const levelMap = [
-      { label: '未积水', cls: 'none', color: '#67c23a' },
-      { label: '轻度积水', cls: 'light', color: '#409eff' },
-      { label: '中度积水', cls: 'moderate', color: '#e6a23c' },
-      { label: '严重积水', cls: 'severe', color: '#f56c6c' }
-    ]
-    const getLevelInfo = (item) => {
-      const lvl = Number(item.waterloggingLevel || 0)
-      return levelMap[lvl] || levelMap[0]
-    }
-
-    // 地图点位(易积水点为单点要素,直接用经纬度)
-    const allMapPoints = []
-    list.forEach(item => {
-      const lng = Number(item.longitude)
-      const lat = Number(item.latitude)
-      if (!Number.isFinite(lng) || !Number.isFinite(lat) || (lng === 0 && lat === 0)) return
-      const lv = getLevelInfo(item)
-      allMapPoints.push({
-        name: item.pointName || '积水点',
-        lng,
-        lat,
-        color: lv.color,
-        subText: `${lv.label}`,
-        depth: item.waterVolume || item.waterDepth || 0,
-        level: lv.label,
-        levelClass: lv.cls
-      })
-    })
-    waterMapPoints.value = allMapPoints
-    console.log('[大屏调试] 积水地图点位数:', allMapPoints.length)
-
-    // 积水等级统计
-    let none = 0, light = 0, moderate = 0, severe = 0
-    list.forEach(p => {
-      const lv = getLevelInfo(p)
-      if (lv.cls === 'none') none++
-      else if (lv.cls === 'light') light++
-      else if (lv.cls === 'moderate') moderate++
-      else severe++
-    })
-    waterLevelCount.value = { none, light, moderate, severe }
-
-    // 实时数据表格
-    waterRealtimeData.value = list.slice(0, 12).map((p, i) => {
-      const lv = getLevelInfo(p)
-      return {
-        name: p.pointName || `积水点-${i + 1}`,
-        time: p.createTime || p.updateTime || '',
-        depth: p.waterVolume || p.waterDepth || p.depth || 0,
-        level: lv.label,
-        levelClass: lv.cls
-      }
-    })
-
-    // 设备在线统计 - 取易积水点关联的摄像头
-    const allEqIds = []
-    list.forEach(p => {
-      if (p.cameraIds) {
-        p.cameraIds.split(',').forEach(id => {
-          const trimmed = id.trim()
-          if (trimmed) allEqIds.push(trimmed)
-        })
-      }
-    })
-    const uniqueEqIds = [...new Set(allEqIds)]
-
-    let online = 0, offline = 0
-    const linkedDeviceCodes = new Set()
-    if (uniqueEqIds.length > 0) {
-      try {
-        const eqRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
-        const eqData = eqRes.code !== undefined ? eqRes.data : eqRes
-        const allEqs = eqData?.records || eqData?.rows || []
-        const eqMap = {}
-        allEqs.forEach(eq => { eqMap[eq.equipmentId] = eq })
-        uniqueEqIds.forEach(id => {
-          const eq = eqMap[id]
-          if (eq) {
-            if (eq.onlineStatus === 1) online++
-            else offline++
-            if (eq.equipmentCode) linkedDeviceCodes.add(eq.equipmentCode)
-          } else { offline++ }
-        })
-      } catch (e) {
-        online = uniqueEqIds.length
-      }
-    }
-    waterOverview.value = { online, offline }
-
-    // 报警信息 - 根据关联设备编码查alarm_data表
-    console.log('[大屏调试] 积水关联设备编码:', [...linkedDeviceCodes])
-    try {
-      const alarmRes = await getAlarmDataPage(1, 50, { deviceType: 'drainage' })
-      const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-      const alarmList = alarmPageData?.records || alarmPageData?.rows || []
-      // 只保留关联设备编码的报警
-      const filteredAlarms = linkedDeviceCodes.size > 0
-        ? alarmList.filter(a => linkedDeviceCodes.has(a.deviceCode))
-        : []
-      waterWarnData.value = filteredAlarms.slice(0, 10).map(a => ({
-        addr: a.deviceCode || '',
-        time: a.createTime || '',
-        type: a.warningType || a.warning_type || '积水报警',
-        value: a.actualValue != null ? `${a.actualValue}cm` : '--'
-      }))
-      waterWarnSummary.value = { water: filteredAlarms.length, online, offline }
-    } catch (e) {
-      waterWarnData.value = []
-      waterWarnSummary.value = { water: 0, online, offline }
-    }
-
-  } catch (e) {
-    console.error('加载积水数据失败:', e)
-  }
-}
-
-async function loadRwTabData() {
-  try {
-    // 1. 获取所有排水分区数据
-    const partRes = await getPartitionPage(1, 999, {})
-    // 兼容 AjaxResult包装 和 直接返回Page对象 两种格式
-    const pageData = partRes.code !== undefined ? partRes.data : partRes
-    const partitionList = pageData?.records || pageData?.rows || []
-    console.log('[大屏调试] 分区数据条数:', partitionList.length)
-    if (partitionList.length > 0) console.log('[大屏调试] 分区示例:', JSON.stringify(partitionList[0]))
-
-    // 2. 收集关联设备ID并查询设备状态
-    const allEquipmentIds = []
-    partitionList.forEach(p => {
-      if (p.equipmentId) {
-        p.equipmentId.split(',').forEach(id => {
-          const trimmed = id.trim()
-          if (trimmed) allEquipmentIds.push(trimmed)
-        })
-      }
-    })
-    const uniqueIds = [...new Set(allEquipmentIds)]
-
-    // 查询所有排水设备,通过ID匹配统计在线/离线
-    let online = 0, offline = 0
-    const equipmentDetails = {}
-    try {
-      const eqPageRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
-      const eqPageData = eqPageRes.code !== undefined ? eqPageRes.data : eqPageRes
-      const allEquipments = eqPageData?.records || eqPageData?.rows || []
-      console.log('[大屏调试] 设备列表总数:', allEquipments.length, '分区关联设备ID:', uniqueIds)
-      if (allEquipments.length > 0) console.log('[大屏调试] 设备示例:', JSON.stringify(allEquipments[0]))
-      // 建立设备ID映射(设备主键字段是equipmentId)
-      const eqMap = {}
-      allEquipments.forEach(eq => { eqMap[eq.equipmentId] = eq })
-      console.log('[大屏调试] 设备ID映射keys:', Object.keys(eqMap).slice(0, 5))
-      // 统计关联设备状态
-      for (const id of uniqueIds) {
-        const eq = eqMap[id]
-        if (eq) {
-          console.log(`[大屏调试] 设备${id}匹配成功, onlineStatus=`, eq.onlineStatus)
-          if (eq.onlineStatus === 1) online++
-          else offline++
-          equipmentDetails[id] = { name: eq.equipmentName || '', code: eq.equipmentCode || '', typeName: eq.equipmentTypeName || '', isOnline: eq.onlineStatus === 1 }
-        } else {
-          console.log(`[大屏调试] 设备${id}在列表中未找到`)
-          offline++
-        }
-      }
-    } catch (e) {
-      console.error('[大屏调试] getEquipmentPage失败:', e)
-      // 如果分页查询失败,退回逐个查询
-      for (const id of uniqueIds) {
-        try {
-          const eqRes = await getEquipmentById(id)
-          const data = eqRes.code !== undefined ? eqRes.data : eqRes
-          console.log(`[大屏调试] getEquipmentById(${id}) 返回:`, JSON.stringify(data))
-          const info = data?.equipmentInfo || data
-          if (info?.onlineStatus === 1) online++
-          else offline++
-          equipmentDetails[id] = { name: info?.equipmentName || '', code: info?.equipmentCode || '', typeName: info?.equipmentTypeName || '', isOnline: info?.onlineStatus === 1 }
-        } catch (e2) { offline++ }
-      }
-    }
-    const total = uniqueIds.length
-    rwOverview.value = { total, online, offline }
-    rwWarnSummary.value = { online, offline }
-
-    // 3. 地图标记:使用排水分区的坐标
-    rwMapPoints.value = partitionList
-      .filter(p => p.longitude && p.latitude)
-      .map(p => ({
-        name: p.name || p.partitionName || '排水分区',
-        lng: Number(p.longitude),
-        lat: Number(p.latitude),
-        color: p.equipmentId ? '#67c23a' : '#409eff',
-        subText: p.partitionType || ''
-      }))
-
-    // 4. 实时数据表格:使用分区的真实排水量/积水量
-    rwRealtimeData.value = partitionList.map((p, i) => ({
-      name: p.name || p.partitionName || `监测点-${i + 1}`,
-      time: p.time || p.dataTime || p.createTime || '',
-      waterlogging: p.waterlogging != null ? p.waterlogging : '--',
-      drainage: p.drainage != null ? p.drainage : '--'
-    }))
-
-    // 5. 报警设备列表(只显示分区关联设备的报警)
-    try {
-      const alarmRes = await getAlarmPage(1, 100, { equipmentTypeId: 'drainage' })
-      const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
-      const alarmList = alarmPageData?.records || alarmPageData?.rows || []
-      // 只保留关联设备的报警记录
-      const linkedEquipIds = new Set(uniqueIds)
-      const linkedEquipCodes = new Set(Object.values(equipmentDetails).map(eq => eq.code).filter(Boolean))
-      const filteredAlarms = alarmList.filter(a => {
-        return linkedEquipIds.has(a.equipmentId) || linkedEquipCodes.has(a.equipmentCode || a.deviceCode)
-      })
-      rwWarnData.value = filteredAlarms.slice(0, 10).map(a => {
-        const code = a.equipmentCode || a.deviceCode || a.code || ''
-        const codeNameMap = {}
-        Object.values(equipmentDetails).forEach(eq => { if (eq.code) codeNameMap[eq.code] = eq.name })
-        return {
-          deviceName: a.equipmentName || a.deviceName || a.name || codeNameMap[code] || '',
-          time: a.createTime || '',
-          deviceCode: code,
-          deviceType: a.equipmentTypeName || a.deviceType || '排水设备'
-        }
-      })
-    } catch (e) {
-      rwWarnData.value = []
-    }
-  } catch (e) {
-    console.error('加载雨水污水Tab数据失败:', e)
-  }
-}
-
-
-// ==================== Tab 图表初始化 ====================
-function initTab1Charts() {
-  initChart('rwPieChart', getPieOption(rwOnlineRate.value))
-  // 近日排水量/积水量 - 用分区真实数据作柱状图
-  const names = rwRealtimeData.value.map(r => r.name?.substring(0, 4) || '')
-  const drainVals = rwRealtimeData.value.map(r => Number(r.drainage) || 0)
-  const waterVals = rwRealtimeData.value.map(r => Number(r.waterlogging) || 0)
-  initChart('rwDrainageBarChart', getBarOption(names, drainVals, '', '#409eff'))
-  initChart('rwWaterlogBarChart', getBarOption(names, waterVals, '', '#e6a23c'))
-}
-
-function initTab2Charts() {
-  initChart('devicePieChart', getPieOption(deviceOnlineRate.value))
-  const months = ['1月', '2月', '3月', '4月', '5月', '6月']
-  initChart('deviceRepairChart', getBarOption(months, months.map(() => Math.floor(Math.random() * 20)), '次', '#e6a23c'))
-}
-
-function initTab3Charts() {
-  initChart('waterPieChart', getPieOption(waterOnlineRate.value))
-  updateWaterStatChart()
-}
-
-async function updateWaterStatChart() {
-  // 查询积水数据,按时间统计
-  try {
-    const res = await getWaterloggingPage(1, 9999, {})
-    const pageData = res.code !== undefined ? res.data : res
-    const allList = pageData?.records || pageData?.rows || []
-    let xData = [], yData = []
-    const now = new Date()
-    if (waterStatType.value === 'month') {
-      // 近6个月统计
-      xData = []
-      yData = []
-      for (let i = 5; i >= 0; i--) {
-        const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
-        const month = d.getMonth() + 1
-        const year = d.getFullYear()
-        xData.push(`${month}月`)
-        const count = allList.filter(item => {
-          const t = item.createTime || ''
-          if (!t) return false
-          const itemDate = new Date(t)
-          return itemDate.getFullYear() === year && itemDate.getMonth() + 1 === month
-        }).length
-        yData.push(count)
-      }
-    } else {
-      // 近5年统计
-      const currentYear = now.getFullYear()
-      for (let i = 4; i >= 0; i--) {
-        const y = currentYear - i
-        xData.push(String(y))
-        const count = allList.filter(item => {
-          const t = item.createTime || ''
-          if (!t) return false
-          return new Date(t).getFullYear() === y
-        }).length
-        yData.push(count)
-      }
-    }
-    initChart('waterStatChart', getBarOption(xData, yData, '次', '#00e5ff'))
-  } catch (e) {
-    console.error('积水统计加载失败:', e)
-    initChart('waterStatChart', getBarOption([], [], '次', '#00e5ff'))
-  }
-}
-
-async function doWaterAnalysis() {
-  waterAnalysisResult.value = '正在查询...'
-  try {
-    const res = await getWaterloggingPage(1, 9999, {})
-    const pageData = res.code !== undefined ? res.data : res
-    const allList = pageData?.records || pageData?.rows || []
-    const year = analysisYear.value
-    const level = analysisLevel.value
-    // 按年份过滤
-    let filtered = allList.filter(item => {
-      const t = item.createTime || ''
-      if (!t) return false
-      return new Date(t).getFullYear() === year
-    })
-    // 按等级过滤
-    if (level !== 'all') {
-      const getLevelCls = (item) => {
-        const depth = Number(item.waterDepth || item.depth || item.waterlogging || 0)
-        if (depth <= 0) return 'none'
-        if (depth <= 15) return 'light'
-        if (depth <= 30) return 'moderate'
-        return 'severe'
-      }
-      filtered = filtered.filter(item => getLevelCls(item) === level)
-    }
-    const count = filtered.length
-    const levelText = analysisLevels.find(l => l.value === level)?.label || '全部等级'
-    waterAnalysisResult.value = `${year}年${levelText}积水事件共 ${count} 起。`
-  } catch (e) {
-    waterAnalysisResult.value = '查询失败,请稍后重试。'
-    console.error('积水分析查询失败:', e)
-  }
-}
-
-watch(waterStatType, () => nextTick(updateWaterStatChart))
-
-// ==================== Tab 切换与地图初始化 ====================
-function initTabMaps() {
-  initBMapGL('rwMap', rwMap, rwMapPoints.value, null)
-  initBMapGL('deviceMap', deviceMap, deviceMapPoints.value, (point) => { selectedDevice.value = point; deviceModalVisible.value = true })
-  initBMapGL('waterMap', waterMap, waterMapPoints.value, null)
-}
-
-function waitForBMapAndInit() {
-  let attempts = 0
-  const maxAttempts = 100
-  const tryInit = () => {
-    attempts += 1
-    if (typeof BMapGL !== 'undefined') {
-      initTabMaps()
-      return
-    }
-    if (attempts >= maxAttempts) {
-      pipelineMapError.value = '百度地图加载超时,请检查网络或地图配置。'
-      return
-    }
-    bmapInitTimer = window.setTimeout(tryInit, 200)
-  }
-  tryInit()
-}
-
-function switchTab(tab) {
-  activeTab.value = tab
-  nextTick(() => {
-    Object.values(chartInstances).forEach(c => c && c.resize())
-    if (tab === 0 && !rwMap.value) initBMapGL('rwMap', rwMap, rwMapPoints.value, null)
-    if (tab === 1 && !deviceMap.value) initBMapGL('deviceMap', deviceMap, deviceMapPoints.value, (point) => { selectedDevice.value = point; deviceModalVisible.value = true })
-    if (tab === 2 && !waterMap.value) initBMapGL('waterMap', waterMap, waterMapPoints.value, null)
-
-  })
-}
-
-function closeDeviceModal() { deviceModalVisible.value = false }
-
-// ==================== 生命周期 ====================
-onMounted(async () => {
-  updateTime()
-  timer = setInterval(() => { updateTime(); updateRefreshTime() }, 1000)
-  updateRefreshTime()
-  await Promise.all([loadAllData(), loadWeatherData()])
-  setTimeout(() => {
-    waitForBMapAndInit()
-    initTab1Charts()
-    initTab2Charts()
-    initTab3Charts()
-  }, 300)
-})
-
-onUnmounted(() => {
-  if (timer) clearInterval(timer)
-  if (bmapInitTimer) window.clearTimeout(bmapInitTimer)
-  Object.values(chartInstances).forEach(c => c && c.dispose())
-})
-
-window.addEventListener('resize', () => {
-  Object.values(chartInstances).forEach(c => c && c.resize())
-})
-</script>
-
-<style scoped>
-/* ==================== 全局 ==================== */
-.drainage-home-screen { width: 100%; height: 100vh; background: #0a1929; color: #fff; font-family: 'Microsoft YaHei', sans-serif; overflow: hidden; display: flex; flex-direction: column; position: relative; }
-
-/* ==================== 顶部 ==================== */
-.screen-header { height: 64px; background: linear-gradient(180deg, #0d2137 0%, #0a1929 100%); border-bottom: 1px solid rgba(64, 158, 255, 0.3); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; position: relative; }
-.screen-header::after { content: ''; position: absolute; bottom: 0; left: 10%; right: 10%; height: 1px; background: linear-gradient(90deg, transparent, #409eff, transparent); }
-.header-left { flex: 1; text-align: left; }
-.header-time { font-size: 15px; color: #a0cfff; letter-spacing: 1px; }
-.header-center { flex: 2; text-align: center; }
-.header-title { font-size: 26px; font-weight: 700; margin: 0; letter-spacing: 4px; background: linear-gradient(180deg, #fff 0%, #7ec8e3 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
-.header-right { flex: 1; text-align: right; }
-
-/* ==================== 主体三栏 ==================== */
-.screen-body { flex: 1; display: flex; padding: 12px 12px 90px 12px; gap: 12px; min-height: 0; }
-.panel-left, .panel-right { width: 22%; min-width: 260px; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
-.panel-center { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
-
-/* ==================== 面板盒子 ==================== */
-.panel-box { background: rgba(6, 30, 65, 0.8); border: 1px solid rgba(64, 158, 255, 0.2); border-radius: 6px; padding: 12px; display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
-.panel-title { font-size: 14px; font-weight: 600; color: #7ec8e3; margin-bottom: 10px; display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
-.title-icon { color: #409eff; font-size: 10px; }
-.refresh-time { margin-left: auto; font-size: 11px; color: #607080; font-weight: 400; }
-
-/* ==================== 天气 ==================== */
-.weather-cards { display: flex; gap: 8px; flex-shrink: 0; }
-.panel-left .panel-box:first-child { flex: none; }
-.panel-left .panel-box:nth-child(2) { flex: none; }
-.weather-card { flex: 1; background: rgba(20, 60, 110, 0.5); border: 1px solid rgba(64, 158, 255, 0.15); border-radius: 6px; padding: 10px 8px; text-align: center; }
-.weather-date { font-size: 12px; color: #a0cfff; margin-bottom: 4px; }
-.weather-icon { font-size: 24px; margin: 4px 0; }
-.weather-desc { font-size: 12px; color: #d0e8ff; margin-bottom: 2px; }
-.weather-temp { font-size: 11px; color: #7ec8e3; }
-.weather-wind { font-size: 10px; color: #607080; margin-top: 2px; }
-
-/* ==================== 概况卡片 ==================== */
-.overview-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 12px; flex-shrink: 0; }
-.overview-card { background: rgba(20, 60, 110, 0.4); border: 1px solid rgba(64, 158, 255, 0.15); border-radius: 4px; padding: 12px 8px; text-align: center; }
-.ov-label { font-size: 12px; color: #a0cfff; }
-.ov-value { font-size: 20px; font-weight: 700; color: #fff; margin-top: 6px; }
-.ov-value.online { color: #67c23a; }
-.ov-value.offline { color: #f56c6c; }
-.ov-value.warn { color: #e6a23c; }
-
-/* ==================== 环形图 ==================== */
-.pie-chart-wrapper { flex: 1; display: flex; align-items: center; min-height: 0; }
-.chart-area { width: 55%; height: 100%; min-height: 120px; }
-.pie-legend { width: 45%; display: flex; flex-direction: column; gap: 8px; }
-.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
-.legend-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
-.legend-label { color: #a0cfff; flex: 1; }
-.legend-value { color: #fff; font-weight: 600; }
-
-/* ==================== 表格 ==================== */
-.realtime-table-wrap { flex: 1; overflow-y: auto; min-height: 0; }
-.dark-table { width: 100%; border-collapse: collapse; font-size: 12px; }
-.dark-table th { background: rgba(20, 60, 110, 0.6); color: #7ec8e3; padding: 7px 6px; text-align: center; border-bottom: 1px solid rgba(64, 158, 255, 0.2); font-weight: 500; white-space: nowrap; }
-.dark-table td { padding: 6px; text-align: center; color: #d0e8ff; border-bottom: 1px solid rgba(64, 158, 255, 0.08); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 80px; }
-.dark-table tbody tr:nth-child(odd) { background: rgba(10, 25, 50, 0.4); }
-.dark-table tbody tr:nth-child(even) { background: rgba(20, 40, 70, 0.3); }
-.dark-table tbody tr:hover { background: rgba(64, 158, 255, 0.1); }
-.level-tag { padding: 1px 6px; border-radius: 3px; font-size: 11px; }
-.level-none { color: #67c23a; background: rgba(103, 194, 58, 0.15); }
-.level-light { color: #409eff; background: rgba(64, 158, 255, 0.15); }
-.level-moderate { color: #e6a23c; background: rgba(230, 162, 60, 0.15); }
-.level-severe { color: #f56c6c; background: rgba(245, 108, 108, 0.15); }
-
-/* ==================== 地图 ==================== */
-.map-container { flex: 1; border-radius: 6px; overflow: hidden; border: 1px solid rgba(64, 158, 255, 0.2); min-height: 0; }
-.pipeline-map-status { position: absolute; top: 12px; left: 12px; z-index: 5; display: flex; align-items: center; gap: 7px; padding: 7px 12px; color: #d9f4ff; background: rgba(6, 30, 65, 0.86); border: 1px solid rgba(0, 229, 255, 0.42); border-radius: 4px; font-size: 12px; pointer-events: none; }
-.pipeline-status-dot { width: 9px; height: 9px; background: #00e5ff; border-radius: 50%; box-shadow: 0 0 8px rgba(0, 229, 255, 0.9); }
-.pipeline-map-error { position: absolute; left: 50%; bottom: 18px; z-index: 5; max-width: 70%; padding: 8px 14px; color: #ffd591; background: rgba(80, 45, 0, 0.88); border: 1px solid rgba(230, 162, 60, 0.55); border-radius: 4px; font-size: 12px; text-align: center; transform: translateX(-50%); pointer-events: none; }
-.chart-area-sm { width: 100%; flex: 1; min-height: 130px; }
-
-/* ==================== 预警卡片 ==================== */
-.warn-summary { display: flex; gap: 8px; margin-bottom: 10px; flex-shrink: 0; }
-.warn-card { flex: 1; background: rgba(20, 60, 110, 0.4); border: 1px solid rgba(64, 158, 255, 0.15); border-radius: 6px; padding: 10px 4px; text-align: center; }
-.warn-icon { font-size: 20px; }
-.warn-label { font-size: 11px; color: #a0cfff; margin-top: 4px; }
-.warn-value { font-size: 16px; font-weight: 700; color: #fff; margin-top: 2px; }
-.warn-value.online { color: #67c23a; }
-.warn-value.offline { color: #f56c6c; }
-
-/* ==================== 警告信息 ==================== */
-.warn-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
-.warn-dot.red { background: #f56c6c; box-shadow: 0 0 6px #f56c6c; }
-.warn-dot.orange { background: #e6a23c; box-shadow: 0 0 6px #e6a23c; }
-.warn-dot.yellow { background: #f2d66c; box-shadow: 0 0 6px #f2d66c; }
-
-/* ==================== 报警统计 ==================== */
-.alarm-stat { display: flex; gap: 12px; justify-content: center; align-items: center; flex: 1; }
-.alarm-stat-item { text-align: center; }
-.alarm-icon { font-size: 28px; }
-.alarm-label { font-size: 12px; color: #a0cfff; margin-top: 4px; }
-.alarm-value { font-size: 20px; font-weight: 700; margin-top: 2px; }
-.alarm-value.yellow { color: #f2d66c; }
-.alarm-value.red { color: #f56c6c; }
-
-/* ==================== 积水等级数量 ==================== */
-.level-counts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; margin-top: 10px; font-size: 12px; color: #a0cfff; flex-shrink: 0; }
-.level-count { display: flex; align-items: center; gap: 6px; }
-.dot { width: 8px; height: 8px; border-radius: 50%; }
-.dot.green { background: #67c23a; }
-.dot.blue { background: #409eff; }
-.dot.orange { background: #e6a23c; }
-.dot.red { background: #f56c6c; }
-
-/* ==================== 积水分析 ==================== */
-.analysis-form { display: flex; flex-direction: column; gap: 10px; }
-.form-row { display: flex; align-items: center; gap: 8px; }
-.form-row label { color: #a0cfff; font-size: 12px; width: 60px; }
-.form-row select { flex: 1; background: rgba(10, 25, 50, 0.6); border: 1px solid rgba(64, 158, 255, 0.3); color: #fff; padding: 5px 8px; border-radius: 4px; font-size: 12px; }
-.analysis-btn { background: rgba(64, 158, 255, 0.2); border: 1px solid rgba(64, 158, 255, 0.5); color: #7ec8e3; padding: 7px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: all 0.3s; }
-.analysis-btn:hover { background: rgba(64, 158, 255, 0.4); color: #fff; }
-.analysis-result { background: rgba(10, 25, 50, 0.5); border: 1px solid rgba(64, 158, 255, 0.15); border-radius: 4px; padding: 8px; font-size: 12px; color: #d0e8ff; line-height: 1.6; min-height: 60px; }
-
-/* ==================== 统计切换 ==================== */
-.stat-tabs { display: flex; gap: 8px; margin-bottom: 8px; flex-shrink: 0; }
-.stat-tabs span { flex: 1; text-align: center; padding: 5px; font-size: 12px; color: #a0cfff; background: rgba(10, 25, 50, 0.5); border: 1px solid rgba(64, 158, 255, 0.2); border-radius: 4px; cursor: pointer; }
-.stat-tabs span.active { background: rgba(64, 158, 255, 0.3); color: #fff; border-color: rgba(64, 158, 255, 0.6); }
-
-/* ==================== 底部 Tab ==================== */
-.bottom-tabs { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 16px; z-index: 100; background: rgba(6, 30, 65, 0.7); padding: 8px 16px; border-radius: 12px; border: 1px solid rgba(64, 158, 255, 0.2); }
-.bottom-tab { padding: 8px 32px; border-radius: 8px; font-size: 14px; font-weight: 600; color: #0a1929; background: #fff; cursor: pointer; transition: all 0.3s; border: 1px solid transparent; }
-.bottom-tab:hover { background: #e6f2ff; }
-.bottom-tab.active { background: #409eff; color: #fff; border-color: #409eff; box-shadow: 0 0 12px rgba(64, 158, 255, 0.5); }
-
-/* ==================== 弹窗 ==================== */
-.device-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; }
-.device-modal-content { width: 420px; max-width: 90vw; background: rgba(6, 30, 65, 0.95); border: 1px solid rgba(64, 158, 255, 0.4); border-radius: 8px; padding: 16px; box-shadow: 0 0 20px rgba(64, 158, 255, 0.2); }
-.device-modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
-.device-modal-title { font-size: 15px; font-weight: 600; color: #7ec8e3; }
-.device-modal-close { font-size: 22px; color: #a0cfff; cursor: pointer; line-height: 1; }
-.device-modal-close:hover { color: #fff; }
-.device-info-list { display: flex; flex-direction: column; gap: 10px; }
-.device-info-item { display: flex; justify-content: space-between; font-size: 13px; padding: 8px; background: rgba(10, 25, 50, 0.5); border-radius: 4px; }
-.info-label { color: #a0cfff; }
-.info-value { color: #fff; }
-
-/* ==================== 滚动条 ==================== */
-::-webkit-scrollbar { width: 4px; }
-::-webkit-scrollbar-track { background: rgba(10, 25, 50, 0.3); }
-::-webkit-scrollbar-thumb { background: rgba(64, 158, 255, 0.3); border-radius: 2px; }
-::-webkit-scrollbar-thumb:hover { background: rgba(64, 158, 255, 0.5); }
-</style>

+ 0 - 1375
src/views/subSystem/gas/ghome/gHome.vue

@@ -1,1375 +0,0 @@
-<script setup>
-import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
-import * as echarts from 'echarts'
-import { Odometer, CircleCheckFilled, VideoCameraFilled, WarnTriangleFilled } from '@element-plus/icons-vue'
-import { wgs84ToBd09 } from '@/utils/coordTransform'
-import { getGasDashboardSummary, getGisPipeLines } from '@/api/pipeNetwork/basic'
-
-const nowText = ref('2025/12/31 12:59:59')
-const weekText = ref('星期三')
-const mapError = ref('')
-const loading = ref(false)
-
-// ========== 仪表盘数据 ==========
-const dashboardData = ref({})
-
-const baseCards = computed(() => {
-  const bs = dashboardData.value.baseStats || {}
-  return [
-    { label: '燃气设备', value: bs.totalEquipments || 0, unit: '台', icon: Odometer, color: '#60b8ff' },
-    { label: '在线设备', value: bs.onlineEquipments || 0, unit: '台', icon: CircleCheckFilled, color: '#52e0a8' },
-    { label: '离线设备', value: bs.offlineEquipments || 0, unit: '台', icon: VideoCameraFilled, color: '#ff8e8e' },
-    { label: '异常设备', value: bs.alarmCount || 0, unit: '台', icon: WarnTriangleFilled, color: '#ffb54d' }
-  ]
-})
-
-const operationSummary = computed(() => {
-  const ps = dashboardData.value.pipeStats || {}
-  return [
-    { label: '管网总长度', value: (ps.totalLength || 0) + 'km' },
-    { label: '燃气管网', value: (ps.gasNetworks || 0) + '条' }
-  ]
-})
-
-const repairRecords = computed(() => {
-  const list = dashboardData.value.repairTrend || []
-  if (list.length > 0) return list
-  return [
-    { month: '1月', value: 0 }, { month: '2月', value: 0 }, { month: '3月', value: 0 },
-    { month: '4月', value: 0 }, { month: '5月', value: 0 }, { month: '6月', value: 0 }
-  ]
-})
-
-const pointRows = computed(() => {
-  const list = dashboardData.value.pointRows || []
-  return list.length > 0 ? list : [{ id: 1, name: '暂无数据', time: '--', value: '--', status: '离线' }]
-})
-
-const equipmentRows = computed(() => {
-  const list = dashboardData.value.equipmentRows || []
-  return list.length > 0 ? list : [{ id: 1, name: '暂无数据', code: '--', location: '--', status: '--' }]
-})
-
-const warningRows = computed(() => {
-  const list = dashboardData.value.warningRows || []
-  return list.length > 0 ? list : [{ level: '中', title: '暂无告警', code: '--', time: '--:--:--', desc: '当前无告警信息', tag: '正常' }]
-})
-
-const hazardSummary = computed(() => {
-  const map = dashboardData.value.hazardStats || {}
-  return [
-    { label: '重大隐患', value: map['重大隐患'] || 0, color: '#ff3030' },
-    { label: '较大隐患', value: map['较大隐患'] || 0, color: '#ff9f1a' },
-    { label: '一般隐患', value: map['一般隐患'] || 0, color: '#f3e64d' }
-  ]
-})
-
-const mapKpis = computed(() => {
-  const bs = dashboardData.value.baseStats || {}
-  const es = dashboardData.value.equipmentStats || {}
-  const onlineRate = bs.onlineRate
-  const avgPressure = es.avgPressure
-  return [
-    { label: '在线率', value: onlineRate != null ? Number(onlineRate).toFixed(1) + '%' : '--' },
-    { label: '压力均值', value: avgPressure != null ? Number(avgPressure).toFixed(2) + 'MPa' : '--' },
-    { label: '今日告警', value: (es.todayAlarm || 0) + '条' }
-  ]
-})
-
-/** 压力最新采集时间,用于说明均值取自哪个时刻 */
-const pressureTimeText = computed(() => {
-  const text = dashboardData.value.pressureUpdatedAt
-  if (!text) return '--'
-  return text.length > 8 ? text.slice(-8) : text
-})
-
-
-const mapWrapRef = ref(null)
-const repairChartRef = ref(null)
-const hazardChartRef = ref(null)
-let mapInstance = null
-let repairChart = null
-let hazardChart = null
-let clockTimer = null
-
-function formatWeek(day) {
-  return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
-}
-
-function pad(value) {
-  return String(value).padStart(2, '0')
-}
-
-function updateClock() {
-  const now = new Date()
-  nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
-  weekText.value = formatWeek(now.getDay())
-}
-
-function initRepairChart() {
-  if (!repairChartRef.value) return
-  // 定时刷新时复用已有实例,避免整块图表重建闪烁
-  if (!repairChart) {
-    repairChart = echarts.init(repairChartRef.value)
-  }
-  const data = repairRecords.value
-  repairChart.setOption({
-    grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
-    xAxis: {
-      type: 'category',
-      data: data.map(item => item.month),
-      axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
-      axisLabel: { color: '#d2efff' }
-    },
-    yAxis: {
-      type: 'value',
-      splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
-      axisLine: { show: false },
-      axisLabel: { color: '#d2efff' }
-    },
-    tooltip: { trigger: 'axis' },
-    series: [{
-      type: 'bar',
-      barWidth: 24,
-      data: data.map(item => item.value),
-      itemStyle: {
-        borderRadius: [6, 6, 0, 0],
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-          { offset: 0, color: '#38d8ff' },
-          { offset: 1, color: '#166dff' }
-        ])
-      }
-    }]
-  })
-}
-
-function initHazardChart() {
-  if (!hazardChartRef.value) return
-  if (!hazardChart) {
-    hazardChart = echarts.init(hazardChartRef.value)
-  }
-  const data = hazardSummary.value
-  hazardChart.setOption({
-    grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
-    xAxis: {
-      type: 'value',
-      splitLine: { show: false },
-      axisLine: { show: false },
-      axisTick: { show: false },
-      axisLabel: { show: false }
-    },
-    yAxis: {
-      type: 'category',
-      data: data.map(item => item.label),
-      axisLine: { show: false },
-      axisTick: { show: false },
-      axisLabel: { color: '#d2efff', fontSize: 13 }
-    },
-    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-    series: [{
-      type: 'bar',
-      data: data.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
-      barWidth: 14,
-      label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
-      showBackground: true,
-      backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
-    }]
-  })
-}
-
-function waitForBMapGL() {
-  return new Promise((resolve, reject) => {
-    if (window.BMapGL) {
-      resolve(window.BMapGL)
-      return
-    }
-    let attempts = 0
-    const timer = setInterval(() => {
-      attempts += 1
-      if (window.BMapGL) {
-        clearInterval(timer)
-        resolve(window.BMapGL)
-        return
-      }
-      if (attempts >= 100) {
-        clearInterval(timer)
-        reject(new Error('百度地图加载超时'))
-      }
-    }, 200)
-  })
-}
-
-/** 管网状态 → 线条颜色,取值见字典 pipe_network_status:0 正常 / 1 维护中 / 2 异常 */
-const PIPE_STATUS_COLOR = { 0: '#ff4d4f', 1: '#ffb020', 2: '#ff1f1f' }
-
-/** 压力点状态 → 标签配色 */
-const PRESSURE_STATUS_STYLE = {
-  normal: { border: 'rgba(82, 224, 168, 0.75)', text: '#d8ffe9' },
-  warning: { border: 'rgba(255, 181, 77, 0.85)', text: '#ffe9c7' },
-  danger: { border: 'rgba(255, 48, 48, 0.9)', text: '#ffd6d6' },
-  offline: { border: 'rgba(150, 170, 190, 0.7)', text: '#cfd8e3' }
-}
-
-/** 地图叠加物,刷新时整体清除后重画 */
-let pipeOverlays = []
-let pressureOverlays = []
-
-function clearOverlays(overlays) {
-  if (mapInstance) {
-    overlays.forEach(overlay => mapInstance.removeOverlay(overlay))
-  }
-  return []
-}
-
-/**
- * 绘制燃气管段
- *
- * 燃气管网每条记录只有起终点坐标,按状态着色,不再逐点画装饰圆(数百条管段会拖慢地图)。
- */
-function drawPipelines(BMapGL, features) {
-  pipeOverlays = clearOverlays(pipeOverlays)
-  features.forEach(feature => {
-    const props = feature.properties || {}
-    const coordinates = feature.geometry && feature.geometry.coordinates
-    if (!Array.isArray(coordinates) || coordinates.length < 2) return
-    const path = coordinates
-      .map(([lng, lat]) => {
-        const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
-        return new BMapGL.Point(bdLng, bdLat)
-      })
-      .filter(point => !Number.isNaN(point.lng) && !Number.isNaN(point.lat))
-    if (path.length < 2) return
-
-    const status = String(props.status == null ? '' : props.status)
-    const line = new BMapGL.Polyline(path, {
-      strokeColor: PIPE_STATUS_COLOR[status] || PIPE_STATUS_COLOR[0],
-      strokeWeight: status === '2' ? 5 : 4,
-      strokeOpacity: 0.92
-    })
-    mapInstance.addOverlay(line)
-    pipeOverlays.push(line)
-  })
-}
-
-/**
- * 绘制压力监测点,标签显示站点名称与最新压力值
- */
-function drawPressurePoints(BMapGL, points) {
-  pressureOverlays = clearOverlays(pressureOverlays)
-  points.forEach(point => {
-    const lng = Number(point.lng)
-    const lat = Number(point.lat)
-    if (!lng || !lat) return
-    const [bdLng, bdLat] = wgs84ToBd09(lng, lat)
-    const position = new BMapGL.Point(bdLng, bdLat)
-
-    const marker = new BMapGL.Marker(position)
-    mapInstance.addOverlay(marker)
-    pressureOverlays.push(marker)
-
-    const style = PRESSURE_STATUS_STYLE[String(point.status || 'normal')] || PRESSURE_STATUS_STYLE.normal
-    const pressureText = point.latestPressure == null
-      ? '--'
-      : (Number(point.latestPressure) / 1000).toFixed(2) + 'MPa'
-    const label = new BMapGL.Label(`${point.name || '压力点'} ${pressureText}`, {
-      offset: new BMapGL.Size(-48, -44)
-    })
-    label.setPosition(position)
-    label.setStyle({
-      color: style.text,
-      background: 'rgba(12, 46, 82, 0.86)',
-      border: `1px solid ${style.border}`,
-      borderRadius: '8px',
-      padding: '5px 9px',
-      fontSize: '12px',
-      lineHeight: '16px',
-      whiteSpace: 'nowrap'
-    })
-    mapInstance.addOverlay(label)
-    pressureOverlays.push(label)
-  })
-}
-
-/** 按管网实际范围自动缩放,只有一条管段时按固定级别定位 */
-function fitPipeView(BMapGL, features) {
-  const points = []
-  features.forEach(feature => {
-    const coordinates = feature.geometry && feature.geometry.coordinates
-    if (!Array.isArray(coordinates)) return
-    coordinates.forEach(([lng, lat]) => {
-      const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
-      points.push(new BMapGL.Point(bdLng, bdLat))
-    })
-  })
-  if (!points.length) return
-  if (points.length === 1) {
-    mapInstance.centerAndZoom(points[0], 16)
-    return
-  }
-  mapInstance.setViewport(points)
-}
-
-/** 拉取燃气管网数据并上图 */
-async function loadPipeLines(BMapGL) {
-  try {
-    const res = await getGisPipeLines({ networkType: '0' })
-    const features = (res.data && res.data.features) || []
-    if (!features.length) {
-      mapError.value = '暂无燃气管网坐标数据,请先在管网基础数据中补齐起终点经纬度。'
-      return
-    }
-    mapError.value = ''
-    drawPipelines(BMapGL, features)
-    fitPipeView(BMapGL, features)
-  } catch (error) {
-    console.error('[燃气大屏] 加载燃气管网失败', error)
-    mapError.value = '燃气管网数据加载失败,请稍后重试。'
-  }
-}
-
-async function initMap() {
-  if (!mapWrapRef.value) return
-  try {
-    const BMapGL = await waitForBMapGL()
-    mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
-    const center = new BMapGL.Point(110.3965, 28.4638)
-    mapInstance.centerAndZoom(center, 15)
-    mapInstance.enableScrollWheelZoom(true)
-    mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
-
-    drawPressurePoints(BMapGL, dashboardData.value.pressurePoints || [])
-    await loadPipeLines(BMapGL)
-  } catch (error) {
-    mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
-  }
-}
-
-/** 定时刷新压力点位,不重建底图 */
-function refreshPressureLayer() {
-  if (!mapInstance || !window.BMapGL) return
-  drawPressurePoints(window.BMapGL, dashboardData.value.pressurePoints || [])
-}
-
-function handleResize() {
-  repairChart?.resize()
-  hazardChart?.resize()
-}
-
-/** 仪表盘刷新间隔(毫秒),压力点位与在线率按此周期同步最新采集值 */
-const REFRESH_INTERVAL = 30 * 1000
-let refreshTimer = null
-
-async function loadDashboardData(silent = false) {
-  if (!silent) loading.value = true
-  try {
-    const res = await getGasDashboardSummary()
-    dashboardData.value = res.data || {}
-    refreshPressureLayer()
-    if (silent) {
-      initRepairChart()
-      initHazardChart()
-    }
-  } catch (e) {
-    console.error('[燃气大屏] 加载仪表盘数据失败', e)
-  } finally {
-    if (!silent) loading.value = false
-  }
-}
-
-onMounted(async () => {
-  updateClock()
-  clockTimer = window.setInterval(updateClock, 1000)
-  await loadDashboardData()
-  await nextTick()
-  initRepairChart()
-  initHazardChart()
-  await initMap()
-  refreshTimer = window.setInterval(() => loadDashboardData(true), REFRESH_INTERVAL)
-  window.addEventListener('resize', handleResize)
-})
-
-onBeforeUnmount(() => {
-  if (clockTimer) window.clearInterval(clockTimer)
-  if (refreshTimer) window.clearInterval(refreshTimer)
-  window.removeEventListener('resize', handleResize)
-  repairChart?.dispose()
-  hazardChart?.dispose()
-  pipeOverlays = []
-  pressureOverlays = []
-  mapInstance = null
-})
-</script>
-
-<template>
-  <div class="gas-home-screen">
-    <div class="screen-header">
-      <div class="header-time">{{ nowText }} {{ weekText }}</div>
-      <div class="header-title-wrap">
-        <div class="header-title">燃气管网安全运行监测系统</div>
-        <div class="header-subtitle">Gas Pipeline Operation Dashboard</div>
-      </div>
-      <div class="header-tags">
-        <span class="header-tag">实时监测</span>
-        <span class="header-tag">管网运行</span>
-        <span class="header-tag">隐患预警</span>
-      </div>
-    </div>
-
-    <div class="screen-body">
-      <div class="side-panel left-panel">
-        <section class="panel-card base-card">
-          <div class="panel-title">基础概况</div>
-          <div class="base-grid">
-            <div v-for="item in baseCards" :key="item.label" class="base-item">
-              <div class="base-icon" :style="{ background: item.color }">
-                <component :is="item.icon" class="base-icon-svg" />
-              </div>
-              <div class="base-text">
-                <div class="base-label">{{ item.label }}</div>
-                <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
-              </div>
-            </div>
-          </div>
-        </section>
-
-        <section class="panel-card">
-          <div class="panel-title">维修趋势</div>
-          <div ref="repairChartRef" class="chart-box repair-chart"></div>
-        </section>
-
-        <section class="panel-card table-card">
-          <div class="panel-title">监测点数据</div>
-          <div class="table-wrap">
-            <table class="data-table">
-              <thead>
-                <tr>
-                  <th>序号</th>
-                  <th>监测点</th>
-                  <th>时间</th>
-                  <th>数值</th>
-                  <th>状态</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr v-for="row in pointRows" :key="row.id">
-                  <td>{{ row.id }}</td>
-                  <td :title="`${row.name || '-'} / ${row.deviceName || row.deviceCode || '-'}`">{{ row.name }} / {{ row.deviceName || row.deviceCode || '-' }}</td>
-                  <td :title="row.time">{{ row.time }}</td>
-                  <td :title="row.value">{{ row.value }}</td>
-                  <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </section>
-      </div>
-
-      <div class="center-panel">
-        <div class="map-stage">
-          <!-- 管网、在线率、压力、告警合并为一条指标条,按份均分宽度,窄屏也不会溢出 -->
-          <div class="map-stage-top">
-            <div v-for="item in operationSummary" :key="item.label" class="summary-item">
-              <span class="summary-label">{{ item.label }}</span>
-              <strong class="summary-value">{{ item.value }}</strong>
-            </div>
-            <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
-              <span>{{ item.label }}</span>
-              <strong>{{ item.value }}</strong>
-            </div>
-          </div>
-
-          <div ref="mapWrapRef" class="map-view"></div>
-          <div v-if="mapError" class="map-error">{{ mapError }}</div>
-
-          <div class="map-panel info-panel">
-            <div class="mini-title">当前管网信息</div>
-            <div class="mini-row"><span>管网总长度</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.totalLength) || 0 }}km</strong></div>
-            <div class="mini-row"><span>燃气管网</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.gasNetworks) || 0 }}条</strong></div>
-            <div class="mini-row"><span>设备总数</span><strong>{{ (dashboardData.equipmentStats && dashboardData.equipmentStats.totalEquipment) || 0 }}台</strong></div>
-            <div class="mini-row" :title="dashboardData.pressureUpdatedAt || '暂无压力采集时间'">
-              <span>压力采集</span><strong>{{ pressureTimeText }}</strong>
-            </div>
-          </div>
-
-          <div class="map-panel point-panel">
-            <div class="mini-title">当前管点信息</div>
-            <div class="mini-row"><span>监测点总数</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.totalPoints) || 0 }}个</strong></div>
-            <div class="mini-row"><span>在线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.onlinePoints) || 0 }}个</strong></div>
-            <div class="mini-row"><span>离线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.offlinePoints) || 0 }}个</strong></div>
-          </div>
-        </div>
-      </div>
-
-      <div class="side-panel right-panel">
-        <section class="panel-card">
-          <div class="panel-title">设备运行</div>
-          <div class="table-wrap equipment-wrap">
-            <table class="data-table compact-table">
-              <thead>
-                <tr>
-                  <th>序号</th>
-                  <th>设备名称</th>
-                  <th>设备编号</th>
-                  <th>位置</th>
-                  <th>状态</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr v-for="row in equipmentRows" :key="row.id">
-                  <td>{{ row.id }}</td>
-                  <td>{{ row.name }}</td>
-                  <td>{{ row.code }}</td>
-                  <td>{{ row.location }}</td>
-                  <td>{{ row.status }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </section>
-
-        <section class="panel-card warning-card">
-          <div class="panel-title">告警动态</div>
-          <div class="warning-scroll">
-            <div class="warning-list">
-              <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
-                <div class="warning-side">
-                  <div class="warning-badge">{{ item.level }}</div>
-                  <div class="warning-tag">{{ item.tag }}</div>
-                </div>
-                <div class="warning-main">
-                  <div class="warning-title-row">
-                    <span class="warning-title">{{ item.title }}</span>
-                    <span class="warning-time">{{ item.time }}</span>
-                  </div>
-                  <div class="warning-code">{{ item.code }}</div>
-                  <div class="warning-desc">{{ item.desc }}</div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </section>
-
-        <section class="panel-card hazard-card">
-          <div class="panel-title">隐患统计</div>
-          <div class="hazard-summary three-col">
-            <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
-              <span class="hazard-dot" :style="{ background: item.color }"></span>
-              <span>{{ item.label }}</span>
-              <strong :style="{ color: item.color }">{{ item.value }}项</strong>
-            </div>
-          </div>
-          <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
-        </section>
-      </div>
-    </div>
-
-    <a href="https://deerflow.tech" target="_blank" class="deerflow-mark" title="Created By Deerflow">
-      <span class="deerflow-diamond">◆</span>
-      <span class="deerflow-text">Deerflow</span>
-    </a>
-  </div>
-</template>
-
-<style scoped lang="scss">
-.gas-home-screen {
-  min-height: 100vh;
-  color: #fff;
-  background:
-    linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
-    radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
-    #081425;
-  position: relative;
-}
-
-.gas-home-screen::before {
-  content: '';
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  z-index: 0;
-  background:
-    radial-gradient(circle at 28% 32%, rgba(56, 216, 255, 0.06) 0%, transparent 45%),
-    radial-gradient(circle at 72% 58%, rgba(22, 109, 255, 0.05) 0%, transparent 40%),
-    radial-gradient(circle at 50% 85%, rgba(56, 216, 255, 0.04) 0%, transparent 35%);
-  animation: bgPulse 8s ease-in-out infinite alternate;
-}
-
-@keyframes bgPulse {
-  0% { opacity: 0.6; }
-  100% { opacity: 1; }
-}
-
-.screen-header {
-  height: 72px;
-  padding: 0 24px;
-  display: grid;
-  grid-template-columns: 320px 1fr 320px;
-  align-items: center;
-  position: relative;
-  z-index: 1;
-  background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
-  border-bottom: 1px solid rgba(93, 194, 255, 0.28);
-}
-
-.screen-header::after {
-  content: '';
-  position: absolute;
-  bottom: -1px;
-  left: 5%;
-  width: 90%;
-  height: 1px;
-  background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.7), rgba(22, 109, 255, 0.7), transparent);
-  animation: headerGlow 4s ease-in-out infinite alternate;
-}
-
-@keyframes headerGlow {
-  0% { opacity: 0.4; }
-  100% { opacity: 1; }
-}
-
-.header-time {
-  font-size: 18px;
-  font-weight: 700;
-  letter-spacing: 1.5px;
-  color: #ddf6ff;
-  font-variant-numeric: tabular-nums;
-  text-shadow: 0 0 18px rgba(56, 216, 255, 0.35);
-}
-
-.header-title-wrap {
-  text-align: center;
-}
-
-.header-title {
-  font-size: 30px;
-  font-weight: 800;
-  letter-spacing: 3px;
-  background: linear-gradient(180deg, #e8f6ff 0%, #8fd6ff 100%);
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  background-clip: text;
-}
-
-.header-subtitle {
-  margin-top: 4px;
-  color: rgba(196, 234, 255, 0.65);
-  font-size: 11px;
-  letter-spacing: 3px;
-  text-transform: uppercase;
-}
-
-.header-tags {
-  display: flex;
-  justify-content: flex-end;
-  gap: 8px;
-}
-
-.header-tag {
-  padding: 6px 14px;
-  border: 1px solid rgba(117, 206, 255, 0.35);
-  border-radius: 16px;
-  color: #d8f5ff;
-  background: rgba(13, 77, 128, 0.45);
-  font-size: 12px;
-  letter-spacing: 1px;
-  transition: all 0.3s;
-  cursor: default;
-}
-
-.header-tag:hover {
-  border-color: rgba(117, 206, 255, 0.7);
-  background: rgba(13, 77, 128, 0.65);
-  box-shadow: 0 0 16px rgba(56, 216, 255, 0.15);
-}
-
-.screen-body {
-  height: calc(100vh - 72px);
-  padding: 12px;
-  display: grid;
-  grid-template-columns: 360px 1fr 360px;
-  gap: 12px;
-  position: relative;
-  z-index: 1;
-}
-
-.side-panel,
-.center-panel {
-  min-height: 0;
-}
-
-.left-panel,
-.right-panel {
-  display: grid;
-  gap: 12px;
-}
-
-.left-panel {
-  grid-template-rows: 220px 220px 1fr;
-}
-
-.right-panel {
-  grid-template-rows: 250px 260px 1fr;
-}
-
-.panel-card {
-  min-height: 0;
-  padding: 14px;
-  position: relative;
-  background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
-  border: 1px solid rgba(65, 158, 219, 0.22);
-  border-radius: 12px;
-  box-shadow: 0 2px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.025);
-  transition: border-color 0.4s;
-}
-
-.panel-card::after {
-  content: '';
-  position: absolute;
-  top: -1px;
-  left: 20px;
-  right: 20px;
-  height: 1px;
-  border-radius: 1px;
-  background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.25), transparent);
-  pointer-events: none;
-}
-
-.panel-card:hover {
-  border-color: rgba(65, 158, 219, 0.38);
-}
-
-.panel-title {
-  position: relative;
-  padding-left: 16px;
-  margin-bottom: 14px;
-  color: #8fe5ff;
-  font-size: 18px;
-  font-weight: 800;
-  letter-spacing: 1px;
-}
-
-.panel-title::before {
-  content: '';
-  position: absolute;
-  left: 0;
-  top: 3px;
-  width: 4px;
-  height: 20px;
-  border-radius: 2px;
-  background: linear-gradient(180deg, #28d7ff, #1776ff);
-  box-shadow: 0 0 10px rgba(40, 215, 255, 0.5);
-}
-
-.base-grid {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 14px;
-}
-
-.base-card {
-  background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
-}
-
-.base-grid {
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 12px;
-}
-
-.base-item {
-  display: flex;
-  align-items: center;
-  gap: 14px;
-  min-height: 72px;
-  padding: 14px;
-  border-radius: 14px;
-  background: linear-gradient(180deg, rgba(20, 57, 89, 0.45), rgba(18, 44, 72, 0.35));
-  border: 1px solid rgba(97, 190, 255, 0.12);
-  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
-  cursor: default;
-  position: relative;
-  overflow: hidden;
-}
-
-.base-item::before {
-  content: '';
-  position: absolute;
-  inset: 0;
-  border-radius: 14px;
-  background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.03), transparent 70%);
-  pointer-events: none;
-  transition: opacity 0.35s;
-  opacity: 0;
-}
-
-.base-item:hover {
-  transform: translateY(-2px);
-  border-color: rgba(97, 190, 255, 0.35);
-  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(97, 190, 255, 0.1) inset;
-}
-
-.base-item:hover::before {
-  opacity: 1;
-}
-
-.base-icon {
-  width: 48px;
-  height: 48px;
-  border-radius: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex: 0 0 48px;
-  position: relative;
-  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s;
-}
-
-.base-item:hover .base-icon {
-  transform: scale(1.08);
-  box-shadow: 0 0 28px rgba(56, 216, 255, 0.3);
-}
-
-.base-icon-svg {
-  width: 26px;
-  height: 26px;
-  color: #fff;
-  transition: transform 0.35s;
-}
-
-.base-item:hover .base-icon-svg {
-  transform: scale(1.1);
-}
-
-.base-text {
-  min-width: 0;
-}
-
-.base-label {
-  color: #c0dff5;
-  font-size: 12px;
-  letter-spacing: 1px;
-  text-transform: uppercase;
-}
-
-.base-value {
-  margin-top: 4px;
-  color: #fff;
-  font-size: 32px;
-  font-weight: 800;
-  line-height: 1;
-  font-variant-numeric: tabular-nums;
-  letter-spacing: -0.5px;
-}
-
-.base-value span {
-  margin-left: 4px;
-  color: #90d9ff;
-  font-size: 12px;
-  font-weight: 500;
-  letter-spacing: 0;
-}
-
-.chart-box {
-  width: 100%;
-  height: calc(100% - 36px);
-}
-
-.table-card,
-.warning-card,
-.hazard-card {
-  overflow: hidden;
-}
-
-.table-wrap {
-  height: calc(100% - 38px);
-  overflow: auto;
-}
-
-.data-table {
-  width: 100%;
-  border-collapse: collapse;
-  table-layout: fixed;
-}
-
-.data-table thead {
-  background: rgba(24, 122, 198, 0.55);
-}
-
-.data-table thead th {
-  font-weight: 700;
-  letter-spacing: 0.5px;
-  color: #b9e4ff;
-}
-
-.data-table th,
-.data-table td {
-  padding: 10px 6px;
-  color: #f4fbff;
-  text-align: center;
-  font-size: 13px;
-  border-bottom: 1px solid rgba(130, 194, 231, 0.1);
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.data-table tbody tr {
-  transition: background 0.25s;
-}
-
-.data-table tbody tr:nth-child(odd) {
-  background: rgba(133, 196, 242, 0.10);
-}
-
-.data-table tbody tr:nth-child(even) {
-  background: rgba(133, 196, 242, 0.04);
-}
-
-.data-table tbody tr:hover {
-  background: rgba(56, 216, 255, 0.12);
-}
-
-.compact-table th,
-.compact-table td {
-  padding: 9px 4px;
-}
-
-.status-cell.ok {
-  color: #6effc4;
-  position: relative;
-}
-
-.status-cell.warn {
-  color: #ffe16a;
-}
-
-.status-cell.off {
-  color: #ff8e8e;
-}
-
-.status-cell::before {
-  content: '';
-  display: inline-block;
-  width: 6px;
-  height: 6px;
-  border-radius: 50%;
-  margin-right: 6px;
-  vertical-align: middle;
-}
-
-.status-cell.ok::before {
-  background: #6effc4;
-  box-shadow: 0 0 6px rgba(110, 255, 196, 0.6);
-}
-
-.status-cell.warn::before {
-  background: #ffe16a;
-  box-shadow: 0 0 6px rgba(255, 225, 106, 0.6);
-}
-
-.status-cell.off::before {
-  background: #ff8e8e;
-  box-shadow: 0 0 6px rgba(255, 142, 142, 0.6);
-}
-
-.center-panel {
-  position: relative;
-}
-
-.map-stage {
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 12px;
-  border: 1px solid rgba(56, 159, 223, 0.2);
-  background: rgba(8, 20, 37, 0.64);
-  box-shadow: 0 0 40px rgba(0, 0, 0, 0.3), inset 0 0 60px rgba(8, 30, 60, 0.3);
-}
-
-.map-stage-top {
-  position: absolute;
-  top: 14px;
-  left: 14px;
-  right: 14px;
-  z-index: 3;
-  display: flex;
-  align-items: stretch;
-  gap: 8px;
-  pointer-events: none;
-}
-
-.summary-item,
-.map-kpi {
-  flex: 1 1 0;
-  min-width: 0;
-  padding: 8px 10px;
-  border-radius: 10px;
-  background: rgba(8, 31, 54, 0.82);
-  border: 1px solid rgba(79, 177, 238, 0.18);
-  backdrop-filter: blur(8px);
-  transition: all 0.3s;
-  overflow: hidden;
-}
-
-.summary-label,
-.map-kpi span {
-  display: block;
-  color: #91d7ff;
-  font-size: 12px;
-  letter-spacing: 0.5px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.summary-value,
-.map-kpi strong {
-  display: block;
-  margin-top: 4px;
-  color: #fff;
-  font-size: 20px;
-  font-weight: 800;
-  letter-spacing: 0.5px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.map-view {
-  width: 100%;
-  height: 100%;
-}
-
-.map-error {
-  position: absolute;
-  inset: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffd3d3;
-  background: rgba(5, 10, 20, 0.72);
-  z-index: 4;
-}
-
-.map-panel {
-  position: absolute;
-  z-index: 3;
-  width: 190px;
-  padding: 14px;
-  border-radius: 12px;
-  background: rgba(8, 31, 54, 0.82);
-  border: 1px solid rgba(79, 177, 238, 0.18);
-  backdrop-filter: blur(10px);
-}
-
-.info-panel {
-  left: 18px;
-  bottom: 20px;
-}
-
-.point-panel {
-  right: 18px;
-  bottom: 20px;
-}
-
-.mini-title {
-  margin-bottom: 10px;
-  color: #8fe5ff;
-  font-size: 14px;
-  font-weight: 700;
-  letter-spacing: 0.5px;
-}
-
-.mini-row {
-  display: flex;
-  justify-content: space-between;
-  margin-top: 6px;
-  padding: 4px 0;
-  color: #d6efff;
-  font-size: 13px;
-  border-bottom: 1px solid rgba(130, 194, 231, 0.06);
-}
-
-.mini-row:last-child {
-  border-bottom: none;
-}
-
-.warning-scroll {
-  height: calc(100% - 38px);
-  overflow-y: auto;
-  padding-right: 4px;
-}
-
-.warning-scroll::-webkit-scrollbar {
-  width: 5px;
-}
-
-.warning-scroll::-webkit-scrollbar-thumb {
-  border-radius: 999px;
-  background: rgba(96, 180, 235, 0.3);
-}
-
-.warning-scroll::-webkit-scrollbar-track {
-  background: transparent;
-}
-
-.warning-list {
-  display: grid;
-  gap: 10px;
-}
-
-.warning-item {
-  display: flex;
-  gap: 12px;
-  align-items: stretch;
-  padding: 14px;
-  border-radius: 14px;
-  border: 1px solid rgba(255, 108, 108, 0.1);
-  background: linear-gradient(180deg, rgba(72, 24, 38, 0.42), rgba(49, 19, 31, 0.30));
-  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
-  position: relative;
-  overflow: hidden;
-}
-
-.warning-item::before {
-  content: '';
-  position: absolute;
-  left: 0;
-  top: 12px;
-  bottom: 12px;
-  width: 3px;
-  border-radius: 0 3px 3px 0;
-  transition: all 0.35s;
-}
-
-.warning-item.danger::before {
-  background: linear-gradient(180deg, #ff5858, #ff3030);
-  box-shadow: 0 0 12px rgba(255, 58, 58, 0.5);
-  animation: dangerPulse 2s ease-in-out infinite;
-}
-
-.warning-item.notice::before {
-  background: linear-gradient(180deg, #ffc635, #ff9f1a);
-  box-shadow: 0 0 10px rgba(255, 198, 53, 0.35);
-}
-
-@keyframes dangerPulse {
-  0%, 100% { opacity: 0.6; }
-  50% { opacity: 1; }
-}
-
-.warning-item:hover {
-  transform: translateX(3px);
-}
-
-.warning-item.danger:hover {
-  border-color: rgba(255, 108, 108, 0.25);
-  box-shadow: 0 4px 20px rgba(255, 58, 58, 0.1);
-}
-
-.warning-item.notice:hover {
-  border-color: rgba(255, 201, 92, 0.2);
-  box-shadow: 0 4px 20px rgba(255, 198, 53, 0.08);
-}
-
-.warning-item.notice {
-  border-color: rgba(255, 201, 92, 0.1);
-  background: linear-gradient(180deg, rgba(78, 63, 23, 0.28), rgba(48, 43, 20, 0.18));
-}
-
-.warning-side {
-  width: 52px;
-  flex: 0 0 52px;
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-  align-items: center;
-}
-
-.warning-badge {
-  width: 34px;
-  height: 34px;
-  border-radius: 10px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  font-weight: 800;
-  color: #fff;
-  transition: transform 0.3s;
-}
-
-.warning-item:hover .warning-badge {
-  transform: scale(1.1);
-}
-
-.warning-tag {
-  width: 100%;
-  padding: 3px 0;
-  border-radius: 999px;
-  text-align: center;
-  font-size: 10px;
-  letter-spacing: 0.5px;
-  color: #dcefff;
-  background: rgba(255, 255, 255, 0.06);
-}
-
-.warning-item.danger .warning-badge {
-  background: rgba(255, 58, 58, 0.28);
-  color: #ff8787;
-}
-
-.warning-item.danger .warning-title {
-  color: #ffd8d8;
-}
-
-.warning-item.notice .warning-badge {
-  background: rgba(255, 198, 53, 0.2);
-  color: #ffd45d;
-}
-
-.warning-item.notice .warning-title {
-  color: #fff2c4;
-}
-
-.warning-main {
-  min-width: 0;
-  flex: 1;
-}
-
-.warning-title-row {
-  display: flex;
-  align-items: flex-start;
-  justify-content: space-between;
-  gap: 8px;
-}
-
-.warning-title {
-  font-weight: 700;
-  font-size: 16px;
-  line-height: 1.2;
-}
-
-.warning-time,
-.warning-code,
-.warning-desc {
-  color: #dcefff;
-  font-size: 12px;
-}
-
-.warning-time {
-  color: #b6dfff;
-  white-space: nowrap;
-}
-
-.warning-code {
-  margin: 6px 0 4px;
-  color: #9ad8ff;
-}
-
-.warning-desc {
-  line-height: 1.5;
-}
-
-.hazard-summary {
-  display: flex;
-  gap: 12px;
-  margin-bottom: 10px;
-}
-
-.hazard-summary.three-col {
-  justify-content: space-between;
-}
-
-.hazard-summary-item {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 13px;
-  padding: 8px 12px;
-  border-radius: 10px;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.04);
-  transition: all 0.3s;
-}
-
-.hazard-summary-item:hover {
-  background: rgba(255, 255, 255, 0.06);
-  border-color: rgba(255, 255, 255, 0.08);
-}
-
-.hazard-dot {
-  width: 8px;
-  height: 24px;
-  display: inline-block;
-  border-radius: 4px;
-  box-shadow: 0 0 10px currentColor;
-}
-
-.hazard-chart {
-  height: 160px;
-}
-
-.equipment-wrap {
-  height: calc(100% - 38px);
-}
-
-:deep(.BMap_cpyCtrl),
-:deep(.anchorBL) {
-  display: none !important;
-}
-
-.deerflow-mark {
-  position: fixed;
-  bottom: 14px;
-  right: 18px;
-  z-index: 10;
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  text-decoration: none;
-  opacity: 0.28;
-  transition: opacity 0.4s;
-}
-
-.deerflow-mark:hover {
-  opacity: 0.6;
-}
-
-.deerflow-diamond {
-  font-size: 10px;
-  color: #8fe5ff;
-}
-
-.deerflow-text {
-  font-size: 11px;
-  letter-spacing: 1px;
-  color: #8fe5ff;
-  font-weight: 500;
-}
-
-@media (max-width: 1500px) {
-  .screen-body {
-    grid-template-columns: 320px 1fr 320px;
-  }
-
-  .header-title {
-    font-size: 24px;
-  }
-
-  .summary-item,
-  .map-kpi {
-    min-width: 0;
-    padding: 7px 8px;
-  }
-
-  .summary-label,
-  .map-kpi span {
-    font-size: 11px;
-  }
-
-  .summary-value,
-  .map-kpi strong {
-    font-size: 16px;
-  }
-}
-</style>

+ 131 - 45
src/views/subSystem/manholeCover/home/index.vue → src/views/subSystem/pipeNetwork/home/index.vue

@@ -1,10 +1,13 @@
-<!--窨井盖监测首页大屏:中间地图按 equipmentGIS.vue 的 GIS 一张图规范实现-->
+<!-- 供水/排水/燃气/窨井通用首页:数据范围由 sys_menu.query.rootTypeId 控制;地图按 equipmentGIS.vue 的点位规范实现,并按原燃气大屏口径叠加管网线段 -->
 <template>
   <div class="monitor-container">
     <!-- 顶部:标题 + 设备状态总览(全域健康度带) -->
     <header class="top-bar">
       <div class="brand">
-        <div class="brand-title">窨井盖监测中心</div>
+        <div class="brand-title">{{ subsystemName }}监测中心</div>
+        <div class="scope-badge" :class="{ invalid: !rootTypeId }">
+          rootTypeId {{ rootTypeId ?? '—' }} · {{ scopeSourceText }}
+        </div>
       </div>
 
       <div class="health-band">
@@ -26,6 +29,8 @@
       </div>
     </header>
 
+    <el-alert v-if="scopeError" class="scope-alert" type="error" show-icon :closable="false" :title="scopeError" />
+
     <!-- 主体三列 -->
     <div class="main-grid">
       <!-- 左:实时报警 + 报警类型分布 -->
@@ -77,20 +82,21 @@
       <div class="col center-col">
         <section class="panel map-panel">
           <div class="map-header">
-            <span>窨井设备分布</span>
+            <span>{{ subsystemName }}设备分布 · 管网 {{ pipeFeatures.length }} 段</span>
             <el-button :icon="Refresh" size="small" :loading="gisLoading" @click="loadGisData">刷新</el-button>
           </div>
           <div class="map-wrap">
             <GisMapCanvas
               ref="gisMapRef"
               :points="mapPoints"
+              :lines="mapLines"
               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="map-legend" :class="{ 'behind-dialog': detailVisible }" @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>在线
@@ -101,6 +107,14 @@
               <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.alarm }" @click="toggleLegend('alarm')">
                 <i class="legend-dot alarm"></i>报警(在线/离线时叠加)
               </button>
+              <button type="button" class="legend-row" :class="{ 'is-off': !pipelineVisible }" @click="pipelineVisible = !pipelineVisible">
+                <i class="legend-line"></i>管网({{ pipeFeatures.length }} 段)
+              </button>
+              <div class="pipe-legend">
+                <span><i class="pipe-swatch normal"></i>正常</span>
+                <span><i class="pipe-swatch maintain"></i>维护中</span>
+                <span><i class="pipe-swatch abnormal"></i>异常</span>
+              </div>
             </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>
@@ -247,17 +261,39 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, nextTick, computed } from 'vue'
+import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue'
+import { useRoute } from 'vue-router'
 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 {
+  getHomeDevices, getHomeWorkOrders, getHomeAlarms, getHomeGisPoints,
+  getHomePipeLines, getHomeEquipmentDetail
+} from '@/api/pipeNetwork/home'
 import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
-import { getAlarmDataPage } from '@/api/drainage'
 import { gisPointStatusIcons } from '@/utils/gisMapColors'
 import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
+import usePermissionStore from '@/store/modules/permission'
+import { HOME_ROOT_TYPES, resolveHomeScope } from '@/utils/homeScope'
+
+// ==================== 数据范围:sys_menu.query.rootTypeId ====================
+/**
+ * 不再写死任何设备大类:菜单 query 是唯一数据范围来源,URL 参数丢失时再从
+ * /getRouters 下发的已授权菜单里按当前路由恢复。取不到范围就不发任何请求,
+ * 避免四个子系统首页都静默展示窨井(rootTypeId=4)数据。
+ */
+const route = useRoute()
+const permissionStore = usePermissionStore()
+const homeScope = computed(() => resolveHomeScope({
+  query: route.query,
+  path: route.path,
+  menus: permissionStore.sidebarRouters
+}))
+const rootTypeId = computed(() => homeScope.value.rootTypeId)
+const subsystemName = computed(() => HOME_ROOT_TYPES[rootTypeId.value] || '未指定子系统')
+const scopeSourceText = computed(() => ({ url: 'URL参数', menu: '菜单配置', 'only-menu': '唯一授权菜单' }[homeScope.value.source] || '未配置'))
+const scopeError = computed(() => rootTypeId.value ? '' :
+  '当前菜单未配置 sys_menu.query.rootTypeId(1-供水 / 2-排水 / 3-燃气 / 4-窨井),请执行 migration_unified_subsystem_home.sql 后重新登录刷新菜单;为避免越权,未加载任何数据。')
 
 // ==================== 状态 ====================
 const loading = reactive({ work: false, alarm: false, device: false })
@@ -271,10 +307,8 @@ const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
 /** 无设备数据时仍展示的区域维度,取自 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) */
-const MANHOLE_ROOT_TYPE_ID = 4
+/** 管网状态配色,取值见 pipe_network_status 字典:0-正常 1-维护中 2-异常;线型沿用原燃气大屏口径 */
+const PIPE_STATUS_COLOR = { 0: '#16a34a', 1: '#ffb020', 2: '#ef4444' }
 /** 报警查询后端限制 pageSize 不超过 100 */
 const ALARM_PAGE_SIZE = 100
 const ALARM_MAX_PAGES = 3
@@ -369,12 +403,32 @@ const alarmTypeDist = computed(() => {
   return list.map(x => ({ ...x, rate: Math.round(x.count / max * 100) }))
 })
 
-// ==================== GIS 一张图(与 equipmentGIS.vue 同规范) ====================
+// ==================== GIS 一张图(点位同 equipmentGIS.vue,管线按 GeoJSON LineString 状态着色) ====================
 const mapCenter = { lng: 110.3965, lat: 28.4638 }
 const gisDevices = ref([])
 const gisLoading = ref(false)
 const mapError = ref('')
 const gisMapRef = ref(null)
+const pipeFeatures = ref([])
+const pipelineVisible = ref(true)
+
+/** GeoJSON LineString → GisMapCanvas 的 lines;坐标转换由组件内的 convertCoord 统一处理 */
+const mapLines = computed(() => {
+  if (!pipelineVisible.value) return []
+  return pipeFeatures.value.map(feature => {
+    const props = feature?.properties || {}
+    const coordinates = Array.isArray(feature?.geometry?.coordinates) ? feature.geometry.coordinates : []
+    const status = String(props.status ?? '')
+    return {
+      points: coordinates
+        .filter(pair => Array.isArray(pair) && pair.length >= 2)
+        .map(([lng, lat]) => ({ lng: Number(lng), lat: Number(lat) })),
+      color: PIPE_STATUS_COLOR[status] || PIPE_STATUS_COLOR[0],
+      strokeWeight: status === '2' ? 5 : 4,
+      strokeOpacity: 0.92
+    }
+  }).filter(line => line.points.length >= 2)
+})
 
 /** 标记图预加载:在线=绿、离线=灰,报警时在其右上角叠加红色标记 */
 const iconsReady = ref(false)
@@ -482,17 +536,27 @@ function normalizeEquipment(row) {
 }
 
 async function loadGisData() {
+  if (!rootTypeId.value) return
   gisLoading.value = true
   mapError.value = ''
   try {
-    // rootTypeId=4 只取窨井设备,与 equipmentGIS 一致
-    const res = await getEquipmentGisPoints({ rootTypeId: MANHOLE_ROOT_TYPE_ID })
-    const list = res?.data ?? res
+    // 点位按菜单 rootTypeId 隔离;1/2/3 只取对应管网,4(窨井) 不区分管网类型、返回全部管线
+    const [pointsResult, pipesResult] = await Promise.allSettled([
+      getHomeGisPoints(rootTypeId.value),
+      getHomePipeLines(rootTypeId.value)
+    ])
+    if (pointsResult.status === 'rejected') {
+      throw pointsResult.reason
+    }
+    const list = pointsResult.value?.data ?? pointsResult.value
     gisDevices.value = (Array.isArray(list) ? list : []).map(normalizeEquipment)
+    const geojson = pipesResult.status === 'fulfilled' ? (pipesResult.value?.data ?? pipesResult.value) : null
+    pipeFeatures.value = Array.isArray(geojson?.features) ? geojson.features : []
     await nextTick()
+    // GisMapCanvas.fitBounds 已把 lines 的坐标纳入视野计算
     gisMapRef.value?.fitBounds()
   } catch (e) {
-    mapError.value = '监测设备数据加载失败,请点击右上角“刷新”重试'
+    mapError.value = '设备或管网数据加载失败,请点击右上角“刷新”重试'
   } finally {
     gisLoading.value = false
   }
@@ -523,7 +587,7 @@ async function openDetail(equipmentId) {
   selectedDetail.value = null
   detailError.value = ''
   try {
-    const detail = await getEquipmentDetail(equipmentId)
+    const detail = await getHomeEquipmentDetail(equipmentId, rootTypeId.value)
     selectedDetail.value = detail?.data || detail
   } catch (e) {
     detailError.value = '设备详情加载失败,请关闭后重试'
@@ -534,31 +598,30 @@ async function openDetail(equipmentId) {
 
 // ==================== 数据加载 ====================
 async function loadAll() {
+  // 没有合法 rootTypeId 时清空旧数据并停止请求,避免复用组件时残留上一个子系统的数据
+  if (!rootTypeId.value) {
+    allDevices.value = []
+    workOrders.value = []
+    recentAlarms.value = []
+    gisDevices.value = []
+    pipeFeatures.value = []
+    return
+  }
   await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData(), loadGisData()])
 }
 
 async function loadDevices() {
   loading.device = true
   try {
-    const res = await getManholeLayerData({ isQueryManholeData: true })
-    const devices = res.data?.devices || []
-    allDevices.value = devices.map(d => {
-      const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
-      const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
-      const manholeAlarm = Number(d.manholeData?.alarmStatus)
-      const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
-        Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
-        Number(d.manholeData?.waterLevelAlarmStatus) === 1
-      let status = 'offline'
-      if (isAlert) status = 'alert'
-      else if (onlineStatus === 1) status = 'normal'
-      return {
-        id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
-        location: d.equipmentLocation,
-        district: d.district != null ? districtLabel(d.district) : '',
-        status
-      }
-    })
+    // 后端已按 rootTypeId 过滤并归一化三态运行状态,四个子系统数据结构完全一致
+    const res = await getHomeDevices(rootTypeId.value)
+    const devices = res?.data ?? res
+    allDevices.value = (Array.isArray(devices) ? devices : []).map(d => ({
+      id: d.id, code: d.code, name: d.name,
+      location: d.location,
+      district: d.district != null ? districtLabel(d.district) : '',
+      status: d.status
+    }))
   } catch (e) {
     ElMessage.error('设备数据加载失败')
   } finally {
@@ -569,11 +632,11 @@ async function loadDevices() {
 async function loadWorkOrders() {
   loading.work = true
   try {
-    const res = await listWorkOrder({
+    const res = await getHomeWorkOrders({
+      rootTypeId: rootTypeId.value,
       pageNum: 1,
       pageSize: 20,
-      orderStatus: TODO_ORDER_STATUS,
-      deviceTypeParentId: DEVICE_TYPE_PARENT_ID
+      orderStatus: TODO_ORDER_STATUS
     })
     workOrders.value = res.data?.records || []
   } catch (e) {
@@ -584,17 +647,17 @@ async function loadWorkOrders() {
 }
 
 /**
- * 拉取近 N 天的窨井报警。
- * rootTypeId 是报警查询必填项(后端校验「设备大类不能为空」),pageSize 上限 100,按页累加。
+ * 拉取近 N 天的当前子系统报警。
+ * rootTypeId 来自菜单 query,是后端数据范围校验必填项;pageSize 上限 100,按页累加。
  */
 async function fetchRecentAlarms() {
   const params = {
-    rootTypeId: MANHOLE_ROOT_TYPE_ID,
+    rootTypeId: rootTypeId.value,
     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 res = await getHomeAlarms({ ...params, pageNum: page, pageSize: ALARM_PAGE_SIZE })
     const raw = res.rows || res.data?.records || res.data || []
     const pageRecords = Array.isArray(raw) ? raw : []
     records.push(...pageRecords)
@@ -623,6 +686,12 @@ onMounted(() => {
   loadMarkerImages()
   loadAll()
 })
+// 同一组件被四个菜单复用:切换子系统菜单(标签页/keep-alive)后按新的 rootTypeId 重载
+watch(rootTypeId, () => {
+  gisDevices.value = []
+  pipeFeatures.value = []
+  loadAll()
+})
 </script>
 
 <!-- ==================== 全局样式(非 scoped):el-table 浅色主题(与 realTimeShow.vue 一致) ==================== -->
@@ -697,6 +766,13 @@ onMounted(() => {
 }
 .brand { flex: none; min-width: 210px; }
 .brand-title { font-size: 20px; font-weight: 700; letter-spacing: 2px; color: #0a1628; }
+.scope-badge {
+  display: inline-flex; margin-top: 6px; padding: 2px 8px; border-radius: 10px;
+  background: #eef6ff; border: 1px solid #d6e6ff; color: #1677ff;
+  font-size: 11px; font-family: ui-monospace, Menlo, Consolas, monospace;
+}
+.scope-badge.invalid { background: #fff1f0; border-color: #ffccc7; color: #ef4444; }
+.scope-alert { flex: none; }
 .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; }
@@ -790,6 +866,9 @@ onMounted(() => {
   box-shadow: 0 2px 8px rgba(15, 34, 67, 0.1);
   pointer-events: auto;
 }
+/* 设备详情弹窗 append-to-body,z-index 由 Element Plus 动态分配(2000+);
+   弹窗打开时把图例压到遮罩下面,避免浮在弹窗内容上。 */
+.map-legend.behind-dialog { z-index: 1; }
 .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;
@@ -804,6 +883,13 @@ onMounted(() => {
 .legend-dot.online { background: #16a34a; }
 .legend-dot.offline { background: #94a3b8; }
 .legend-dot.alarm { background: #ef4444; }
+.legend-line { width: 12px; height: 4px; border-radius: 2px; flex: none; background: #16a34a; }
+.pipe-legend { display: flex; gap: 8px; margin-top: 6px; padding-top: 6px; border-top: 1px dashed #e3e9f2; font-size: 11px; color: #97a3b7; }
+.pipe-legend span { display: inline-flex; align-items: center; gap: 3px; }
+.pipe-swatch { width: 8px; height: 8px; border-radius: 2px; }
+.pipe-swatch.normal { background: #16a34a; }
+.pipe-swatch.maintain { background: #ffb020; }
+.pipe-swatch.abnormal { 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; }
 

+ 0 - 14
src/views/subSystem/pipeNetwork/phome/pHome.vue

@@ -1,14 +0,0 @@
-<!--管网设施智能管控系统-->
-<script setup>
-
-</script>
-
-<template>
-  <div class="app-container">
-    <h1>管网设施智能管控系统123</h1>
-  </div>
-</template>
-
-<style scoped lang="scss">
-
-</style>

+ 0 - 1170
src/views/subSystem/waterSupply/components/WaterSupplyHome.vue

@@ -1,1170 +0,0 @@
-<template>
-  <div class="water-page home-page">
-    <div class="page-heading">
-      <div class="page-title">
-        <span class="page-title-icon"><el-icon><HomeFilled /></el-icon></span>
-        <div>
-          <h2>供水首页工作台</h2>
-          <span class="muted">面向值班态的一站式供水运行工作台,仅统计供水设施与供水监测设备</span>
-        </div>
-      </div>
-      <div class="heading-actions">
-        <el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
-      </div>
-    </div>
-
-    <el-alert v-if="loadError" class="load-error" type="error" show-icon :closable="false"
-              title="部分数据加载失败" :description="loadError">
-      <el-button size="small" type="primary" :loading="loading" @click="loadAll">重试</el-button>
-    </el-alert>
-
-    <!-- A 指挥台 -->
-    <section class="home-section">
-      <div class="metric-grid">
-        <div v-for="card in homeKpis" :key="card.key" class="metric">
-          <div class="metric-icon"><el-icon><component :is="kpiIcon(card.key)"/></el-icon></div>
-          <div class="metric-body">
-            <span>{{ card.label }}</span>
-            <div class="metric-value">
-              <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="workbench-grid">
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Bell /></el-icon><span>实时报警</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/monitorAlarm/index')">前往报警管理</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in alarmRows" :key="item.id || item.deviceCode" type="button"
-                    class="compact-item" @click="openRealtimeAlarm(item)">
-              <span class="status-dot dot-alarm" />
-              <span class="compact-main">
-                <b>{{ item.deviceName }}</b>
-                <small>{{ item.deviceCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="item.displayStatusType">{{ item.displayStatusText }}</el-tag>
-                <el-tag size="small" :type="levelType(item.alarmLevel)">{{ alarmLevels.find(level => Number(level.value) === Number(item.alarmLevel))?.label || item.alarmLevelText }}</el-tag>
-                <small>{{ item.alarmTime }}</small>
-              </span>
-            </button>
-            <el-empty v-if="!alarmRows.length" description="当前无实时报警" :image-size="60" />
-          </div>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel map-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><MapLocation /></el-icon><span>全域态势</span>
-              <div class="map-legend">
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.alarm }" />报警/异常
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.workOrder }" />工单/检修中
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.warning }" />预警/欠费
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.offline }" />离线
-                <i class="legend-dot" :style="{ background: gisPointStatusColors.normal }" />正常
-              </div>
-              <div class="panel-links">
-                <el-button link type="primary" class="panel-link" @click="goPage('/facility/gis')">设施</el-button>
-                <el-button link type="primary" class="panel-link" @click="goPage('/device/gis')">设备</el-button>
-                <el-button link type="primary" class="panel-link" @click="goPage('/waterSupplyAlarmGIS', { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID })">报警</el-button>
-              </div>
-            </div>
-          </template>
-          <div class="map-stage">
-            <GisMapCanvas
-              ref="overviewMapRef"
-              :points="overviewMapPoints"
-              :lines="overviewMapLines"
-              cluster-label="供水点位"
-              :center="overviewMapCenter"
-              :zoom="overviewMapZoom"
-              @point-click="openOverviewPoint"
-              @line-click="openOverviewPoint"
-              @map-click="overviewDetailOpen = false"
-              @ready="handleOverviewMapReady"
-              @error="overviewMapError = $event"
-            />
-            <el-alert v-if="overviewMapError" class="map-inline-alert" type="warning" show-icon :closable="false"
-                      :title="overviewMapError">
-              <el-button link type="primary" @click="loadAll">重试</el-button>
-            </el-alert>
-            <el-empty v-else-if="overviewLoaded && !overviewMapPoints.length && !overviewMapLines.length"
-                      class="map-inline-empty" description="暂无可展示的全域点位" :image-size="70" />
-          </div>
-          <el-drawer v-model="overviewDetailOpen" title="点位详情" size="420px" direction="rtl"
-                     class="detail-drawer" append-to-body>
-            <div v-if="overviewDetail" class="overview-detail">
-              <section class="detail-hero">
-                <h3>{{ overviewDetail.name }}</h3>
-                <p>{{ overviewDetail.code }}</p>
-                <div class="status-tags">
-                  <el-tag size="small" :type="gisStatusTagType(overviewDetail.statusKey)">
-                    {{ overviewDetail.statusText || gisStatusLabel(overviewDetail.statusKey) }}
-                  </el-tag>
-                  <el-tag v-if="overviewDetail.meta" size="small" type="info">{{ overviewDetail.meta }}</el-tag>
-                </div>
-              </section>
-              <section class="detail-section">
-                <h4>点位信息</h4>
-                <dl>
-                  <dt>名称</dt><dd>{{ overviewDetail.name }}</dd>
-                  <dt>编码</dt><dd>{{ overviewDetail.code }}</dd>
-                  <dt>类别</dt><dd>{{ overviewKindLabel(overviewDetail.kind) }}</dd>
-                  <dt>状态</dt><dd>{{ overviewDetail.statusText || gisStatusLabel(overviewDetail.statusKey) }}</dd>
-                  <dt>类型 / 报警</dt><dd>{{ overviewDetail.meta }}</dd>
-                  <dt>经度</dt><dd>{{ overviewDetail.lng }}</dd>
-                  <dt>纬度</dt><dd>{{ overviewDetail.lat }}</dd>
-                </dl>
-              </section>
-            </div>
-            <el-empty v-else description="请点击地图点位查看详情" :image-size="70" />
-          </el-drawer>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel order-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Tickets /></el-icon><span>工单进展</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/device/maintain')">设备维修派单</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in workOrderRows" :key="item.orderId || item.orderNo" type="button"
-                    class="compact-item" @click="goPage('/device/maintain', { keyword: item.orderNo })">
-              <span class="status-dot" :class="dotByOrderType(item.orderStatusType)" />
-              <span class="compact-main">
-                <b>{{ item.orderNo }}</b>
-                <small>{{ item.deviceName || item.deviceCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="item.orderStatusType">{{ item.orderStatusText }}</el-tag>
-              </span>
-            </button>
-            <el-empty v-if="!workOrderRows.length" description="当前暂无供水工单" :image-size="60" />
-          </div>
-          <div class="order-brief">
-            <span>待处理 <b>{{ workOrderStat.pendingCount }}</b></span>
-            <span>处理中 <b>{{ workOrderStat.processingCount }}</b></span>
-            <span>待验收 <b>{{ workOrderStat.verifyingCount }}</b></span>
-            <span>已完成 <b>{{ workOrderStat.finishedCount }}</b></span>
-          </div>
-        </el-card>
-      </div>
-    </section>
-
-    <!-- B 异常待办 -->
-    <section class="home-section">
-      <div class="workbench-grid workbench-grid--equal">
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Warning /></el-icon><span>异常待办</span>
-              <el-button link type="primary" class="panel-link" @click="goPage('/device/abnormal')">查看全部</el-button>
-            </div>
-          </template>
-          <div class="compact-list">
-            <button v-for="item in abnormalTodoRows" :key="item.equipmentId" type="button"
-                    class="compact-item" :class="{ active: selectedDevice && selectedDevice.equipmentId === item.equipmentId }"
-                    @click="selectAbnormal(item)">
-              <span class="status-dot" :class="abnormalDot(item)" />
-              <span class="compact-main">
-                <b>{{ item.equipmentName || item.equipmentCode }}</b>
-                <small>{{ item.equipmentCode }}</small>
-              </span>
-              <span class="compact-meta">
-                <el-tag size="small" :type="abnormalType(item)">{{ item.abnormalReasonText || '设备异常' }}</el-tag>
-              </span>
-            </button>
-            <el-empty v-if="!abnormalTodoRows.length" description="当前无异常设备待办" :image-size="60" />
-          </div>
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel detail-panel">
-          <template #header>
-            <div class="card-title"><el-icon><Cpu /></el-icon><span>设备详情</span></div>
-          </template>
-          <div v-if="selectedDevice" class="device-summary">
-            <div class="summary-top">
-              <div class="summary-name">{{ selectedDevice.name }}</div>
-              <div class="summary-tags">
-                <el-tag size="small" :type="Number(selectedDevice.alarmStatus) === 1 ? 'danger' : 'success'">{{ selectedDevice.alarmStatusText }}</el-tag>
-                <el-tag size="small" :type="Number(selectedDevice.onlineStatus) === 1 ? 'success' : 'info'">{{ selectedDevice.onlineStatusText }}</el-tag>
-                <el-tag v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" size="small"
-                        :type="selectedDevice.workOrderStatusCode != null ? orderType(selectedDevice.workOrderStatusCode) : 'info'">
-                  {{ selectedDevice.workOrderStatusText }}
-                </el-tag>
-              </div>
-            </div>
-            <el-descriptions :column="1" border size="small">
-              <el-descriptions-item label="设备编码">{{ selectedDevice.code }}</el-descriptions-item>
-              <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
-              <el-descriptions-item label="所属区域">{{ selectedDevice.district || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="安装位置">{{ selectedDevice.location || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="报警等级">{{ alarmLevels.find(level => Number(level.value) === Number(selectedDevice.alertLevel ?? selectedDevice.alarmLevel))?.label || selectedDevice.alarmLevelText }}</el-descriptions-item>
-              <el-descriptions-item label="异常原因">{{ selectedDevice.abnormalReasonText || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="关联工单">
-                <el-button v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" link type="primary"
-                           @click="goPage('/device/maintain', { keyword: selectedDevice.relatedOrderNo })">
-                  {{ selectedDevice.relatedOrderNo }}
-                </el-button>
-                <span v-else>-</span>
-              </el-descriptions-item>
-            </el-descriptions>
-          </div>
-          <el-empty v-else description="从左侧异常待办中选择设备" :image-size="70" />
-        </el-card>
-
-        <el-card shadow="never" class="water-panel home-panel">
-          <template #header>
-            <div class="card-title"><el-icon><Grid /></el-icon><span>快捷入口</span></div>
-          </template>
-          <div class="quick-grid">
-            <button v-for="entry in quickEntries" :key="entry.key" type="button" class="quick-entry"
-                    @click="goPage(entry.suffix, entry.query)">
-              <el-icon><component :is="entryIcon(entry.key)"/></el-icon>
-              <span>{{ entry.label }}</span>
-            </button>
-          </div>
-        </el-card>
-      </div>
-    </section>
-
-    <!-- C 数据总览 -->
-    <section class="home-section">
-      <div class="metric-grid">
-        <div v-for="card in overviewCards" :key="card.key" class="metric">
-          <div class="metric-icon"><el-icon><component :is="overviewIcon(card.key)"/></el-icon></div>
-          <div class="metric-body">
-            <span>{{ card.label }}</span>
-            <div class="metric-value">
-              <strong :class="card.tone">{{ card.value }}</strong><small>{{ card.unit }}</small>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="chart-grid">
-        <el-card shadow="never" class="water-panel chart-card trend-card">
-          <template #header>
-            <div class="card-title">
-              <el-icon><TrendCharts /></el-icon><span>报警趋势</span>
-              <span class="muted">近 {{ trendDays }} 天 · 按四个报警等级分别统计</span>
-            </div>
-          </template>
-          <div ref="trendChartRef" class="chart" />
-        </el-card>
-        <el-card shadow="never" class="water-panel chart-card">
-          <template #header>
-            <div class="card-title"><el-icon><PieChart /></el-icon><span>报警分类占比</span></div>
-          </template>
-          <div ref="warningPieRef" class="chart" />
-        </el-card>
-        <el-card shadow="never" class="water-panel chart-card">
-          <template #header>
-            <div class="card-title"><el-icon><DataAnalysis /></el-icon><span>设备分类占比</span></div>
-          </template>
-          <div ref="devicePieRef" class="chart" />
-        </el-card>
-      </div>
-    </section>
-  </div>
-</template>
-
-<script setup>
-import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
-import { useRouter } from 'vue-router'
-import * as echarts from 'echarts'
-import {
-  Bell, BellFilled, Connection, Cpu, DataAnalysis, Finished, Grid, HomeFilled,
-  MapLocation, OfficeBuilding, PieChart, Refresh, Tickets, TrendCharts,
-  Warning, WarningFilled
-} from '@element-plus/icons-vue'
-import { getAlarmStats, getDeviceGisAllPoints,
-  getWaterFacilityFeatures, getWaterFacilityOverview,
-  listRealtimeAlarms } from '@/api/pipeNetwork/waterSupply'
-import { getAlarmDashboard, getAlarmMap } from '@/api/alarm'
-import { getEquipmentStatusStatistics, listEquipment } from '@/api/pipeNetwork/pdeviceInform'
-import { listWorkOrder, workOrderStatusCount } from '@/api/pipeNetwork/ops/workOrder'
-import { normalizeGeoJson, normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
-import { normalizeDeviceGisPoints } from '@/utils/waterSupplyUiModel'
-import { gisPointStatusColors, gisPointStatusLabels, gisPointStatusTagTypes } from '@/utils/gisMapColors'
-import { buildFacilityMapModel } from '@/utils/waterFacilityGisModel'
-import { resolveWaterHomeRoute } from '@/router/waterHomeRouteResolver'
-import GisMapCanvas from '@/components/GisMapCanvas.vue'
-import { useDict } from '@/utils/dict'
-import {
-  HOME_QUICK_ENTRIES, WATER_DEVICE_ROOT_TYPE_ID, buildHomeGisOverview, buildHomeWorkOrderQuery,
-  normalizeAbnormalTodoRows, normalizeAlarmOverviewCards, normalizeAlarmTrend, normalizeDeviceSummary,
-  normalizeDeviceStatistics, normalizeHomeKpis, normalizePieRatio, normalizeRealtimeAlarmRows, realtimeAlarmJumpTarget,
-  normalizeWorkOrderRows, normalizeWorkOrderStatusStat
-} from '@/utils/waterHomeModel'
-
-const { alarm_level: alarmLevels } = useDict('alarm_level')
-const router = useRouter()
-
-const loading = ref(true)
-const loadError = ref('')
-const trendDays = 7
-const quickEntries = HOME_QUICK_ENTRIES
-
-const facilityRows = ref([])
-const ledgerStats = ref({})
-const alarmStats = ref({})
-const alarmDashboard = ref({})
-const alarmRows = ref([])
-const workOrderRows = ref([])
-const workOrderStat = ref({})
-const abnormalTodoRows = ref([])
-const overviewMapPoints = ref([])
-const overviewMapLines = ref([])
-const overviewDeviceRaw = ref([])
-const overviewMapCenter = { lng: 110.3965, lat: 28.4638 }
-const overviewMapZoom = 15
-const overviewMapError = ref('')
-const overviewLoaded = ref(false)
-const overviewDetailOpen = ref(false)
-const overviewDetail = ref(null)
-const overviewMapRef = ref(null)
-const selectedDevice = ref(null)
-
-const homeKpis = ref([])
-const overviewCards = ref([])
-const trendData = ref({ dates: [], levels: [] })
-const warningRatio = ref([])
-const deviceRatio = ref([])
-
-const trendChartRef = ref(null)
-const warningPieRef = ref(null)
-const devicePieRef = ref(null)
-let trendChart = null
-let warningPie = null
-let devicePie = null
-
-const KPI_ICON = {
-  facility: OfficeBuilding,
-  device: Cpu,
-  online: Connection,
-  alarm: BellFilled
-}
-
-const OVERVIEW_ICON = {
-  alarmTotal: BellFilled,
-  alarmingDevice: WarningFilled,
-  verifyingOrder: Tickets,
-  onlineRate: TrendCharts
-}
-
-const ENTRY_ICON = {
-  abnormal: WarningFilled,
-  alarm: BellFilled,
-  audit: Finished,
-  map: MapLocation
-}
-
-function kpiIcon(key) { return KPI_ICON[key] || OfficeBuilding }
-function overviewIcon(key) { return OVERVIEW_ICON[key] || DataAnalysis }
-function entryIcon(key) { return ENTRY_ICON[key] || Grid }
-function levelType(level) {
-  const meta = { 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' }
-  return meta[Number(level)] || 'info'
-}
-function orderType(status) {
-  const meta = {
-    0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning',
-    4: 'primary', 5: 'success', 6: 'warning', 7: 'info'
-  }
-  return meta[Number(status)] || 'info'
-}
-function dotByOrderType(type) {
-  const map = { danger: 'dot-danger', warning: 'dot-warning', primary: 'dot-primary', success: 'dot-success', info: 'dot-info' }
-  return map[type] || 'dot-info'
-}
-function abnormalDot(row) {
-  if (Number(row.alarmStatus) === 1) return 'dot-alarm'
-  if (Number(row.onlineStatus) === 0) return 'dot-offline'
-  return 'dot-warning'
-}
-function abnormalType(row) {
-  if (Number(row.alarmStatus) === 1) return 'danger'
-  if (Number(row.onlineStatus) === 0) return 'info'
-  return 'warning'
-}
-
-async function settle(fn, fallback = null, onError = null) {
-  try {
-    return await fn()
-  } catch (error) {
-    console.warn('供水首页数据片段加载失败', error)
-    // 失败上报必须在这里完成:本函数已吞掉异常并返回兜底值,
-    // 调用侧再挂 .catch 永远不会触发,“部分数据加载失败”提示会变成死代码。
-    onError?.()
-    if (fallback !== null) return fallback
-    throw error
-  }
-}
-
-async function loadAll() {
-  loading.value = true
-  loadError.value = ''
-  const problems = []
-  const recordProblem = label => () => { problems.push(label) }
-
-  const [overview, ledger, alarm, dashboard, realtime, abnormal, orderStat, orders, deviceGis] = await Promise.allSettled([
-    settle(() => getWaterFacilityOverview(), [], recordProblem('设施概览')),
-    settle(() => getEquipmentStatusStatistics(WATER_DEVICE_ROOT_TYPE_ID), {}, recordProblem('设备统计')),
-    settle(() => getAlarmStats(), {}, recordProblem('报警统计')),
-    settle(() => getAlarmDashboard({ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID, days: trendDays }), {}, recordProblem('报警态势')),
-    settle(() => listRealtimeAlarms(), [], recordProblem('实时报警')),
-    // 异常待办 = 当前处于报警中的供水设备;设备状态由 equipment_status 维护
-    settle(() => listEquipment({ pageNum: 1, pageSize: 50, rootTypeId: WATER_DEVICE_ROOT_TYPE_ID, alarmStatus: 1 }), { records: [], total: 0 }, recordProblem('异常待办')),
-    settle(() => workOrderStatusCount(buildHomeWorkOrderQuery()), { total: 0, counts: [] }, recordProblem('工单统计')),
-    settle(() => listWorkOrder(buildHomeWorkOrderQuery({ pageSize: 20 })), { records: [], total: 0 }, recordProblem('工单列表')),
-    settle(() => getDeviceGisAllPoints(), { features: [] }, recordProblem('设备点位'))
-  ])
-
-  const overviewData = unwrapResponse(overview.value || [])
-  facilityRows.value = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
-  ledgerStats.value = normalizeDeviceStatistics(unwrapResponse(ledger.value || {}))
-  alarmStats.value = unwrapResponse(alarm.value || {})
-  alarmDashboard.value = unwrapResponse(dashboard.value || {})
-  alarmRows.value = normalizeRealtimeAlarmRows(unwrapResponse(realtime.value || []), 20)
-  const abnormalPage = normalizePage(abnormal.value || { records: [], total: 0 })
-  abnormalTodoRows.value = normalizeAbnormalTodoRows(abnormalPage.records, 20)
-  workOrderStat.value = normalizeWorkOrderStatusStat(unwrapResponse(orderStat.value || { total: 0, counts: [] }))
-  const orderList = unwrapResponse(orders.value || { records: [] })
-  workOrderRows.value = normalizeWorkOrderRows(orderList?.records || [], 20)
-
-  const gisData = unwrapResponse(deviceGis.value || {})
-  overviewDeviceRaw.value = Array.isArray(gisData) ? gisData : (gisData?.features || [])
-  await loadOverviewMap(recordProblem)
-
-  homeKpis.value = normalizeHomeKpis({ facilityRows: facilityRows.value, ledgerStats: ledgerStats.value, alarmStats: alarmStats.value })
-  overviewCards.value = normalizeAlarmOverviewCards({
-    dashboard: alarmDashboard.value,
-    deviceStats: ledgerStats.value,
-    workOrderStat: workOrderStat.value
-  })
-  trendData.value = normalizeAlarmTrend(alarmDashboard.value?.trend || [])
-  warningRatio.value = normalizePieRatio(alarmDashboard.value?.warningTypeRatio || [], '未分类', 6)
-  deviceRatio.value = normalizePieRatio(alarmDashboard.value?.deviceTypeRatio || [], '未分类', 6)
-
-  if (problems.length) loadError.value = problems.join(';')
-  loading.value = false
-  await nextTick()
-  renderCharts()
-}
-
-function selectAbnormal(row) {
-  selectedDevice.value = normalizeDeviceSummary(row)
-}
-
-async function loadOverviewMap(recordProblem) {
-  overviewMapError.value = ''
-  const [facilityRes, alarmRes] = await Promise.all([
-    settle(() => getWaterFacilityFeatures(), { features: [] }, recordProblem('设施GIS点位')),
-    settle(() => getAlarmMap({ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }), [], recordProblem('报警点位'))
-  ])
-  const facilityModel = buildFacilityMapModel(normalizeGeoJson(facilityRes))
-  const overviewModel = buildHomeGisOverview({
-    facilityPoints: facilityModel.points,
-    facilityLines: facilityModel.lines,
-    devicePoints: normalizeDeviceGisPoints(overviewDeviceRaw.value),
-    alarmPoints: unwrapResponse(alarmRes) || []
-  })
-  overviewMapPoints.value = overviewModel.points
-  overviewMapLines.value = overviewModel.lines
-  overviewLoaded.value = true
-  await nextTick()
-  overviewMapRef.value?.fitBounds()
-}
-
-function handleOverviewMapReady() {
-  nextTick(() => overviewMapRef.value?.fitBounds())
-}
-
-function openOverviewPoint(point) {
-  overviewDetail.value = point || null
-  overviewDetailOpen.value = Boolean(point)
-}
-
-function openRealtimeAlarm(item) {
-  const target = realtimeAlarmJumpTarget(item)
-  goPage(target.path, target.query)
-}
-
-function resolveRoutePath(suffix) {
-  const matched = resolveWaterHomeRoute(router.getRoutes(), suffix)
-  return matched ? matched.path : ''
-}
-
-function goPage(suffix, query = {}) {
-  const path = resolveRoutePath(suffix)
-  if (!path) {
-    console.warn(`未找到供水入口路由: ${suffix}`)
-    return
-  }
-  router.push({ path, query }).catch(() => {})
-}
-
-function gisStatusLabel(statusKey) {
-  return gisPointStatusLabels[statusKey] || '正常'
-}
-
-function gisStatusTagType(statusKey) {
-  return gisPointStatusTagTypes[statusKey] || 'success'
-}
-
-function overviewKindLabel(kind) {
-  return { device: '监测设备', alarm: '报警', facility: '供水设施' }[kind] || '点位'
-}
-
-watch(alarmLevels, () => { if (trendChart) renderTrendChart() })
-function renderCharts() {
-  renderTrendChart()
-  warningPie?.dispose()
-  warningPie = renderPieChart(warningPieRef.value, warningRatio.value, '报警数')
-  devicePie?.dispose()
-  devicePie = renderPieChart(devicePieRef.value, deviceRatio.value, '设备数')
-}
-
-function renderTrendChart() {
-  if (!trendChartRef.value) return
-  trendChart?.dispose()
-  trendChart = echarts.init(trendChartRef.value)
-  const { dates, levels } = trendData.value
-  const palette = ['#f56c6c', '#e6a23c', '#409eff', '#67c23a']
-  trendChart.setOption({
-    tooltip: { trigger: 'axis' },
-    legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
-    grid: { left: 34, right: 14, top: 18, bottom: 48, containLabel: true },
-    xAxis: {
-      type: 'category',
-      data: dates,
-      axisLine: { lineStyle: { color: '#cbd5e1' } },
-      axisLabel: { color: '#64748b' }
-    },
-    yAxis: {
-      type: 'value',
-      minInterval: 1,
-      splitLine: { lineStyle: { color: '#eef2f7' } },
-      axisLabel: { color: '#64748b' }
-    },
-    color: palette,
-    series: levels.map((level, index) => ({
-      name: alarmLevels.value.find(item => Number(item.value) === level.level)?.label || level.text,
-      type: 'bar',
-      stack: 'alarm',
-      barWidth: 26,
-      data: level.data,
-      itemStyle: { borderRadius: index === levels.length - 1 ? [5, 5, 0, 0] : 0 }
-    }))
-  })
-}
-
-function renderPieChart(container, data, unit) {
-  if (!container) return null
-  const total = data.reduce((sum, item) => sum + item.count, 0)
-  const chart = echarts.init(container)
-  chart.setOption({
-    tooltip: { trigger: 'item', formatter: `{b}:{c} ${unit}({d}%)` },
-    legend: { bottom: 0, icon: 'circle', itemWidth: 9, itemHeight: 9, textStyle: { color: '#475569' } },
-    title: {
-      text: String(total),
-      subtext: unit,
-      left: 'center',
-      top: '32%',
-      textStyle: { color: '#1e293b', fontSize: 24, fontWeight: 700 },
-      subtextStyle: { color: '#64748b', fontSize: 12 }
-    },
-    color: ['#2563eb', '#14b8a6', '#f59e0b', '#ef4444', '#8b5cf6', '#64748b'],
-    series: [{
-      type: 'pie',
-      radius: ['48%', '68%'],
-      center: ['50%', '43%'],
-      data: data.map(item => ({ name: item.name, value: item.count })),
-      label: { formatter: '{b}\n{d}%', color: '#475569', lineHeight: 17 },
-      itemStyle: { borderColor: '#fff', borderWidth: 3, borderRadius: 8 },
-      emphasis: { scaleSize: 7 }
-    }]
-  })
-  return chart
-}
-
-function handleResize() {
-  trendChart?.resize()
-  warningPie?.resize()
-  devicePie?.resize()
-}
-
-onMounted(async () => {
-  await loadAll()
-  window.addEventListener('resize', handleResize)
-})
-
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', handleResize)
-  trendChart?.dispose()
-  warningPie?.dispose()
-  devicePie?.dispose()
-})
-</script>
-
-<style lang="scss">
-@use './waterSupplyPageTheme.scss';
-</style>
-
-<style scoped>
-.home-page {
-  background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
-}
-
-.heading-actions {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.page-title-icon {
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #fff;
-  background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
-  border-radius: 8px;
-  box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
-}
-
-.load-error {
-  margin-bottom: 14px;
-}
-
-.home-section {
-  margin-bottom: 20px;
-}
-
-.metric-grid {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 12px;
-  margin-bottom: 14px;
-}
-
-.metric {
-  display: flex;
-  align-items: center;
-  gap: 14px;
-  min-width: 0;
-  padding: 14px 16px;
-  background: #fff;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04);
-}
-
-.metric-icon {
-  position: static;
-  flex: 0 0 42px;
-  display: grid;
-  place-items: center;
-  width: 42px;
-  height: 42px;
-  color: #2563eb;
-  background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
-  border-radius: 8px;
-  font-size: 22px;
-}
-
-.metric-body {
-  min-width: 0;
-}
-
-.metric-body span {
-  display: block;
-  overflow: hidden;
-  margin-bottom: 3px;
-  color: #64748b;
-  font-size: 13px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.metric-value {
-  display: flex;
-  align-items: baseline;
-  gap: 5px;
-}
-
-.metric-value strong {
-  color: #1e293b;
-  font-size: 24px;
-  line-height: 1.2;
-}
-
-.metric-value strong.danger { color: #dc2626; }
-.metric-value strong.warning { color: #d97706; }
-
-.metric-value small {
-  color: #94a3b8;
-}
-
-.workbench-grid {
-  display: grid;
-  grid-template-columns: minmax(240px, 1fr) minmax(0, 3fr) minmax(240px, 1fr);
-  gap: 12px;
-  align-items: stretch;
-  min-height: 520px;
-}
-
-.workbench-grid--equal {
-  grid-template-columns: repeat(3, minmax(0, 1fr));
-  min-height: auto;
-}
-
-.home-panel {
-  min-width: 0;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
-}
-
-/* 让卡片本身纵向弹性布局 */
-.order-panel {
-  display: flex;
-  flex-direction: column;
-}
-
-/* 让卡片内容区占满剩余高度,并变成纵向弹性盒子 */
-.order-panel :deep(.el-card__body) {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-}
-
-/* 关键:让底部统计栏自动推到最下方 */
-.order-panel .order-brief {
-  margin-top: auto;
-}
-
-.map-panel {
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
-}
-
-.map-panel :deep(.el-card__body) {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  min-height: 0;
-}
-
-.home-panel :deep(.el-card__header) {
-  padding: 12px 16px;
-  border-bottom: 1px solid #e1ebf3;
-  background: #fbfdff;
-}
-
-.home-panel :deep(.el-card__body) {
-  padding: 12px 14px;
-}
-
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  color: #1e293b;
-  font-weight: 600;
-}
-
-.card-title :deep(.el-icon) {
-  color: #2563eb;
-  font-size: 18px;
-}
-
-.card-title .muted {
-  margin-left: auto;
-  font-size: 12px;
-  font-weight: 400;
-}
-
-.panel-link {
-  margin-left: auto;
-}
-
-.panel-links {
-  display: flex;
-  align-items: center;
-  gap: 2px;
-  margin-left: auto;
-}
-
-.panel-links .panel-link {
-  margin-left: 0;
-}
-
-.legend-dot {
-  display: inline-block;
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-}
-
-.compact-list {
-  display: flex;
-  flex-direction: column;
-  gap: 7px;
-  min-height: 220px;
-  max-height: 360px;
-  overflow-y: auto;
-  padding-right: 2px;
-}
-
-.compact-item {
-  display: grid;
-  grid-template-columns: 10px minmax(0, 1fr) auto;
-  gap: 9px;
-  align-items: center;
-  width: 100%;
-  padding: 9px 10px;
-  text-align: left;
-  background: #fff;
-  border: 1px solid #eef2f7;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.15s ease;
-}
-
-.compact-item:hover {
-  background: #f6fbff;
-  border-color: #bfdbfe;
-}
-
-.compact-item.active {
-  background: #eef6ff;
-  border-color: #93c5fd;
-}
-
-.status-dot {
-  width: 9px;
-  height: 9px;
-  border-radius: 50%;
-  background: #94a3b8;
-}
-
-.dot-alarm { background: #dc2626; }
-.dot-offline { background: #94a3b8; }
-.dot-normal { background: #16a34a; }
-.dot-warning { background: #d97706; }
-.dot-danger { background: #dc2626; }
-.dot-primary { background: #2563eb; }
-.dot-success { background: #16a34a; }
-.dot-info { background: #94a3b8; }
-.dot-workorder { background: #2563eb; }
-
-.compact-main {
-  display: block;
-  min-width: 0;
-}
-
-.compact-main b,
-.compact-main small {
-  display: block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.compact-main b {
-  color: #1e293b;
-  font-size: 13px;
-  font-weight: 600;
-}
-
-.compact-main small {
-  margin-top: 2px;
-  color: #94a3b8;
-  font-size: 12px;
-}
-
-.compact-meta {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  gap: 3px;
-  min-width: 0;
-}
-
-.compact-meta small {
-  color: #94a3b8;
-  font-size: 11px;
-  white-space: nowrap;
-}
-
-.map-stage {
-  position: relative;
-  flex: 1;
-  min-height: 0;
-  height: auto;
-  overflow: hidden;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  background: #eef6ff;
-}
-
-.map-stage :deep(.gis-map-canvas) {
-  width: 100%;
-  height: 100%;
-  min-height: 0;
-}
-
-.map-inline-alert {
-  position: absolute;
-  left: 16px;
-  bottom: 16px;
-  z-index: 3;
-  width: 340px;
-}
-
-.map-inline-empty {
-  position: absolute;
-  left: 50%;
-  top: 50%;
-  z-index: 2;
-  transform: translate(-50%, -50%);
-}
-
-.map-legend {
-  display: flex;
-  align-items: center;
-  gap: 7px;
-  margin-left: auto;
-  color: #64748b;
-  font-size: 12px;
-}
-
-.overview-detail {
-  min-height: 240px;
-}
-
-.detail-hero h3,
-.detail-section h4 {
-  margin: 0 0 8px;
-  color: #1c4b78;
-}
-
-.detail-hero p {
-  margin: 0 0 10px;
-  color: #678197;
-  font-size: 13px;
-}
-
-.status-tags {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
-}
-
-.detail-section {
-  margin-top: 18px;
-  padding-top: 12px;
-  border-top: 1px solid #e5eef7;
-}
-
-.detail-section dl {
-  display: grid;
-  grid-template-columns: 92px minmax(0, 1fr);
-  gap: 7px 10px;
-  margin: 0;
-  font-size: 13px;
-}
-
-.detail-section dt {
-  color: #71889c;
-}
-
-.detail-section dd {
-  margin: 0;
-  color: #2b4c67;
-  overflow-wrap: anywhere;
-}
-
-.order-brief {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 6px;
-  margin-top: 10px;
-  padding-top: 10px;
-  border-top: 1px solid #eef2f7;
-}
-
-.order-brief span {
-  color: #64748b;
-  font-size: 12px;
-}
-
-.order-brief b {
-  margin-left: 3px;
-  color: #17324d;
-  font-size: 14px;
-}
-
-.device-summary {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.detail-panel :deep(.el-card__body) {
-  display: flex;
-  flex-direction: column;
-}
-
-.detail-panel .device-summary {
-  flex: 1;
-}
-
-.summary-top {
-  display: flex;
-  align-items: flex-start;
-  justify-content: space-between;
-  gap: 10px;
-  flex-wrap: wrap;
-}
-
-.summary-name {
-  color: #17324d;
-  font-size: 16px;
-  font-weight: 700;
-}
-
-.summary-tags {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: flex-end;
-  gap: 6px;
-}
-
-.detail-panel :deep(.el-descriptions__label) {
-  width: 86px;
-  color: #64748b;
-  background: #f8fafc;
-  white-space: nowrap;
-}
-
-.quick-grid {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 10px;
-}
-
-.quick-entry {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  min-width: 0;
-  padding: 14px 12px;
-  color: #1e293b;
-  background: #fff;
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.15s ease;
-}
-
-.quick-entry:hover {
-  background: #f6fbff;
-  border-color: #bfdbfe;
-}
-
-.quick-entry :deep(.el-icon) {
-  flex: 0 0 20px;
-  color: #2563eb;
-  font-size: 20px;
-}
-
-.quick-entry span {
-  overflow: hidden;
-  font-size: 13px;
-  font-weight: 600;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.chart-grid {
-  display: grid;
-  grid-template-columns: 1.5fr 1fr 1fr;
-  gap: 12px;
-}
-
-.chart-card {
-  border: 1px solid #e1ebf3;
-  border-radius: 8px;
-  box-shadow: 0 5px 16px rgba(28, 75, 110, 0.05);
-}
-
-.chart-card .chart {
-  height: 260px;
-}
-
-.chart-card :deep(.el-card__header) {
-  padding: 12px 16px;
-  border-bottom: 1px solid #e1ebf3;
-  background: #fbfdff;
-}
-
-.chart-card :deep(.el-card__body) {
-  padding: 10px 12px;
-}
-
-@media (max-width: 1200px) {
-  .metric-grid {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-
-  .workbench-grid {
-    grid-template-columns: 1fr;
-  }
-
-  .chart-grid {
-    grid-template-columns: 1fr 1fr;
-  }
-
-  .trend-card {
-    grid-column: 1 / -1;
-  }
-}
-
-@media (max-width: 768px) {
-  .page-heading,
-  .heading-actions {
-    align-items: stretch;
-    flex-direction: column;
-  }
-
-  .metric-grid,
-  .chart-grid {
-    grid-template-columns: 1fr;
-  }
-
-  .trend-card {
-    grid-column: auto;
-  }
-
-  .map-legend {
-    display: none;
-  }
-
-  .panel-links {
-    display: none;
-  }
-
-  .quick-grid {
-    grid-template-columns: 1fr;
-  }
-}
-</style>

+ 0 - 2
src/views/subSystem/waterSupply/index.vue

@@ -1,2 +0,0 @@
-<script setup>import WaterSupplyHome from './components/WaterSupplyHome.vue'</script>
-<template><WaterSupplyHome /></template>

+ 0 - 2
src/views/subSystem/waterSupply/whome/wHome.vue

@@ -1,2 +0,0 @@
-<script setup>import WaterSupplyHome from '../components/WaterSupplyHome.vue'</script>
-<template><WaterSupplyHome /></template>

+ 84 - 0
tests/homeScope.test.mjs

@@ -0,0 +1,84 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { normalizeRootTypeId, collectHomeScopes, resolveHomeScope } from '../src/utils/homeScope.js'
+
+const homeMenus = [
+  {
+    path: '/subSystem/waterSupply',
+    children: [
+      { path: 'home', name: 'WaterHome', query: '{"rootTypeId":1}' }
+    ]
+  },
+  {
+    path: '/subSystem/drainage',
+    children: [
+      { path: 'home', name: 'DrainageHome', query: '{"rootTypeId":2}' }
+    ]
+  }
+]
+
+test('只接受四个设备大类,绝不回退到默认值', () => {
+  assert.equal(normalizeRootTypeId('1'), 1)
+  assert.equal(normalizeRootTypeId(4), 4)
+  assert.equal(normalizeRootTypeId(undefined), undefined)
+  assert.equal(normalizeRootTypeId(''), undefined)
+  assert.equal(normalizeRootTypeId(0), undefined)
+  assert.equal(normalizeRootTypeId(5), undefined)
+  assert.equal(normalizeRootTypeId('abc'), undefined)
+})
+
+test('URL 带 rootTypeId 时按 URL 取范围,不会变成窨井', () => {
+  const scope = resolveHomeScope({
+    query: { rootTypeId: '2' },
+    path: '/subSystem/drainage/home',
+    menus: homeMenus
+  })
+  assert.equal(scope.rootTypeId, 2)
+  assert.equal(scope.source, 'url')
+})
+
+test('URL 丢失参数时从 /getRouters 的菜单 query 恢复', () => {
+  const scope = resolveHomeScope({
+    query: {},
+    path: '/subSystem/waterSupply/home',
+    menus: homeMenus
+  })
+  assert.equal(scope.rootTypeId, 1)
+  assert.equal(scope.source, 'menu')
+})
+
+test('菜单 query 是 JSON 字符串也能解析', () => {
+  const scopes = collectHomeScopes(homeMenus)
+  assert.deepEqual(scopes.map(item => item.rootTypeId), [1, 2])
+  assert.equal(scopes[0].path, '/subSystem/waterSupply/home')
+})
+
+test('未配置 query 时不返回任何范围,页面应报错而不是查窨井数据', () => {
+  const scope = resolveHomeScope({ query: {}, path: '/subSystem/gas/home', menus: [] })
+  assert.equal(scope.rootTypeId, undefined)
+  assert.equal(scope.source, undefined)
+})
+
+test('多个首页菜单共用同一路径时由 URL 参数区分,不猜测默认值', () => {
+  const menus = [
+    { path: '/home', query: '{"rootTypeId":3}' },
+    { path: '/home', query: '{"rootTypeId":4}' }
+  ]
+  assert.equal(resolveHomeScope({ query: { rootTypeId: 3 }, path: '/home', menus }).rootTypeId, 3)
+  assert.equal(resolveHomeScope({ query: {}, path: '/home', menus }).rootTypeId, undefined)
+})
+
+test('账号只有一个已授权首页菜单时可据此兜底', () => {
+  const scope = resolveHomeScope({ query: {}, path: '/other', menus: [homeMenus[0]] })
+  assert.equal(scope.rootTypeId, 1)
+  assert.equal(scope.source, 'only-menu')
+})
+
+test('兼容 ?query={"rootTypeId":N} 形式的透传链接', () => {
+  const scope = resolveHomeScope({
+    query: { query: '{"rootTypeId":3}' },
+    path: '/subSystem/gas/home',
+    menus: []
+  })
+  assert.equal(scope.rootTypeId, 3)
+})

+ 0 - 61
tests/waterDeviceGisMap.test.mjs

@@ -1,61 +0,0 @@
-import test from 'node:test'
-import assert from 'node:assert/strict'
-import { readFile } from 'node:fs/promises'
-
-test('device gis page is a dedicated map at the public route seam', async () => {
-  const apiSource = await readFile('src/api/pipeNetwork/waterSupply.js', 'utf8')
-  const entrySource = await readFile('src/views/subSystem/waterSupply/device/gis.vue', 'utf8')
-
-  assert.match(apiSource, /export const getDeviceGisAllPoints = \(\) => get\('\/waterSupply\/device\/gis\/allPoints'\)/u)
-  assert.match(apiSource, /export const getDeviceGisAbnormalPoints = \(\) => get\('\/waterSupply\/device\/gis\/abnormalPoints'\)/u)
-  assert.match(apiSource, /export const getDeviceGisStatistics = \(\) => get\('\/waterSupply\/device\/gis\/statistics'\)/u)
-  assert.match(apiSource, /export const getDeviceGisDetail = \(id\) => get\(`\/waterSupply\/device\/gis\/detail\/\$\{id\}`\)/u)
-  assert.match(entrySource, /import WaterDeviceGisMap from '\.\.\/components\/WaterDeviceGisMap\.vue'/u)
-  assert.doesNotMatch(entrySource, /WaterDevicePage/)
-})
-
-test('device gis map exposes water filtering, exception highlight and detail behavior', async () => {
-  const source = await readFile(
-    'src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue',
-    'utf8'
-  )
-
-  assert.match(source, /getDeviceGisAllPoints/u)
-  assert.match(source, /getDeviceGisAbnormalPoints/u)
-  assert.match(source, /getDeviceGisStatistics/u)
-  assert.match(source, /getDeviceGisDetail/u)
-  assert.match(source, /equipmentTypeId/u)
-  assert.match(source, /设备类型/u)
-  assert.match(source, /searchKeyword/u)
-  assert.match(source, /搜索设备/u)
-  assert.match(source, /图例/u)
-  assert.match(source, /报警/u)
-  assert.match(source, /维修/u)
-  assert.match(source, /离线/u)
-  assert.match(source, /正常/u)
-  assert.match(source, /<GisMapCanvas/u)
-  assert.match(source, /:points="mapPoints"/u)
-  assert.match(source, /el-drawer/u)
-  assert.match(source, /设备详情/u)
-  assert.match(source, /v-loading/u)
-  assert.match(source, /暂无可显示的监测设备/u)
-  assert.match(source, /重试/u)
-  assert.match(source, /刷新/u)
-})
-
-test('device gis map separates status sources in detail text', async () => {
-  const source = await readFile(
-    'src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue',
-    'utf8'
-  )
-
-  assert.match(source, /设备状态/u)
-  assert.match(source, /在线状态/u)
-  assert.match(source, /报警状态/u)
-  assert.match(source, /报警等级/u)
-  assert.match(source, /异常原因/u)
-  assert.match(source, /最新数据/u)
-  assert.match(source, /维修记录/u)
-  assert.match(source, /selectedDetail\.longitude/u)
-  assert.match(source, /selectedDetail\.latitude/u)
-})

+ 0 - 35
tests/waterHomeRouteResolver.test.mjs

@@ -1,35 +0,0 @@
-import test from 'node:test'
-import assert from 'node:assert/strict'
-import { resolveWaterHomeRoute } from '../src/router/waterHomeRouteResolver.js'
-
-test('resolves dedicated water routes by canonical suffix', () => {
-  const route = resolveWaterHomeRoute([
-    { path: '/subSystem/waterSupply/monitorAlarm/index', meta: { title: '报警管理' } },
-    { path: '/subSystem/waterSupply/monitorAlarm/audit', meta: { title: '报警审核' } }
-  ], '/monitorAlarm/index')
-
-  assert.equal(route.path, '/subSystem/waterSupply/monitorAlarm/index')
-})
-
-test('resolves legacy shared alarm menus without changing database routes', () => {
-  const routes = [
-    { path: '/subSystem/alarm/alarmList', meta: { title: '报警管理' }, query: { rootTypeId: 1 } },
-    { path: '/subSystem/alarm/alarmAudit', meta: { title: '报警审核' }, query: { rootTypeId: 1 } }
-  ]
-
-  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/index').path, '/subSystem/alarm/alarmList')
-  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/audit').path, '/subSystem/alarm/alarmAudit')
-})
-
-test('does not select an unrelated subsystem route with the same title', () => {
-  const routes = [
-    { path: '/subSystem/drainage/monitorAlarm/index', meta: { title: '报警管理' } },
-    { path: '/subSystem/waterSupply/monitorAlarm/index', meta: { title: '报警管理' }, query: { rootTypeId: 1 } }
-  ]
-
-  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/index').path, '/subSystem/waterSupply/monitorAlarm/index')
-})
-
-test('returns null when the requested entry is not registered', () => {
-  assert.equal(resolveWaterHomeRoute([{ path: '/404', meta: { title: '页面不存在' } }], '/monitorAlarm/index'), null)
-})

+ 0 - 149
tests/waterSupplyHomeContract.test.mjs

@@ -1,149 +0,0 @@
-import test from 'node:test'
-import assert from 'node:assert/strict'
-import { readFile } from 'node:fs/promises'
-import { parse as parseSfc } from '@vue/compiler-sfc'
-import { parse as parseJs } from '@babel/parser'
-
-const homePath = 'src/views/subSystem/waterSupply/components/WaterSupplyHome.vue'
-
-test('home API imports all resolve to real named exports', async () => {
-  const { descriptor } = parseSfc(await readFile(homePath, 'utf8'))
-  const imports = parseJs(descriptor.scriptSetup.content, { sourceType: 'module' }).program.body
-    .filter(node => node.type === 'ImportDeclaration' && node.source.value.startsWith('@/api/'))
-  for (const declaration of imports) {
-    const apiPath = declaration.source.value.replace('@/', 'src/') + '.js'
-    const nodes = parseJs(await readFile(apiPath, 'utf8'), { sourceType: 'module' }).program.body
-    const names = nodes.filter(node => node.type === 'ExportNamedDeclaration').flatMap(node => {
-      if (node.declaration?.type === 'VariableDeclaration') return node.declaration.declarations.map(item => item.id.name)
-      return node.declaration?.id ? [node.declaration.id.name] : node.specifiers.map(item => item.exported.name)
-    })
-    for (const item of declaration.specifiers.filter(item => item.type === 'ImportSpecifier')) {
-      assert.ok(names.includes(item.imported.name), `${apiPath} does not export ${item.imported.name}`)
-    }
-  }
-})
-
-test('home dashboard uses the unified scoped endpoint instead of the removed ledger dashboard', async () => {
-  const source = await readFile(homePath, 'utf8')
-  assert.doesNotMatch(source, /getAlarmLedgerDashboard/u)
-  assert.match(source, /getAlarmDashboard\(\{ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID, days: trendDays \}\)/u)
-  const api = (await readFile('src/api/alarm.js', 'utf8'))
-    .replace("import request from '@/utils/request'", 'const request = config => Promise.resolve(config)')
-  const { getAlarmDashboard } = await import('data:text/javascript;base64,' + Buffer.from(api).toString('base64'))
-  assert.deepEqual(await getAlarmDashboard({ rootTypeId: 1, days: 7 }), {
-    url: '/api/alarm/dashboard', method: 'get', params: { rootTypeId: 1, days: 7 }
-  })
-})
-
-test('home removes the A/B/C placeholder headings', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.doesNotMatch(source, /A · 指挥台/u)
-  assert.doesNotMatch(source, /B · 异常待办/u)
-  assert.doesNotMatch(source, /C · 数据总览/u)
-})
-
-test('home realtime alarms split between audit and work order by display status', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /openRealtimeAlarm\(item\)/u)
-  assert.match(source, /realtimeAlarmJumpTarget\(item\)/u)
-  assert.match(source, /goPage\(target\.path, target\.query\)/u)
-  assert.match(source, /item\.displayStatusText/u)
-  assert.match(source, /item\.displayStatusType/u)
-  assert.match(source, /goPage\('\/device\/maintain'/u)
-})
-
-test('home overview uses the shared GIS map canvas and aggregates all one-map layers', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /<GisMapCanvas/u)
-  assert.match(source, /ref="overviewMapRef"/u)
-  assert.match(source, /:points="overviewMapPoints"/u)
-  assert.match(source, /:lines="overviewMapLines"/u)
-  assert.match(source, /@point-click="openOverviewPoint"/u)
-  assert.match(source, /@line-click="openOverviewPoint"/u)
-  assert.match(source, /buildHomeGisOverview/u)
-  assert.match(source, /getWaterFacilityFeatures\(\)/u)
-  assert.match(source, /getAlarmMap\(\{ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID \}\)/u)
-  assert.match(source, /alarmPoints: unwrapResponse\(alarmRes\) \|\| \[\]/u)
-  assert.doesNotMatch(source, /listAlarmMapPoints/u)
-  assert.match(source, /getDeviceGisAllPoints\(\)/u)
-})
-
-test('home overview map takes at least 60% of the same row in desktop layout', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /grid-template-columns:\s*minmax\(240px,\s*1fr\)\s+minmax\(0,\s*3fr\)\s+minmax\(240px,\s*1fr\)/u)
-})
-
-test('home work order jumps point to device maintenance dispatch page', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  const jumps = source.match(/goPage\('\/device\/maintain'/gu) || []
-  assert.ok(jumps.length >= 3, 'header, work order rows and related order should all route to device maintain')
-  assert.doesNotMatch(source, /monitorAlarm\/workOrder/u)
-  assert.match(source, />设备维修派单<\/el-button>/u)
-})
-
-test('home B-road detail panel is 1:1 with the todo and shortcut panels', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /workbench-grid--equal/u)
-  assert.match(source, /repeat\(3,\s*minmax\(0,\s*1fr\)\)/u)
-  const sectionB = source.match(/<section class="home-section">([\s\S]*?)<el-card shadow="never" class="water-panel home-panel detail-panel">/u)
-  assert.ok(sectionB, 'detail panel should live in the equal-width B section')
-})
-
-test('home list panels scroll to reveal the full queue instead of pinning a few rows', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  const listSelector = source.match(/\.compact-list\s*\{[^}]*\}/u)
-  assert.ok(listSelector, 'compact list style block exists')
-  assert.match(listSelector[0], /max-height:\s*[\w.]+/u)
-  assert.match(listSelector[0], /overflow-y:\s*auto/u)
-  assert.match(source, /listRealtimeAlarms\(\),/u)
-})
-
-test('home overview map fills the full map card body', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /\.map-panel[\s\S]{0,160}?display:\s*flex/u)
-  assert.match(source, /\.map-stage[\s\S]{0,140}?flex:\s*1/u)
-})
-
-test('home overview legend and detail cover unified device and facility status colors', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  assert.match(source, /gisPointStatusColors\.alarm[\s\S]{0,20}报警\/异常/u)
-  assert.match(source, /gisPointStatusColors\.workOrder[\s\S]{0,20}工单\/检修中/u)
-  assert.match(source, /gisPointStatusColors\.warning[\s\S]{0,20}预警\/欠费/u)
-  assert.match(source, /gisPointStatusColors\.offline[\s\S]{0,20}离线/u)
-  assert.match(source, /gisPointStatusColors\.normal[\s\S]{0,20}正常/u)
-  assert.match(source, /overviewDetail\.statusText \|\| gisStatusLabel\(overviewDetail\.statusKey\)/u)
-})
-
-test('home device data sources track the unified equipment endpoints', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  // 设备台账已统一到 /equipment-base:统计与列表都走统一设备接口,供水范围用 rootTypeId 限定
-  assert.match(source, /getEquipmentStatusStatistics\(/u)
-  assert.match(source, /listEquipment\(/u)
-  assert.match(source, /rootTypeId/u)
-  assert.match(source, /normalizeDeviceStatistics/u)
-  // 供水专属的旧设备接口已下线,不得再被首页引用
-  assert.doesNotMatch(source, /getDeviceLedgerStatistics/u)
-  assert.doesNotMatch(source, /listAbnormalDevices/u)
-})
-
-test('home surfaces partial load failures instead of swallowing them', async () => {
-  const source = await readFile(homePath, 'utf8')
-
-  // settle 自身吞掉异常并返回兜底值,因此失败上报必须由 settle 内部完成;
-  // 链式 .catch(recordProblem(...)) 永不触发,会让“部分数据加载失败”提示变成死代码。
-  assert.match(source, /async function settle\(fn, fallback = null, onError = null\)/u)
-  assert.match(source, /onError\?\.\(\)/u)
-  assert.doesNotMatch(source, /\.catch\(recordProblem\(/u)
-  assert.match(source, /settle\(\(\) => getWaterFacilityOverview\(\), \[\], recordProblem\('设施概览'\)\)/u)
-  assert.match(source, /settle\(\(\) => listRealtimeAlarms\(\), \[\], recordProblem\('实时报警'\)\)/u)
-})