home.vue 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. <template>
  2. <div class="drainage-home-screen">
  3. <!-- ==================== 顶部标题栏 ==================== -->
  4. <div class="screen-header">
  5. <div class="header-left">
  6. <span class="header-time">{{ currentDateTime }}</span>
  7. </div>
  8. <div class="header-center">
  9. <h1 class="header-title">排水管网安全运行监测系统</h1>
  10. </div>
  11. <div class="header-right"></div>
  12. </div>
  13. <!-- ==================== Tab 1: 雨水污水 ==================== -->
  14. <div v-show="activeTab === 0" class="screen-body">
  15. <div class="panel-left">
  16. <div class="panel-box">
  17. <div class="panel-title"><span class="title-icon">▶</span> 天气预报</div>
  18. <div class="weather-cards">
  19. <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
  20. <div class="weather-date">{{ w.date }}</div>
  21. <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
  22. <div class="weather-desc">{{ w.desc }}</div>
  23. <div class="weather-temp">{{ w.tempRange }}</div>
  24. <div class="weather-wind">{{ w.wind }}</div>
  25. </div>
  26. </div>
  27. </div>
  28. <div class="panel-box">
  29. <div class="panel-title"><span class="title-icon">▶</span> 监测点概况</div>
  30. <div class="overview-cards">
  31. <div class="overview-card"><div class="ov-label">监测总点数</div><div class="ov-value">{{ rwOverview.total }}</div></div>
  32. <div class="overview-card"><div class="ov-label">在线监测点</div><div class="ov-value online">{{ rwOverview.online }}</div></div>
  33. <div class="overview-card"><div class="ov-label">离线监测点</div><div class="ov-value offline">{{ rwOverview.offline }}</div></div>
  34. </div>
  35. <div class="pie-chart-wrapper">
  36. <div id="rwPieChart" class="chart-area"></div>
  37. <div class="pie-legend">
  38. <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ rwOnlineRate }}%</span></div>
  39. <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>
  40. </div>
  41. </div>
  42. </div>
  43. <div class="panel-box">
  44. <div class="panel-title"><span class="title-icon">▶</span> 实时数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
  45. <div class="realtime-table-wrap">
  46. <table class="dark-table">
  47. <thead><tr><th>序号</th><th>监测点</th><th>时间</th><th>积水量</th><th>排水量</th></tr></thead>
  48. <tbody>
  49. <tr v-for="(row, i) in rwRealtimeData" :key="i">
  50. <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td>{{ row.waterlogging }}</td><td>{{ row.drainage }}</td>
  51. </tr>
  52. </tbody>
  53. </table>
  54. </div>
  55. </div>
  56. </div>
  57. <div class="panel-center">
  58. <div id="rwMap" class="map-container"></div>
  59. <div class="pipeline-map-status">
  60. <span class="pipeline-status-dot"></span>
  61. 排水管网 {{ drainagePipelines.length }} 段
  62. </div>
  63. <div v-if="pipelineMapError" class="pipeline-map-error">{{ pipelineMapError }}</div>
  64. </div>
  65. <div class="panel-right">
  66. <div class="panel-box">
  67. <div class="panel-title"><span class="title-icon">▶</span> 近日排水量</div>
  68. <div id="rwDrainageBarChart" class="chart-area-sm"></div>
  69. </div>
  70. <div class="panel-box">
  71. <div class="panel-title"><span class="title-icon">▶</span> 近日积水量</div>
  72. <div id="rwWaterlogBarChart" class="chart-area-sm"></div>
  73. </div>
  74. <div class="panel-box">
  75. <div class="panel-title"><span class="title-icon">▶</span> 预警信息</div>
  76. <div class="warn-summary">
  77. <div class="warn-card"><div class="warn-icon">✅</div><div class="warn-label">设备在线</div><div class="warn-value online">{{ rwWarnSummary.online }}</div></div>
  78. <div class="warn-card"><div class="warn-icon">❌</div><div class="warn-label">设备离线</div><div class="warn-value offline">{{ rwWarnSummary.offline }}</div></div>
  79. </div>
  80. <div class="realtime-table-wrap">
  81. <table class="dark-table">
  82. <thead><tr><th>序号</th><th>设备名称</th><th>时间</th><th>设备编号</th><th>设备类型</th></tr></thead>
  83. <tbody>
  84. <tr v-for="(row, i) in rwWarnData" :key="i">
  85. <td>{{ i + 1 }}</td><td :title="row.deviceName">{{ row.deviceName }}</td><td>{{ row.time }}</td><td>{{ row.deviceCode }}</td><td>{{ row.deviceType }}</td>
  86. </tr>
  87. </tbody>
  88. </table>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <!-- ==================== Tab 2: 设备管理 ==================== -->
  94. <div v-show="activeTab === 1" class="screen-body">
  95. <div class="panel-left">
  96. <div class="panel-box">
  97. <div class="panel-title"><span class="title-icon">▶</span> 基础信息</div>
  98. <div class="overview-cards">
  99. <div class="overview-card"><div class="ov-label">设备总数</div><div class="ov-value">{{ deviceOverview.total }}</div></div>
  100. <div class="overview-card"><div class="ov-label">在线点</div><div class="ov-value online">{{ deviceOverview.online }}</div></div>
  101. <div class="overview-card"><div class="ov-label">离线点</div><div class="ov-value offline">{{ deviceOverview.offline }}</div></div>
  102. </div>
  103. <div class="pie-chart-wrapper">
  104. <div id="devicePieChart" class="chart-area"></div>
  105. <div class="pie-legend">
  106. <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ deviceOnlineRate }}%</span></div>
  107. <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>
  108. </div>
  109. </div>
  110. </div>
  111. <div class="panel-box">
  112. <div class="panel-title"><span class="title-icon">▶</span> 维修记录</div>
  113. <div id="deviceRepairChart" class="chart-area-sm"></div>
  114. </div>
  115. <div class="panel-box">
  116. <div class="panel-title"><span class="title-icon">▶</span> 点位数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
  117. <div class="realtime-table-wrap">
  118. <table class="dark-table">
  119. <thead><tr><th>序号</th><th>设备名称</th><th>时间</th><th>安装位置</th><th>在线状态</th></tr></thead>
  120. <tbody>
  121. <tr v-for="(row, i) in devicePointData" :key="i">
  122. <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td :title="row.location">{{ row.location }}</td>
  123. <td><span :class="'level-tag level-' + row.statusClass">{{ row.status }}</span></td>
  124. </tr>
  125. </tbody>
  126. </table>
  127. </div>
  128. </div>
  129. </div>
  130. <div class="panel-center">
  131. <div id="deviceMap" class="map-container"></div>
  132. </div>
  133. <div class="panel-right">
  134. <div class="panel-box">
  135. <div class="panel-title"><span class="title-icon">▶</span> 设备信息</div>
  136. <div class="realtime-table-wrap">
  137. <table class="dark-table">
  138. <thead><tr><th>序号</th><th>设备名称</th><th>设备编号</th><th>所在位置</th><th>运行状态</th></tr></thead>
  139. <tbody>
  140. <tr v-for="(row, i) in deviceList" :key="i">
  141. <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.code }}</td><td :title="row.location">{{ row.location }}</td>
  142. <td><span :class="'level-tag level-' + row.statusClass">{{ row.status }}</span></td>
  143. </tr>
  144. </tbody>
  145. </table>
  146. </div>
  147. </div>
  148. <div class="panel-box">
  149. <div class="panel-title"><span class="title-icon">▶</span> 警告信息</div>
  150. <div class="realtime-table-wrap">
  151. <table class="dark-table warn-table">
  152. <thead><tr><th>告警等级</th><th>设备编码</th><th>时间</th><th>报警内容</th></tr></thead>
  153. <tbody>
  154. <tr v-for="(row, i) in deviceWarnData" :key="i">
  155. <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>
  156. </tr>
  157. </tbody>
  158. </table>
  159. </div>
  160. </div>
  161. <div class="panel-box">
  162. <div class="panel-title"><span class="title-icon">▶</span> 报警统计</div>
  163. <div class="alarm-stat">
  164. <div class="alarm-stat-item"><div class="alarm-icon">⚠️</div><div class="alarm-label">一般危险</div><div class="alarm-value yellow">{{ alarmStat.general }}</div></div>
  165. <div class="alarm-stat-item"><div class="alarm-icon">🚨</div><div class="alarm-label">严重危险</div><div class="alarm-value red">{{ alarmStat.severe }}</div></div>
  166. </div>
  167. </div>
  168. </div>
  169. </div>
  170. <!-- ==================== Tab 3: 积水管理 ==================== -->
  171. <div v-show="activeTab === 2" class="screen-body">
  172. <div class="panel-left">
  173. <div class="panel-box">
  174. <div class="panel-title"><span class="title-icon">▶</span> 天气预报</div>
  175. <div class="weather-cards">
  176. <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
  177. <div class="weather-date">{{ w.date }}</div>
  178. <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
  179. <div class="weather-desc">{{ w.desc }}</div>
  180. <div class="weather-temp">{{ w.tempRange }}</div>
  181. <div class="weather-wind">{{ w.wind }}</div>
  182. </div>
  183. </div>
  184. </div>
  185. <div class="panel-box">
  186. <div class="panel-title"><span class="title-icon">▶</span> 监测点概况</div>
  187. <div class="overview-cards">
  188. <div class="overview-card"><div class="ov-label">在线</div><div class="ov-value online">{{ waterOverview.online }}</div></div>
  189. <div class="overview-card"><div class="ov-label">离线</div><div class="ov-value offline">{{ waterOverview.offline }}</div></div>
  190. <div class="overview-card"><div class="ov-label">在线率</div><div class="ov-value">{{ waterOnlineRate }}%</div></div>
  191. </div>
  192. <div class="pie-chart-wrapper">
  193. <div id="waterPieChart" class="chart-area"></div>
  194. <div class="pie-legend">
  195. <div class="legend-item"><span class="legend-dot" style="background:#67c23a"></span><span class="legend-label">在线率</span><span class="legend-value">{{ waterOnlineRate }}%</span></div>
  196. <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>
  197. </div>
  198. </div>
  199. <div class="level-counts">
  200. <div class="level-count"><span class="dot green"></span>未积水: {{ waterLevelCount.none }}</div>
  201. <div class="level-count"><span class="dot blue"></span>轻度积水: {{ waterLevelCount.light }}</div>
  202. <div class="level-count"><span class="dot orange"></span>中度积水: {{ waterLevelCount.moderate }}</div>
  203. <div class="level-count"><span class="dot red"></span>严重积水: {{ waterLevelCount.severe }}</div>
  204. </div>
  205. </div>
  206. <div class="panel-box">
  207. <div class="panel-title"><span class="title-icon">▶</span> 实时数据 <span class="refresh-time">刷新: {{ refreshTime }}</span></div>
  208. <div class="realtime-table-wrap">
  209. <table class="dark-table">
  210. <thead><tr><th>序号</th><th>监测点</th><th>时间</th><th>积水量</th><th>级别</th></tr></thead>
  211. <tbody>
  212. <tr v-for="(row, i) in waterRealtimeData" :key="i">
  213. <td>{{ i + 1 }}</td><td :title="row.name">{{ row.name }}</td><td>{{ row.time }}</td><td>{{ row.depth }}cm</td>
  214. <td><span :class="'level-tag level-' + row.levelClass">{{ row.level }}</span></td>
  215. </tr>
  216. </tbody>
  217. </table>
  218. </div>
  219. </div>
  220. </div>
  221. <div class="panel-center">
  222. <div id="waterMap" class="map-container"></div>
  223. </div>
  224. <div class="panel-right">
  225. <div class="panel-box">
  226. <div class="panel-title"><span class="title-icon">▶</span> 积水分析</div>
  227. <div class="analysis-form">
  228. <div class="form-row"><label>年份</label><select v-model="analysisYear"><option v-for="y in analysisYears" :key="y" :value="y">{{ y }}</option></select></div>
  229. <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>
  230. <button class="analysis-btn" @click="doWaterAnalysis">开始分析</button>
  231. <div class="analysis-result">{{ waterAnalysisResult }}</div>
  232. </div>
  233. </div>
  234. <div class="panel-box">
  235. <div class="panel-title"><span class="title-icon">▶</span> 积水统计</div>
  236. <div class="stat-tabs">
  237. <span :class="{active: waterStatType==='month'}" @click="waterStatType='month'">按月</span>
  238. <span :class="{active: waterStatType==='year'}" @click="waterStatType='year'">按年</span>
  239. </div>
  240. <div id="waterStatChart" class="chart-area-sm"></div>
  241. </div>
  242. <div class="panel-box">
  243. <div class="panel-title"><span class="title-icon">▶</span> 预警信息</div>
  244. <div class="warn-summary">
  245. <div class="warn-card"><div class="warn-icon">💧</div><div class="warn-label">积水报警</div><div class="warn-value">{{ waterWarnSummary.water }}</div></div>
  246. <div class="warn-card"><div class="warn-icon">✅</div><div class="warn-label">设备在线</div><div class="warn-value online">{{ waterWarnSummary.online }}</div></div>
  247. <div class="warn-card"><div class="warn-icon">❌</div><div class="warn-label">设备离线</div><div class="warn-value offline">{{ waterWarnSummary.offline }}</div></div>
  248. </div>
  249. <div class="realtime-table-wrap">
  250. <table class="dark-table">
  251. <thead><tr><th>序号</th><th>地址</th><th>时间</th><th>报警类型</th><th>实时数据</th></tr></thead>
  252. <tbody>
  253. <tr v-for="(row, i) in waterWarnData" :key="i">
  254. <td>{{ i + 1 }}</td><td :title="row.addr">{{ row.addr }}</td><td>{{ row.time }}</td><td>{{ row.type }}</td><td>{{ row.value }}</td>
  255. </tr>
  256. </tbody>
  257. </table>
  258. </div>
  259. </div>
  260. </div>
  261. </div>
  262. <!-- ==================== 底部 Tab 栏 ==================== -->
  263. <div class="bottom-tabs">
  264. <div v-for="tab in tabs" :key="tab.value" class="bottom-tab" :class="{ active: activeTab === tab.value }" @click="switchTab(tab.value)">
  265. {{ tab.label }}
  266. </div>
  267. </div>
  268. <!-- ==================== 设备详情弹窗 ==================== -->
  269. <div v-if="deviceModalVisible" class="device-modal" @click.self="closeDeviceModal">
  270. <div class="device-modal-content">
  271. <div class="device-modal-header">
  272. <span class="device-modal-title">设备详情</span>
  273. <span class="device-modal-close" @click="closeDeviceModal">×</span>
  274. </div>
  275. <div class="device-info-list">
  276. <div class="device-info-item"><span class="info-label">设备编号</span><span class="info-value">{{ selectedDevice.code }}</span></div>
  277. <div class="device-info-item"><span class="info-label">设备名称</span><span class="info-value">{{ selectedDevice.name }}</span></div>
  278. <div class="device-info-item"><span class="info-label">安装日期</span><span class="info-value">{{ selectedDevice.installDate }}</span></div>
  279. <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>
  280. <div class="device-info-item"><span class="info-label">频率</span><span class="info-value">{{ selectedDevice.frequency }}</span></div>
  281. <div class="device-info-item"><span class="info-label">地址</span><span class="info-value">{{ selectedDevice.address }}</span></div>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. </template>
  287. <script setup name="DrainageHome">
  288. import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
  289. import * as echarts from 'echarts'
  290. import locationIcon from '@/assets/images/location.png'
  291. import { getEquipmentPage, getWaterloggingPage, getPartitionPage, getEquipmentById, getAlarmDataPage } from '@/api/drainage'
  292. import { getAlarmPage } from '@/api/pipeNetwork/hazard'
  293. import { getGisPipeLines } from '@/api/pipeNetwork/basic'
  294. import { convertCoord } from '@/utils/coordTransform'
  295. const BAIDU_MAP_AK = 'mvSNSN4CPN6WJs2Xi6sQFNmWwCLRgU6T'
  296. const MAP_STYLE_ID = '7a6c3490bbc3d1e3e9d8602530301e10'
  297. // ==================== 公共:实时时间 ====================
  298. const currentDateTime = ref('')
  299. const refreshTime = ref('')
  300. let timer = null
  301. function updateTime() {
  302. const now = new Date()
  303. const pad = n => String(n).padStart(2, '0')
  304. const weekDays = ['日', '一', '二', '三', '四', '五', '六']
  305. currentDateTime.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())} 星期${weekDays[now.getDay()]}`
  306. }
  307. function updateRefreshTime() {
  308. const now = new Date()
  309. const pad = n => String(n).padStart(2, '0')
  310. refreshTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
  311. }
  312. // ==================== 公共:Tab 切换 ====================
  313. const tabs = [
  314. { label: '雨水污水', value: 0 },
  315. { label: '设备管理', value: 1 },
  316. { label: '积水管理', value: 2 }
  317. ]
  318. const activeTab = ref(0)
  319. // ==================== 公共:天气 ====================
  320. const weatherList = ref([])
  321. function getWeatherIcon(text) {
  322. if (!text) return '☀️'
  323. const t = String(text)
  324. if (t.includes('晴')) return '☀️'
  325. if (t.includes('多云')) return '⛅'
  326. if (t.includes('阴')) return '☁️'
  327. if (t.includes('雨') && t.includes('雪')) return '🌨️'
  328. if (t.includes('雪')) return '❄️'
  329. if (t.includes('雷')) return '⛈️'
  330. if (t.includes('雨')) return '🌧️'
  331. if (t.includes('雾') || t.includes('霾')) return '🌫️'
  332. return '☁️'
  333. }
  334. function jsonpRequest(url) {
  335. return new Promise((resolve, reject) => {
  336. const callbackName = 'drainageHomeWeatherCallback_' + Date.now()
  337. const script = document.createElement('script')
  338. script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
  339. script.onerror = () => { document.head.removeChild(script); delete window[callbackName]; reject(new Error('天气接口加载失败')) }
  340. window[callbackName] = (data) => { document.head.removeChild(script); delete window[callbackName]; resolve(data) }
  341. document.head.appendChild(script)
  342. })
  343. }
  344. async function loadWeatherData() {
  345. try {
  346. const url = `/baidu-weather/weather/v1/?district_id=431222&data_type=all&ak=${BAIDU_MAP_AK}&output=json`
  347. const response = await fetch(url)
  348. const res = await response.json()
  349. if (res && (res.status === 0 || res.status === '0') && res.result && Array.isArray(res.result.forecasts)) {
  350. weatherList.value = res.result.forecasts.slice(0, 3).map(day => {
  351. const dateStr = day.date || ''
  352. const monthDay = dateStr.length >= 10 ? `${dateStr.slice(5, 7)}/${dateStr.slice(8, 10)}` : dateStr
  353. 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 || ''}` }
  354. })
  355. } else { useMockWeather() }
  356. } catch (e) { console.error('加载天气数据失败:', e); useMockWeather() }
  357. }
  358. function useMockWeather() {
  359. const now = new Date(); const pad = n => String(n).padStart(2, '0')
  360. const formatDate = (offset) => { const d = new Date(now.getTime() + offset * 24 * 60 * 60 * 1000); return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}` }
  361. weatherList.value = [
  362. { date: formatDate(0), text: '多云', desc: '多云', tempRange: '18°C ~ 26°C', wind: '东南风 2级' },
  363. { date: formatDate(1), text: '小雨', desc: '小雨', tempRange: '16°C ~ 23°C', wind: '东风 3级' },
  364. { date: formatDate(2), text: '阴', desc: '阴', tempRange: '15°C ~ 22°C', wind: '北风 2级' }
  365. ]
  366. }
  367. // ==================== Tab 1: 雨水污水 ====================
  368. const rwOverview = ref({ total: 0, online: 0, offline: 0 })
  369. const rwOnlineRate = computed(() => {
  370. const total = Number(rwOverview.value.total || 0)
  371. return total ? Math.round((Number(rwOverview.value.online || 0) / total) * 100) : 0
  372. })
  373. const rwRealtimeData = ref([])
  374. const rwWarnSummary = ref({ online: 0, offline: 0 })
  375. const rwWarnData = ref([])
  376. const rwMapPoints = ref([])
  377. const drainagePipelines = ref([])
  378. const pipelineMapError = ref('')
  379. // ==================== Tab 2: 设备管理 ====================
  380. const deviceOverview = ref({ total: 0, online: 0, offline: 0 })
  381. const deviceOnlineRate = computed(() => {
  382. const total = Number(deviceOverview.value.total || 0)
  383. return total ? Math.round((Number(deviceOverview.value.online || 0) / total) * 100) : 0
  384. })
  385. const devicePointData = ref([])
  386. const deviceList = ref([])
  387. const deviceWarnData = ref([])
  388. const alarmStat = ref({ general: 0, severe: 0 })
  389. const deviceMapPoints = ref([])
  390. const deviceModalVisible = ref(false)
  391. const selectedDevice = ref({ code: '', name: '', installDate: '', status: '', statusClass: '', frequency: '', address: '' })
  392. // ==================== Tab 3: 积水管理 ====================
  393. const waterOverview = ref({ online: 98, offline: 12 })
  394. const waterOnlineRate = computed(() => {
  395. const online = Number(waterOverview.value.online || 0)
  396. const offline = Number(waterOverview.value.offline || 0)
  397. const total = online + offline
  398. return total ? Math.round((online / total) * 100) : 0
  399. })
  400. const waterLevelCount = ref({ none: 45, light: 32, moderate: 18, severe: 15 })
  401. const waterRealtimeData = ref([])
  402. const waterWarnSummary = ref({ water: 8, online: 98, offline: 12 })
  403. const waterWarnData = ref([])
  404. const waterMapPoints = ref([])
  405. const analysisYear = ref(new Date().getFullYear())
  406. const analysisYears = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
  407. const analysisLevel = ref('all')
  408. const analysisLevels = [{ value: 'all', label: '全部等级' }, { value: 'light', label: '轻度' }, { value: 'moderate', label: '中度' }, { value: 'severe', label: '严重' }]
  409. const waterAnalysisResult = ref('点击“开始分析”生成积水趋势分析结果。')
  410. const waterStatType = ref('month')
  411. // ==================== 地图实例 ====================
  412. const rwMap = ref(null), deviceMap = ref(null), waterMap = ref(null)
  413. let bmapInitTimer = null
  414. function initBMapGL(containerId, mapRef, points, clickHandler) {
  415. const container = document.getElementById(containerId)
  416. if (!container || typeof BMapGL === 'undefined') return false
  417. try {
  418. const map = new BMapGL.Map(containerId)
  419. map.enableScrollWheelZoom(true)
  420. map.setMapStyleV2({ styleId: MAP_STYLE_ID })
  421. mapRef.value = map
  422. // 首页中间的雨水污水地图展示 pipe_network_base 中的排水管网;
  423. // 设备、积水页签仍只展示各自的业务点位。
  424. const viewPoints = containerId === 'rwMap' ? drawDrainagePipelines(map) : []
  425. if (points.length > 0) {
  426. points.forEach(p => addLabelMarker(map, p, clickHandler))
  427. viewPoints.push(...points.map(p => {
  428. const [lng, lat] = convertCoord(Number(p.lng), Number(p.lat))
  429. return new BMapGL.Point(lng, lat)
  430. }))
  431. }
  432. if (viewPoints.length > 0) {
  433. map.setViewport(viewPoints)
  434. if (containerId !== 'rwMap' && map.getZoom() < 13) map.setZoom(13)
  435. } else {
  436. map.centerAndZoom(new BMapGL.Point(110.393, 28.452), 14)
  437. }
  438. return true
  439. } catch (e) {
  440. console.error('百度地图初始化失败:', e)
  441. if (containerId === 'rwMap') pipelineMapError.value = '排水管网地图初始化失败,请刷新页面重试。'
  442. return false
  443. }
  444. }
  445. function escapeMapText(value) {
  446. return String(value ?? '')
  447. .replace(/&/g, '&amp;')
  448. .replace(/</g, '&lt;')
  449. .replace(/>/g, '&gt;')
  450. .replace(/"/g, '&quot;')
  451. .replace(/'/g, '&#39;')
  452. }
  453. function getPipelineColor(level) {
  454. const text = String(level || '').toLowerCase()
  455. if (text.includes('主') || text.includes('一级') || text.includes('main')) return '#00e5ff'
  456. if (text.includes('支') || text.includes('二级') || text.includes('branch')) return '#409eff'
  457. return '#67c23a'
  458. }
  459. function drawDrainagePipelines(map) {
  460. const viewportPoints = []
  461. drainagePipelines.value.forEach(line => {
  462. if (!Array.isArray(line.points) || line.points.length < 2) return
  463. const bmapPoints = line.points.map(point => {
  464. const [lng, lat] = convertCoord(point.lng, point.lat)
  465. return new BMapGL.Point(lng, lat)
  466. })
  467. const polyline = new BMapGL.Polyline(bmapPoints, {
  468. strokeColor: getPipelineColor(line.level),
  469. strokeWeight: 5,
  470. strokeOpacity: 0.9
  471. })
  472. polyline.addEventListener('click', event => {
  473. event?.domEvent?.stopPropagation?.()
  474. const content = `<div style="min-width:220px;color:#303133;line-height:1.8">
  475. <div style="font-size:15px;font-weight:700;color:#1677ff;margin-bottom:6px">${escapeMapText(line.name || '排水管网')}</div>
  476. <div>设施编号:${escapeMapText(line.code || '-')}</div>
  477. <div>管网等级:${escapeMapText(line.level || '-')}</div>
  478. <div>管径:${escapeMapText(line.diameter ?? '-')} mm</div>
  479. <div>长度:${escapeMapText(line.length ?? '-')} m</div>
  480. <div>材质:${escapeMapText(line.material || '-')}</div>
  481. <div>位置:${escapeMapText(line.location || '-')}</div>
  482. </div>`
  483. map.openInfoWindow(new BMapGL.InfoWindow(content, { width: 280, enableMessage: false }), bmapPoints[0])
  484. })
  485. map.addOverlay(polyline)
  486. viewportPoints.push(...bmapPoints)
  487. })
  488. return viewportPoints
  489. }
  490. function addLabelMarker(map, point, clickHandler) {
  491. const [lng, lat] = convertCoord(Number(point.lng), Number(point.lat))
  492. const bPoint = new BMapGL.Point(lng, lat)
  493. const color = point.color || '#409eff'
  494. const icon = point.icon || locationIcon
  495. const html = `<div style="text-align:center;cursor:pointer;">
  496. <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>
  497. ${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>` : ''}
  498. <div><img src="${icon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
  499. </div>`
  500. const label = new BMapGL.Label(html, { position: bPoint, offset: new BMapGL.Size(-30, -55) })
  501. label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
  502. map.addOverlay(label)
  503. if (clickHandler) {
  504. label.addEventListener('click', () => clickHandler(point))
  505. }
  506. }
  507. // ==================== ECharts 实例 ====================
  508. const chartInstances = {}
  509. function initChart(domId, option) {
  510. const dom = document.getElementById(domId)
  511. if (!dom) return null
  512. if (chartInstances[domId]) chartInstances[domId].dispose()
  513. chartInstances[domId] = echarts.init(dom)
  514. chartInstances[domId].setOption(option)
  515. return chartInstances[domId]
  516. }
  517. function getPieOption(rate) {
  518. return {
  519. tooltip: { trigger: 'item', formatter: '{b}: {d}%' },
  520. graphic: [
  521. { type: 'text', left: 'center', top: '35%', style: { text: '在线率', fill: '#a0cfff', fontSize: 11, textAlign: 'center' } },
  522. { type: 'text', left: 'center', top: '50%', style: { text: `${rate}%`, fill: '#fff', fontSize: 18, fontWeight: 'bold', textAlign: 'center' } }
  523. ],
  524. series: [{
  525. type: 'pie', radius: ['40%', '65%'], center: ['50%', '50%'], label: { show: false }, labelLine: { show: false },
  526. data: [
  527. { name: '在线', value: rate, itemStyle: { color: '#67c23a' } },
  528. { name: '离线', value: 100 - rate, itemStyle: { color: '#f56c6c' } }
  529. ],
  530. emphasis: { scale: true, scaleSize: 5 }
  531. }]
  532. }
  533. }
  534. function getBarOption(xData, yData, name = '数值', color = '#409eff') {
  535. return {
  536. tooltip: { trigger: 'axis' },
  537. grid: { left: 40, right: 10, top: 10, bottom: 25 },
  538. xAxis: { type: 'category', data: xData, axisLabel: { color: '#7ec8e3', fontSize: 9 }, axisLine: { lineStyle: { color: '#1a3a5c' } } },
  539. yAxis: { type: 'value', name, nameTextStyle: { color: '#7ec8e3', fontSize: 10 }, axisLabel: { color: '#7ec8e3', fontSize: 10 }, splitLine: { lineStyle: { color: '#1a3a5c' } }, axisLine: { show: false } },
  540. series: [{ type: 'bar', data: yData, barWidth: 10, itemStyle: { color, borderRadius: [2, 2, 0, 0] } }]
  541. }
  542. }
  543. // ==================== 数据加载(mock + API 占位) ====================
  544. async function loadAllData() {
  545. await Promise.all([
  546. loadDrainagePipelineData(),
  547. loadRwTabData(),
  548. loadEquipmentData(),
  549. loadWaterloggingData()
  550. ])
  551. }
  552. async function loadDrainagePipelineData() {
  553. pipelineMapError.value = ''
  554. try {
  555. const response = await getGisPipeLines({ networkType: '1' })
  556. const features = Array.isArray(response?.data?.features) ? response.data.features : []
  557. drainagePipelines.value = features.map(feature => {
  558. const properties = feature?.properties || {}
  559. const coordinates = feature?.geometry?.type === 'LineString' ? feature.geometry.coordinates : []
  560. const points = coordinates
  561. .map(item => ({ lng: Number(item?.[0]), lat: Number(item?.[1]) }))
  562. .filter(item => Number.isFinite(item.lng) && Number.isFinite(item.lat))
  563. return {
  564. id: properties.networkId,
  565. code: properties.networkCode,
  566. name: properties.networkName,
  567. level: properties.networkLevel,
  568. diameter: properties.diameter,
  569. length: properties.length,
  570. material: properties.material,
  571. location: properties.location,
  572. points
  573. }
  574. }).filter(line => line.points.length >= 2)
  575. if (!drainagePipelines.value.length) {
  576. pipelineMapError.value = '暂无带完整起止坐标的排水管网数据'
  577. }
  578. } catch (error) {
  579. drainagePipelines.value = []
  580. pipelineMapError.value = '排水管网数据加载失败,请检查后端服务。'
  581. console.error('加载排水管网 GIS 线路失败:', error)
  582. }
  583. }
  584. async function loadEquipmentData() {
  585. try {
  586. const eqPageRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
  587. const eqPageData = eqPageRes.code !== undefined ? eqPageRes.data : eqPageRes
  588. const allEquipments = eqPageData?.records || eqPageData?.rows || []
  589. console.log('[设备管理调试] 设备列表总数:', allEquipments.length)
  590. if (allEquipments.length > 0) console.log('[设备管理调试] 设备示例:', JSON.stringify(allEquipments[0]))
  591. // 统计在线/离线
  592. const online = allEquipments.filter(e => e.onlineStatus === 1).length
  593. const offline = allEquipments.filter(e => e.onlineStatus !== 1).length
  594. deviceOverview.value = { total: allEquipments.length, online, offline }
  595. // 设备列表
  596. deviceList.value = allEquipments.map(e => ({
  597. name: e.equipmentName || '',
  598. code: e.equipmentCode || '',
  599. location: e.equipmentLocation || '',
  600. status: e.onlineStatus === 1 ? '在线' : '离线',
  601. statusClass: e.onlineStatus === 1 ? 'none' : 'severe'
  602. }))
  603. // 点位数据
  604. devicePointData.value = allEquipments.map(e => ({
  605. name: e.equipmentName || '',
  606. time: e.createTime || '',
  607. location: e.equipmentLocation || '',
  608. status: e.onlineStatus === 1 ? '在线' : '离线',
  609. statusClass: e.onlineStatus === 1 ? 'none' : 'severe'
  610. }))
  611. // 地图标记
  612. deviceMapPoints.value = allEquipments
  613. .filter(e => e.longitude && e.latitude)
  614. .map(e => ({
  615. name: e.equipmentName || '',
  616. lng: Number(e.longitude),
  617. lat: Number(e.latitude),
  618. color: e.onlineStatus === 1 ? '#67c23a' : '#f56c6c',
  619. icon: locationIcon,
  620. subText: e.onlineStatus === 1 ? '在线' : '离线'
  621. }))
  622. // 查询alarm_data报警数据(只查排水设备)
  623. try {
  624. const alarmDataRes = await getAlarmDataPage(1, 50, { deviceType: 'drainage' })
  625. const alarmPageData = alarmDataRes.code !== undefined ? alarmDataRes.data : alarmDataRes
  626. const alarmDataList = alarmPageData?.records || alarmPageData?.rows || []
  627. console.log('[大屏调试] alarm_data返回:', alarmDataList.length, alarmDataList.length > 0 ? JSON.stringify(alarmDataList[0]) : '')
  628. deviceWarnData.value = alarmDataList.slice(0, 10).map(a => {
  629. const raw = a.alarmLevel != null ? a.alarmLevel : (a.alarm_level != null ? a.alarm_level : null)
  630. // alarmLevel: 1-最高 4-最低(字典编码字符串),统一转数值后比较
  631. const lvl = raw == null || raw === '' ? null : Number(raw)
  632. let levelText = '--'
  633. let levelClass = 'yellow'
  634. if (lvl != null && Number.isFinite(lvl) && lvl > 0) {
  635. levelText = lvl === 1 ? '一级' : lvl === 2 ? '二级' : lvl === 3 ? '三级' : '四级'
  636. levelClass = lvl <= 2 ? 'red' : lvl === 3 ? 'orange' : 'yellow'
  637. } else {
  638. levelText = '预警'
  639. levelClass = 'orange'
  640. }
  641. return {
  642. level: lvl != null && Number.isFinite(lvl) ? lvl : 3,
  643. levelText,
  644. levelClass,
  645. code: a.deviceCode || a.device_code || '',
  646. time: a.createTime || '',
  647. content: a.warningType || a.warning_type || a.remark || ''
  648. }
  649. })
  650. // 报警统计(等级为字典编码字符串,需转数值后比较)
  651. const levelOf = a => {
  652. const raw = a.alarmLevel != null ? a.alarmLevel : a.alarm_level
  653. const n = raw == null || raw === '' ? NaN : Number(raw)
  654. return Number.isFinite(n) ? n : 0
  655. }
  656. const general = alarmDataList.filter(a => levelOf(a) >= 3).length
  657. const severe = alarmDataList.filter(a => levelOf(a) >= 1 && levelOf(a) <= 2).length
  658. alarmStat.value = { general, severe }
  659. } catch (e) {
  660. deviceWarnData.value = []
  661. }
  662. } catch (e) { console.error('加载设备数据失败:', e) }
  663. }
  664. async function loadWaterloggingData() {
  665. try {
  666. const res = await getWaterloggingPage(1, 999, {})
  667. const pageData = res.code !== undefined ? res.data : res
  668. const list = pageData?.records || pageData?.rows || []
  669. console.log('[大屏调试] 积水数据:', list.length, list.length > 0 ? JSON.stringify(list[0]) : '')
  670. // 积水等级判断(用waterloggingLevel字段:0=未积水, 1=轻度, 2=中度, 3=严重)
  671. const levelMap = [
  672. { label: '未积水', cls: 'none', color: '#67c23a' },
  673. { label: '轻度积水', cls: 'light', color: '#409eff' },
  674. { label: '中度积水', cls: 'moderate', color: '#e6a23c' },
  675. { label: '严重积水', cls: 'severe', color: '#f56c6c' }
  676. ]
  677. const getLevelInfo = (item) => {
  678. const lvl = Number(item.waterloggingLevel || 0)
  679. return levelMap[lvl] || levelMap[0]
  680. }
  681. // 地图点位(易积水点为单点要素,直接用经纬度)
  682. const allMapPoints = []
  683. list.forEach(item => {
  684. const lng = Number(item.longitude)
  685. const lat = Number(item.latitude)
  686. if (!Number.isFinite(lng) || !Number.isFinite(lat) || (lng === 0 && lat === 0)) return
  687. const lv = getLevelInfo(item)
  688. allMapPoints.push({
  689. name: item.pointName || '积水点',
  690. lng,
  691. lat,
  692. color: lv.color,
  693. subText: `${lv.label}`,
  694. depth: item.waterVolume || item.waterDepth || 0,
  695. level: lv.label,
  696. levelClass: lv.cls
  697. })
  698. })
  699. waterMapPoints.value = allMapPoints
  700. console.log('[大屏调试] 积水地图点位数:', allMapPoints.length)
  701. // 积水等级统计
  702. let none = 0, light = 0, moderate = 0, severe = 0
  703. list.forEach(p => {
  704. const lv = getLevelInfo(p)
  705. if (lv.cls === 'none') none++
  706. else if (lv.cls === 'light') light++
  707. else if (lv.cls === 'moderate') moderate++
  708. else severe++
  709. })
  710. waterLevelCount.value = { none, light, moderate, severe }
  711. // 实时数据表格
  712. waterRealtimeData.value = list.slice(0, 12).map((p, i) => {
  713. const lv = getLevelInfo(p)
  714. return {
  715. name: p.pointName || `积水点-${i + 1}`,
  716. time: p.createTime || p.updateTime || '',
  717. depth: p.waterVolume || p.waterDepth || p.depth || 0,
  718. level: lv.label,
  719. levelClass: lv.cls
  720. }
  721. })
  722. // 设备在线统计 - 取易积水点关联的摄像头
  723. const allEqIds = []
  724. list.forEach(p => {
  725. if (p.cameraIds) {
  726. p.cameraIds.split(',').forEach(id => {
  727. const trimmed = id.trim()
  728. if (trimmed) allEqIds.push(trimmed)
  729. })
  730. }
  731. })
  732. const uniqueEqIds = [...new Set(allEqIds)]
  733. let online = 0, offline = 0
  734. const linkedDeviceCodes = new Set()
  735. if (uniqueEqIds.length > 0) {
  736. try {
  737. const eqRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
  738. const eqData = eqRes.code !== undefined ? eqRes.data : eqRes
  739. const allEqs = eqData?.records || eqData?.rows || []
  740. const eqMap = {}
  741. allEqs.forEach(eq => { eqMap[eq.equipmentId] = eq })
  742. uniqueEqIds.forEach(id => {
  743. const eq = eqMap[id]
  744. if (eq) {
  745. if (eq.onlineStatus === 1) online++
  746. else offline++
  747. if (eq.equipmentCode) linkedDeviceCodes.add(eq.equipmentCode)
  748. } else { offline++ }
  749. })
  750. } catch (e) {
  751. online = uniqueEqIds.length
  752. }
  753. }
  754. waterOverview.value = { online, offline }
  755. // 报警信息 - 根据关联设备编码查alarm_data表
  756. console.log('[大屏调试] 积水关联设备编码:', [...linkedDeviceCodes])
  757. try {
  758. const alarmRes = await getAlarmDataPage(1, 50, { deviceType: 'drainage' })
  759. const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
  760. const alarmList = alarmPageData?.records || alarmPageData?.rows || []
  761. // 只保留关联设备编码的报警
  762. const filteredAlarms = linkedDeviceCodes.size > 0
  763. ? alarmList.filter(a => linkedDeviceCodes.has(a.deviceCode))
  764. : []
  765. waterWarnData.value = filteredAlarms.slice(0, 10).map(a => ({
  766. addr: a.deviceCode || '',
  767. time: a.createTime || '',
  768. type: a.warningType || a.warning_type || '积水报警',
  769. value: a.actualValue != null ? `${a.actualValue}cm` : '--'
  770. }))
  771. waterWarnSummary.value = { water: filteredAlarms.length, online, offline }
  772. } catch (e) {
  773. waterWarnData.value = []
  774. waterWarnSummary.value = { water: 0, online, offline }
  775. }
  776. } catch (e) {
  777. console.error('加载积水数据失败:', e)
  778. }
  779. }
  780. async function loadRwTabData() {
  781. try {
  782. // 1. 获取所有排水分区数据
  783. const partRes = await getPartitionPage(1, 999, {})
  784. // 兼容 AjaxResult包装 和 直接返回Page对象 两种格式
  785. const pageData = partRes.code !== undefined ? partRes.data : partRes
  786. const partitionList = pageData?.records || pageData?.rows || []
  787. console.log('[大屏调试] 分区数据条数:', partitionList.length)
  788. if (partitionList.length > 0) console.log('[大屏调试] 分区示例:', JSON.stringify(partitionList[0]))
  789. // 2. 收集关联设备ID并查询设备状态
  790. const allEquipmentIds = []
  791. partitionList.forEach(p => {
  792. if (p.equipmentId) {
  793. p.equipmentId.split(',').forEach(id => {
  794. const trimmed = id.trim()
  795. if (trimmed) allEquipmentIds.push(trimmed)
  796. })
  797. }
  798. })
  799. const uniqueIds = [...new Set(allEquipmentIds)]
  800. // 查询所有排水设备,通过ID匹配统计在线/离线
  801. let online = 0, offline = 0
  802. const equipmentDetails = {}
  803. try {
  804. const eqPageRes = await getEquipmentPage(1, 999, { equipmentTypeId: 'drainage' })
  805. const eqPageData = eqPageRes.code !== undefined ? eqPageRes.data : eqPageRes
  806. const allEquipments = eqPageData?.records || eqPageData?.rows || []
  807. console.log('[大屏调试] 设备列表总数:', allEquipments.length, '分区关联设备ID:', uniqueIds)
  808. if (allEquipments.length > 0) console.log('[大屏调试] 设备示例:', JSON.stringify(allEquipments[0]))
  809. // 建立设备ID映射(设备主键字段是equipmentId)
  810. const eqMap = {}
  811. allEquipments.forEach(eq => { eqMap[eq.equipmentId] = eq })
  812. console.log('[大屏调试] 设备ID映射keys:', Object.keys(eqMap).slice(0, 5))
  813. // 统计关联设备状态
  814. for (const id of uniqueIds) {
  815. const eq = eqMap[id]
  816. if (eq) {
  817. console.log(`[大屏调试] 设备${id}匹配成功, onlineStatus=`, eq.onlineStatus)
  818. if (eq.onlineStatus === 1) online++
  819. else offline++
  820. equipmentDetails[id] = { name: eq.equipmentName || '', code: eq.equipmentCode || '', typeName: eq.equipmentTypeName || '', isOnline: eq.onlineStatus === 1 }
  821. } else {
  822. console.log(`[大屏调试] 设备${id}在列表中未找到`)
  823. offline++
  824. }
  825. }
  826. } catch (e) {
  827. console.error('[大屏调试] getEquipmentPage失败:', e)
  828. // 如果分页查询失败,退回逐个查询
  829. for (const id of uniqueIds) {
  830. try {
  831. const eqRes = await getEquipmentById(id)
  832. const data = eqRes.code !== undefined ? eqRes.data : eqRes
  833. console.log(`[大屏调试] getEquipmentById(${id}) 返回:`, JSON.stringify(data))
  834. const info = data?.equipmentInfo || data
  835. if (info?.onlineStatus === 1) online++
  836. else offline++
  837. equipmentDetails[id] = { name: info?.equipmentName || '', code: info?.equipmentCode || '', typeName: info?.equipmentTypeName || '', isOnline: info?.onlineStatus === 1 }
  838. } catch (e2) { offline++ }
  839. }
  840. }
  841. const total = uniqueIds.length
  842. rwOverview.value = { total, online, offline }
  843. rwWarnSummary.value = { online, offline }
  844. // 3. 地图标记:使用排水分区的坐标
  845. rwMapPoints.value = partitionList
  846. .filter(p => p.longitude && p.latitude)
  847. .map(p => ({
  848. name: p.name || p.partitionName || '排水分区',
  849. lng: Number(p.longitude),
  850. lat: Number(p.latitude),
  851. color: p.equipmentId ? '#67c23a' : '#409eff',
  852. subText: p.partitionType || ''
  853. }))
  854. // 4. 实时数据表格:使用分区的真实排水量/积水量
  855. rwRealtimeData.value = partitionList.map((p, i) => ({
  856. name: p.name || p.partitionName || `监测点-${i + 1}`,
  857. time: p.time || p.dataTime || p.createTime || '',
  858. waterlogging: p.waterlogging != null ? p.waterlogging : '--',
  859. drainage: p.drainage != null ? p.drainage : '--'
  860. }))
  861. // 5. 报警设备列表(只显示分区关联设备的报警)
  862. try {
  863. const alarmRes = await getAlarmPage(1, 100, { equipmentTypeId: 'drainage' })
  864. const alarmPageData = alarmRes.code !== undefined ? alarmRes.data : alarmRes
  865. const alarmList = alarmPageData?.records || alarmPageData?.rows || []
  866. // 只保留关联设备的报警记录
  867. const linkedEquipIds = new Set(uniqueIds)
  868. const linkedEquipCodes = new Set(Object.values(equipmentDetails).map(eq => eq.code).filter(Boolean))
  869. const filteredAlarms = alarmList.filter(a => {
  870. return linkedEquipIds.has(a.equipmentId) || linkedEquipCodes.has(a.equipmentCode || a.deviceCode)
  871. })
  872. rwWarnData.value = filteredAlarms.slice(0, 10).map(a => {
  873. const code = a.equipmentCode || a.deviceCode || a.code || ''
  874. const codeNameMap = {}
  875. Object.values(equipmentDetails).forEach(eq => { if (eq.code) codeNameMap[eq.code] = eq.name })
  876. return {
  877. deviceName: a.equipmentName || a.deviceName || a.name || codeNameMap[code] || '',
  878. time: a.createTime || '',
  879. deviceCode: code,
  880. deviceType: a.equipmentTypeName || a.deviceType || '排水设备'
  881. }
  882. })
  883. } catch (e) {
  884. rwWarnData.value = []
  885. }
  886. } catch (e) {
  887. console.error('加载雨水污水Tab数据失败:', e)
  888. }
  889. }
  890. // ==================== Tab 图表初始化 ====================
  891. function initTab1Charts() {
  892. initChart('rwPieChart', getPieOption(rwOnlineRate.value))
  893. // 近日排水量/积水量 - 用分区真实数据作柱状图
  894. const names = rwRealtimeData.value.map(r => r.name?.substring(0, 4) || '')
  895. const drainVals = rwRealtimeData.value.map(r => Number(r.drainage) || 0)
  896. const waterVals = rwRealtimeData.value.map(r => Number(r.waterlogging) || 0)
  897. initChart('rwDrainageBarChart', getBarOption(names, drainVals, '', '#409eff'))
  898. initChart('rwWaterlogBarChart', getBarOption(names, waterVals, '', '#e6a23c'))
  899. }
  900. function initTab2Charts() {
  901. initChart('devicePieChart', getPieOption(deviceOnlineRate.value))
  902. const months = ['1月', '2月', '3月', '4月', '5月', '6月']
  903. initChart('deviceRepairChart', getBarOption(months, months.map(() => Math.floor(Math.random() * 20)), '次', '#e6a23c'))
  904. }
  905. function initTab3Charts() {
  906. initChart('waterPieChart', getPieOption(waterOnlineRate.value))
  907. updateWaterStatChart()
  908. }
  909. async function updateWaterStatChart() {
  910. // 查询积水数据,按时间统计
  911. try {
  912. const res = await getWaterloggingPage(1, 9999, {})
  913. const pageData = res.code !== undefined ? res.data : res
  914. const allList = pageData?.records || pageData?.rows || []
  915. let xData = [], yData = []
  916. const now = new Date()
  917. if (waterStatType.value === 'month') {
  918. // 近6个月统计
  919. xData = []
  920. yData = []
  921. for (let i = 5; i >= 0; i--) {
  922. const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
  923. const month = d.getMonth() + 1
  924. const year = d.getFullYear()
  925. xData.push(`${month}月`)
  926. const count = allList.filter(item => {
  927. const t = item.createTime || ''
  928. if (!t) return false
  929. const itemDate = new Date(t)
  930. return itemDate.getFullYear() === year && itemDate.getMonth() + 1 === month
  931. }).length
  932. yData.push(count)
  933. }
  934. } else {
  935. // 近5年统计
  936. const currentYear = now.getFullYear()
  937. for (let i = 4; i >= 0; i--) {
  938. const y = currentYear - i
  939. xData.push(String(y))
  940. const count = allList.filter(item => {
  941. const t = item.createTime || ''
  942. if (!t) return false
  943. return new Date(t).getFullYear() === y
  944. }).length
  945. yData.push(count)
  946. }
  947. }
  948. initChart('waterStatChart', getBarOption(xData, yData, '次', '#00e5ff'))
  949. } catch (e) {
  950. console.error('积水统计加载失败:', e)
  951. initChart('waterStatChart', getBarOption([], [], '次', '#00e5ff'))
  952. }
  953. }
  954. async function doWaterAnalysis() {
  955. waterAnalysisResult.value = '正在查询...'
  956. try {
  957. const res = await getWaterloggingPage(1, 9999, {})
  958. const pageData = res.code !== undefined ? res.data : res
  959. const allList = pageData?.records || pageData?.rows || []
  960. const year = analysisYear.value
  961. const level = analysisLevel.value
  962. // 按年份过滤
  963. let filtered = allList.filter(item => {
  964. const t = item.createTime || ''
  965. if (!t) return false
  966. return new Date(t).getFullYear() === year
  967. })
  968. // 按等级过滤
  969. if (level !== 'all') {
  970. const getLevelCls = (item) => {
  971. const depth = Number(item.waterDepth || item.depth || item.waterlogging || 0)
  972. if (depth <= 0) return 'none'
  973. if (depth <= 15) return 'light'
  974. if (depth <= 30) return 'moderate'
  975. return 'severe'
  976. }
  977. filtered = filtered.filter(item => getLevelCls(item) === level)
  978. }
  979. const count = filtered.length
  980. const levelText = analysisLevels.find(l => l.value === level)?.label || '全部等级'
  981. waterAnalysisResult.value = `${year}年${levelText}积水事件共 ${count} 起。`
  982. } catch (e) {
  983. waterAnalysisResult.value = '查询失败,请稍后重试。'
  984. console.error('积水分析查询失败:', e)
  985. }
  986. }
  987. watch(waterStatType, () => nextTick(updateWaterStatChart))
  988. // ==================== Tab 切换与地图初始化 ====================
  989. function initTabMaps() {
  990. initBMapGL('rwMap', rwMap, rwMapPoints.value, null)
  991. initBMapGL('deviceMap', deviceMap, deviceMapPoints.value, (point) => { selectedDevice.value = point; deviceModalVisible.value = true })
  992. initBMapGL('waterMap', waterMap, waterMapPoints.value, null)
  993. }
  994. function waitForBMapAndInit() {
  995. let attempts = 0
  996. const maxAttempts = 100
  997. const tryInit = () => {
  998. attempts += 1
  999. if (typeof BMapGL !== 'undefined') {
  1000. initTabMaps()
  1001. return
  1002. }
  1003. if (attempts >= maxAttempts) {
  1004. pipelineMapError.value = '百度地图加载超时,请检查网络或地图配置。'
  1005. return
  1006. }
  1007. bmapInitTimer = window.setTimeout(tryInit, 200)
  1008. }
  1009. tryInit()
  1010. }
  1011. function switchTab(tab) {
  1012. activeTab.value = tab
  1013. nextTick(() => {
  1014. Object.values(chartInstances).forEach(c => c && c.resize())
  1015. if (tab === 0 && !rwMap.value) initBMapGL('rwMap', rwMap, rwMapPoints.value, null)
  1016. if (tab === 1 && !deviceMap.value) initBMapGL('deviceMap', deviceMap, deviceMapPoints.value, (point) => { selectedDevice.value = point; deviceModalVisible.value = true })
  1017. if (tab === 2 && !waterMap.value) initBMapGL('waterMap', waterMap, waterMapPoints.value, null)
  1018. })
  1019. }
  1020. function closeDeviceModal() { deviceModalVisible.value = false }
  1021. // ==================== 生命周期 ====================
  1022. onMounted(async () => {
  1023. updateTime()
  1024. timer = setInterval(() => { updateTime(); updateRefreshTime() }, 1000)
  1025. updateRefreshTime()
  1026. await Promise.all([loadAllData(), loadWeatherData()])
  1027. setTimeout(() => {
  1028. waitForBMapAndInit()
  1029. initTab1Charts()
  1030. initTab2Charts()
  1031. initTab3Charts()
  1032. }, 300)
  1033. })
  1034. onUnmounted(() => {
  1035. if (timer) clearInterval(timer)
  1036. if (bmapInitTimer) window.clearTimeout(bmapInitTimer)
  1037. Object.values(chartInstances).forEach(c => c && c.dispose())
  1038. })
  1039. window.addEventListener('resize', () => {
  1040. Object.values(chartInstances).forEach(c => c && c.resize())
  1041. })
  1042. </script>
  1043. <style scoped>
  1044. /* ==================== 全局 ==================== */
  1045. .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; }
  1046. /* ==================== 顶部 ==================== */
  1047. .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; }
  1048. .screen-header::after { content: ''; position: absolute; bottom: 0; left: 10%; right: 10%; height: 1px; background: linear-gradient(90deg, transparent, #409eff, transparent); }
  1049. .header-left { flex: 1; text-align: left; }
  1050. .header-time { font-size: 15px; color: #a0cfff; letter-spacing: 1px; }
  1051. .header-center { flex: 2; text-align: center; }
  1052. .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; }
  1053. .header-right { flex: 1; text-align: right; }
  1054. /* ==================== 主体三栏 ==================== */
  1055. .screen-body { flex: 1; display: flex; padding: 12px 12px 90px 12px; gap: 12px; min-height: 0; }
  1056. .panel-left, .panel-right { width: 22%; min-width: 260px; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
  1057. .panel-center { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
  1058. /* ==================== 面板盒子 ==================== */
  1059. .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; }
  1060. .panel-title { font-size: 14px; font-weight: 600; color: #7ec8e3; margin-bottom: 10px; display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
  1061. .title-icon { color: #409eff; font-size: 10px; }
  1062. .refresh-time { margin-left: auto; font-size: 11px; color: #607080; font-weight: 400; }
  1063. /* ==================== 天气 ==================== */
  1064. .weather-cards { display: flex; gap: 8px; flex-shrink: 0; }
  1065. .panel-left .panel-box:first-child { flex: none; }
  1066. .panel-left .panel-box:nth-child(2) { flex: none; }
  1067. .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; }
  1068. .weather-date { font-size: 12px; color: #a0cfff; margin-bottom: 4px; }
  1069. .weather-icon { font-size: 24px; margin: 4px 0; }
  1070. .weather-desc { font-size: 12px; color: #d0e8ff; margin-bottom: 2px; }
  1071. .weather-temp { font-size: 11px; color: #7ec8e3; }
  1072. .weather-wind { font-size: 10px; color: #607080; margin-top: 2px; }
  1073. /* ==================== 概况卡片 ==================== */
  1074. .overview-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 12px; flex-shrink: 0; }
  1075. .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; }
  1076. .ov-label { font-size: 12px; color: #a0cfff; }
  1077. .ov-value { font-size: 20px; font-weight: 700; color: #fff; margin-top: 6px; }
  1078. .ov-value.online { color: #67c23a; }
  1079. .ov-value.offline { color: #f56c6c; }
  1080. .ov-value.warn { color: #e6a23c; }
  1081. /* ==================== 环形图 ==================== */
  1082. .pie-chart-wrapper { flex: 1; display: flex; align-items: center; min-height: 0; }
  1083. .chart-area { width: 55%; height: 100%; min-height: 120px; }
  1084. .pie-legend { width: 45%; display: flex; flex-direction: column; gap: 8px; }
  1085. .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
  1086. .legend-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
  1087. .legend-label { color: #a0cfff; flex: 1; }
  1088. .legend-value { color: #fff; font-weight: 600; }
  1089. /* ==================== 表格 ==================== */
  1090. .realtime-table-wrap { flex: 1; overflow-y: auto; min-height: 0; }
  1091. .dark-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  1092. .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; }
  1093. .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; }
  1094. .dark-table tbody tr:nth-child(odd) { background: rgba(10, 25, 50, 0.4); }
  1095. .dark-table tbody tr:nth-child(even) { background: rgba(20, 40, 70, 0.3); }
  1096. .dark-table tbody tr:hover { background: rgba(64, 158, 255, 0.1); }
  1097. .level-tag { padding: 1px 6px; border-radius: 3px; font-size: 11px; }
  1098. .level-none { color: #67c23a; background: rgba(103, 194, 58, 0.15); }
  1099. .level-light { color: #409eff; background: rgba(64, 158, 255, 0.15); }
  1100. .level-moderate { color: #e6a23c; background: rgba(230, 162, 60, 0.15); }
  1101. .level-severe { color: #f56c6c; background: rgba(245, 108, 108, 0.15); }
  1102. /* ==================== 地图 ==================== */
  1103. .map-container { flex: 1; border-radius: 6px; overflow: hidden; border: 1px solid rgba(64, 158, 255, 0.2); min-height: 0; }
  1104. .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; }
  1105. .pipeline-status-dot { width: 9px; height: 9px; background: #00e5ff; border-radius: 50%; box-shadow: 0 0 8px rgba(0, 229, 255, 0.9); }
  1106. .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; }
  1107. .chart-area-sm { width: 100%; flex: 1; min-height: 130px; }
  1108. /* ==================== 预警卡片 ==================== */
  1109. .warn-summary { display: flex; gap: 8px; margin-bottom: 10px; flex-shrink: 0; }
  1110. .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; }
  1111. .warn-icon { font-size: 20px; }
  1112. .warn-label { font-size: 11px; color: #a0cfff; margin-top: 4px; }
  1113. .warn-value { font-size: 16px; font-weight: 700; color: #fff; margin-top: 2px; }
  1114. .warn-value.online { color: #67c23a; }
  1115. .warn-value.offline { color: #f56c6c; }
  1116. /* ==================== 警告信息 ==================== */
  1117. .warn-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
  1118. .warn-dot.red { background: #f56c6c; box-shadow: 0 0 6px #f56c6c; }
  1119. .warn-dot.orange { background: #e6a23c; box-shadow: 0 0 6px #e6a23c; }
  1120. .warn-dot.yellow { background: #f2d66c; box-shadow: 0 0 6px #f2d66c; }
  1121. /* ==================== 报警统计 ==================== */
  1122. .alarm-stat { display: flex; gap: 12px; justify-content: center; align-items: center; flex: 1; }
  1123. .alarm-stat-item { text-align: center; }
  1124. .alarm-icon { font-size: 28px; }
  1125. .alarm-label { font-size: 12px; color: #a0cfff; margin-top: 4px; }
  1126. .alarm-value { font-size: 20px; font-weight: 700; margin-top: 2px; }
  1127. .alarm-value.yellow { color: #f2d66c; }
  1128. .alarm-value.red { color: #f56c6c; }
  1129. /* ==================== 积水等级数量 ==================== */
  1130. .level-counts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; margin-top: 10px; font-size: 12px; color: #a0cfff; flex-shrink: 0; }
  1131. .level-count { display: flex; align-items: center; gap: 6px; }
  1132. .dot { width: 8px; height: 8px; border-radius: 50%; }
  1133. .dot.green { background: #67c23a; }
  1134. .dot.blue { background: #409eff; }
  1135. .dot.orange { background: #e6a23c; }
  1136. .dot.red { background: #f56c6c; }
  1137. /* ==================== 积水分析 ==================== */
  1138. .analysis-form { display: flex; flex-direction: column; gap: 10px; }
  1139. .form-row { display: flex; align-items: center; gap: 8px; }
  1140. .form-row label { color: #a0cfff; font-size: 12px; width: 60px; }
  1141. .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; }
  1142. .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; }
  1143. .analysis-btn:hover { background: rgba(64, 158, 255, 0.4); color: #fff; }
  1144. .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; }
  1145. /* ==================== 统计切换 ==================== */
  1146. .stat-tabs { display: flex; gap: 8px; margin-bottom: 8px; flex-shrink: 0; }
  1147. .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; }
  1148. .stat-tabs span.active { background: rgba(64, 158, 255, 0.3); color: #fff; border-color: rgba(64, 158, 255, 0.6); }
  1149. /* ==================== 底部 Tab ==================== */
  1150. .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); }
  1151. .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; }
  1152. .bottom-tab:hover { background: #e6f2ff; }
  1153. .bottom-tab.active { background: #409eff; color: #fff; border-color: #409eff; box-shadow: 0 0 12px rgba(64, 158, 255, 0.5); }
  1154. /* ==================== 弹窗 ==================== */
  1155. .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; }
  1156. .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); }
  1157. .device-modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  1158. .device-modal-title { font-size: 15px; font-weight: 600; color: #7ec8e3; }
  1159. .device-modal-close { font-size: 22px; color: #a0cfff; cursor: pointer; line-height: 1; }
  1160. .device-modal-close:hover { color: #fff; }
  1161. .device-info-list { display: flex; flex-direction: column; gap: 10px; }
  1162. .device-info-item { display: flex; justify-content: space-between; font-size: 13px; padding: 8px; background: rgba(10, 25, 50, 0.5); border-radius: 4px; }
  1163. .info-label { color: #a0cfff; }
  1164. .info-value { color: #fff; }
  1165. /* ==================== 滚动条 ==================== */
  1166. ::-webkit-scrollbar { width: 4px; }
  1167. ::-webkit-scrollbar-track { background: rgba(10, 25, 50, 0.3); }
  1168. ::-webkit-scrollbar-thumb { background: rgba(64, 158, 255, 0.3); border-radius: 2px; }
  1169. ::-webkit-scrollbar-thumb:hover { background: rgba(64, 158, 255, 0.5); }
  1170. </style>