dqyjGIS.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. <template>
  2. <div class="gis-page">
  3. <!-- 左侧浮动面板 -->
  4. <div class="side-panel">
  5. <div class="panel-header">
  6. <span class="panel-title">当前预警分布</span>
  7. </div>
  8. <!-- 等级筛选 -->
  9. <div class="panel-section">
  10. <div class="section-label">等级筛选</div>
  11. <div class="layer-list">
  12. <div class="layer-item">
  13. <el-checkbox v-model="levelChecked.severe" @change="onLevelChange">
  14. <span class="level-dot" style="background:#f56c6c"></span> 严重
  15. </el-checkbox>
  16. </div>
  17. <div class="layer-item">
  18. <el-checkbox v-model="levelChecked.important" @change="onLevelChange">
  19. <span class="level-dot" style="background:#e6a23c"></span> 重要
  20. </el-checkbox>
  21. </div>
  22. <div class="layer-item">
  23. <el-checkbox v-model="levelChecked.normal" @change="onLevelChange">
  24. <span class="level-dot" style="background:#409eff"></span> 一般
  25. </el-checkbox>
  26. </div>
  27. </div>
  28. </div>
  29. <!-- 统计数据 -->
  30. <div class="panel-section">
  31. <div class="section-label">预警统计</div>
  32. <div class="stats-row">
  33. <div class="stat-card">
  34. <div class="stat-value">{{ warningList.length }}</div>
  35. <div class="stat-text">总数</div>
  36. </div>
  37. <div class="stat-card stat-severe">
  38. <div class="stat-value">{{ severeCount }}</div>
  39. <div class="stat-text">严重</div>
  40. </div>
  41. <div class="stat-card stat-important">
  42. <div class="stat-value">{{ importantCount }}</div>
  43. <div class="stat-text">重要</div>
  44. </div>
  45. <div class="stat-card stat-normal">
  46. <div class="stat-value">{{ normalCount }}</div>
  47. <div class="stat-text">一般</div>
  48. </div>
  49. </div>
  50. </div>
  51. <!-- 搜索框 -->
  52. <div class="panel-section">
  53. <div class="section-label">快速定位</div>
  54. <el-input
  55. v-model="searchKey"
  56. placeholder="输入设备名称搜索"
  57. clearable
  58. prefix-icon="Search"
  59. @input="onSearch"
  60. />
  61. <div class="search-results" v-if="searchResults.length > 0">
  62. <div
  63. class="search-item"
  64. v-for="item in searchResults"
  65. :key="item.id"
  66. @click="locatePoint(item)"
  67. >
  68. <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
  69. <span class="search-name">{{ item.name }}</span>
  70. <span class="search-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. <!-- 地图容器 -->
  76. <div id="dqyjGISMap" class="map-container"></div>
  77. <!-- 详情弹窗 -->
  78. <el-dialog
  79. v-model="detailVisible"
  80. :title="currentItem.name"
  81. width="480px"
  82. :close-on-click-modal="false"
  83. class="detail-dialog"
  84. >
  85. <div class="detail-content">
  86. <div class="detail-header">
  87. <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">
  88. {{ currentItem.level }}
  89. </el-tag>
  90. <span class="detail-name">{{ currentItem.name }}</span>
  91. </div>
  92. <div class="detail-info">
  93. <div class="info-row">
  94. <span class="info-label">预警编号</span>
  95. <span class="info-value">{{ currentItem.warningNo }}</span>
  96. </div>
  97. <div class="info-row">
  98. <span class="info-label">预警级别</span>
  99. <span class="info-value">
  100. <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">{{ currentItem.level }}</el-tag>
  101. </span>
  102. </div>
  103. <div class="info-row">
  104. <span class="info-label">预警时间</span>
  105. <span class="info-value">{{ currentItem.createTime }}</span>
  106. </div>
  107. <div class="info-row">
  108. <span class="info-label">位置</span>
  109. <span class="info-value">{{ currentItem.location }}</span>
  110. </div>
  111. <div class="info-row">
  112. <span class="info-label">状态</span>
  113. <span class="info-value">{{ currentItem.statusText }}</span>
  114. </div>
  115. </div>
  116. </div>
  117. </el-dialog>
  118. </div>
  119. </template>
  120. <script setup name="DqyjGIS">
  121. import { ref, reactive, computed, onMounted, nextTick } from 'vue'
  122. import { Search } from '@element-plus/icons-vue'
  123. import { ElMessage } from 'element-plus'
  124. import locationIcon from '@/assets/images/location.png'
  125. import { getWarningCurrentPage } from '@/api/drainage'
  126. import useUserStore from '@/store/modules/user'
  127. // ==================== 用户信息 ====================
  128. const userStore = useUserStore()
  129. const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
  130. // ==================== 等级筛选 ====================
  131. const levelChecked = reactive({ severe: true, important: true, normal: true })
  132. // ==================== 预警数据(从API加载)====================
  133. const warningList = ref([])
  134. // 预警级别数字→文字映射
  135. const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
  136. function statusText(status) {
  137. const map = { DRAFT: '草稿', PENDING: '待办', PROCESSING: '处置中', RELEASED: '已发布', HANDLED: '已处置', CLOSED: '已解除' }
  138. return map[status] ?? '-'
  139. }
  140. // 加载当前预警数据
  141. async function loadData() {
  142. try {
  143. const params = {
  144. equipmentType: 'drainage',
  145. warningType: '2',
  146. currentOnly: true,
  147. userId: userStore.id,
  148. isAdmin: isAdmin.value
  149. }
  150. if (userStore.deptId) {
  151. params.deptId = userStore.deptId
  152. }
  153. const res = await getWarningCurrentPage(1, 1000, params)
  154. const pageData = res.code !== undefined ? res.data : res
  155. const records = pageData.records || []
  156. const hasCoordCount = records.filter(w => w.longitude && w.latitude).length
  157. console.log('[GIS预警] API返回记录数:', records.length, '有坐标记录数:', hasCoordCount)
  158. if (records.length > 0 && hasCoordCount === 0) {
  159. console.warn('[GIS预警] 所有预警均无经纬度坐标,请检查发布预警时设备是否有定位数据')
  160. }
  161. const noCoordRecords = records.filter(w => !w.longitude || !w.latitude)
  162. if (noCoordRecords.length > 0) {
  163. console.warn('[GIS预警] 无坐标预警(将被过滤):', noCoordRecords.map(w => ({ id: w.id, name: w.equipmentName, longitude: w.longitude, latitude: w.latitude })))
  164. }
  165. warningList.value = records.map(w => ({
  166. id: w.id,
  167. name: w.equipmentName || '-',
  168. warningNo: w.warningNo || '-',
  169. level: levelMap[w.warningLevel] || '一般',
  170. createTime: w.createTime || '-',
  171. location: w.location || '-',
  172. statusText: statusText(w.status),
  173. lng: parseFloat(w.longitude),
  174. lat: parseFloat(w.latitude)
  175. })).filter(w => w.lng && w.lat)
  176. } catch (error) {
  177. console.error('加载当前预警数据失败', error)
  178. ElMessage.error('加载当前预警数据失败')
  179. }
  180. }
  181. // ==================== 统计计算 ====================
  182. const severeCount = computed(() => warningList.value.filter(v => v.level === '严重').length)
  183. const importantCount = computed(() => warningList.value.filter(v => v.level === '重要').length)
  184. const normalCount = computed(() => warningList.value.filter(v => v.level === '一般').length)
  185. // ==================== 工具方法 ====================
  186. function getLevelColor(level) {
  187. const map = { '严重': '#f56c6c', '重要': '#e6a23c', '一般': '#409eff' }
  188. return map[level] || '#409eff'
  189. }
  190. function getLevelClass(level) {
  191. const map = { '严重': 'level-severe', '重要': 'level-important', '一般': 'level-normal' }
  192. return map[level] || ''
  193. }
  194. function getTagType(level) {
  195. const map = { '严重': 'danger', '重要': 'warning', '一般': '' }
  196. return map[level] || ''
  197. }
  198. function getLevelKey(level) {
  199. const map = { '严重': 'severe', '重要': 'important', '一般': 'normal' }
  200. return map[level] || 'normal'
  201. }
  202. // ==================== 搜索功能 ====================
  203. const searchKey = ref('')
  204. const searchResults = ref([])
  205. function onSearch() {
  206. if (!searchKey.value.trim()) {
  207. searchResults.value = []
  208. return
  209. }
  210. const key = searchKey.value.trim().toLowerCase()
  211. searchResults.value = warningList.value.filter(v =>
  212. v.name.toLowerCase().includes(key) || v.location.toLowerCase().includes(key)
  213. )
  214. }
  215. function locatePoint(item) {
  216. if (mapInstance) {
  217. mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
  218. openDetail(item)
  219. }
  220. searchResults.value = []
  221. searchKey.value = ''
  222. }
  223. // ==================== 地图相关 ====================
  224. let mapInstance = null
  225. const markerMap = new Map()
  226. function initMap() {
  227. const container = document.getElementById('dqyjGISMap')
  228. if (!container || typeof BMapGL === 'undefined') {
  229. console.warn('BMapGL 未加载或容器不存在')
  230. return
  231. }
  232. try {
  233. mapInstance = new BMapGL.Map('dqyjGISMap')
  234. const centerPoint = new BMapGL.Point(110.393, 28.452)
  235. mapInstance.centerAndZoom(centerPoint, 15)
  236. mapInstance.enableScrollWheelZoom(true)
  237. warningList.value.forEach(point => {
  238. addMapMarker(point)
  239. })
  240. // 默认定位到第一个预警点位
  241. if (warningList.value.length > 0) {
  242. const firstPoint = warningList.value[0]
  243. mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
  244. }
  245. } catch (e) {
  246. console.error('百度地图初始化失败:', e)
  247. }
  248. }
  249. function addMapMarker(point) {
  250. const color = getLevelColor(point.level)
  251. const bPoint = new BMapGL.Point(point.lng, point.lat)
  252. const html = `<div style="text-align:center;cursor:pointer;">
  253. <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
  254. ${point.name}
  255. </div>
  256. <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
  257. </div>`
  258. const label = new BMapGL.Label(html, {
  259. position: bPoint,
  260. offset: new BMapGL.Size(-30, -50)
  261. })
  262. label.setStyle({
  263. border: 'none',
  264. background: 'transparent',
  265. padding: '0',
  266. zIndex: '10'
  267. })
  268. mapInstance.addOverlay(label)
  269. markerMap.set(point.id, label)
  270. label.addEventListener('click', function () {
  271. openDetail(point)
  272. })
  273. }
  274. // ==================== 等级筛选 ====================
  275. function onLevelChange() {
  276. warningList.value.forEach(point => {
  277. const label = markerMap.get(point.id)
  278. if (!label) return
  279. const key = getLevelKey(point.level)
  280. const visible = levelChecked[key]
  281. label.show()
  282. if (!visible) {
  283. label.hide()
  284. }
  285. })
  286. }
  287. // ==================== 详情弹窗 ====================
  288. const detailVisible = ref(false)
  289. const currentItem = ref({})
  290. function openDetail(point) {
  291. currentItem.value = { ...point }
  292. detailVisible.value = true
  293. }
  294. // ==================== 生命周期 ====================
  295. onMounted(async () => {
  296. await loadData()
  297. nextTick(() => {
  298. initMap()
  299. })
  300. })
  301. </script>
  302. <style scoped>
  303. .gis-page {
  304. position: relative;
  305. width: 100%;
  306. height: 100vh;
  307. overflow: hidden;
  308. }
  309. .map-container {
  310. width: 100%;
  311. height: 100%;
  312. }
  313. /* ==================== 左侧浮动面板 ==================== */
  314. .side-panel {
  315. position: absolute;
  316. top: 16px;
  317. left: 16px;
  318. width: 280px;
  319. background: rgba(6, 30, 65, 0.85);
  320. border-radius: 8px;
  321. z-index: 100;
  322. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  323. padding: 0;
  324. max-height: calc(100% - 32px);
  325. overflow-y: auto;
  326. }
  327. .side-panel::-webkit-scrollbar {
  328. width: 4px;
  329. }
  330. .side-panel::-webkit-scrollbar-thumb {
  331. background: rgba(255, 255, 255, 0.2);
  332. border-radius: 2px;
  333. }
  334. .panel-header {
  335. padding: 16px 18px 12px;
  336. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  337. }
  338. .panel-title {
  339. font-size: 16px;
  340. font-weight: 600;
  341. color: #fff;
  342. letter-spacing: 1px;
  343. }
  344. .panel-section {
  345. padding: 14px 18px;
  346. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  347. }
  348. .panel-section:last-child {
  349. border-bottom: none;
  350. }
  351. .section-label {
  352. font-size: 12px;
  353. color: rgba(255, 255, 255, 0.5);
  354. margin-bottom: 10px;
  355. text-transform: uppercase;
  356. letter-spacing: 1px;
  357. }
  358. /* 等级列表 */
  359. .layer-list {
  360. display: flex;
  361. flex-direction: column;
  362. gap: 6px;
  363. }
  364. .layer-item :deep(.el-checkbox) {
  365. color: rgba(255, 255, 255, 0.85);
  366. height: auto;
  367. }
  368. .layer-item :deep(.el-checkbox__label) {
  369. color: rgba(255, 255, 255, 0.85);
  370. font-size: 13px;
  371. display: inline-flex;
  372. align-items: center;
  373. gap: 4px;
  374. }
  375. .layer-item :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
  376. background: #409eff;
  377. border-color: #409eff;
  378. }
  379. .level-dot {
  380. display: inline-block;
  381. width: 10px;
  382. height: 10px;
  383. border-radius: 50%;
  384. margin-right: 2px;
  385. vertical-align: middle;
  386. }
  387. /* 统计卡片 */
  388. .stats-row {
  389. display: flex;
  390. gap: 8px;
  391. }
  392. .stat-card {
  393. flex: 1;
  394. background: rgba(255, 255, 255, 0.08);
  395. border-radius: 6px;
  396. padding: 10px 4px;
  397. text-align: center;
  398. }
  399. .stat-card.stat-severe {
  400. background: rgba(245, 108, 108, 0.15);
  401. }
  402. .stat-card.stat-important {
  403. background: rgba(230, 162, 60, 0.15);
  404. }
  405. .stat-card.stat-normal {
  406. background: rgba(64, 158, 255, 0.15);
  407. }
  408. .stat-value {
  409. font-size: 20px;
  410. font-weight: 700;
  411. color: #fff;
  412. line-height: 1.2;
  413. }
  414. .stat-severe .stat-value {
  415. color: #f56c6c;
  416. }
  417. .stat-important .stat-value {
  418. color: #e6a23c;
  419. }
  420. .stat-normal .stat-value {
  421. color: #409eff;
  422. }
  423. .stat-text {
  424. font-size: 11px;
  425. color: rgba(255, 255, 255, 0.5);
  426. margin-top: 4px;
  427. }
  428. /* 搜索 */
  429. .side-panel :deep(.el-input__wrapper) {
  430. background: rgba(255, 255, 255, 0.08);
  431. box-shadow: none;
  432. border: 1px solid rgba(255, 255, 255, 0.12);
  433. }
  434. .side-panel :deep(.el-input__inner) {
  435. color: #fff;
  436. font-size: 13px;
  437. }
  438. .side-panel :deep(.el-input__inner::placeholder) {
  439. color: rgba(255, 255, 255, 0.35);
  440. }
  441. .side-panel :deep(.el-input__prefix .el-icon) {
  442. color: rgba(255, 255, 255, 0.4);
  443. }
  444. .search-results {
  445. margin-top: 8px;
  446. max-height: 200px;
  447. overflow-y: auto;
  448. border-radius: 4px;
  449. }
  450. .search-results::-webkit-scrollbar {
  451. width: 3px;
  452. }
  453. .search-results::-webkit-scrollbar-thumb {
  454. background: rgba(255, 255, 255, 0.15);
  455. border-radius: 2px;
  456. }
  457. .search-item {
  458. display: flex;
  459. align-items: center;
  460. padding: 7px 10px;
  461. cursor: pointer;
  462. border-radius: 4px;
  463. transition: background 0.15s;
  464. }
  465. .search-item:hover {
  466. background: rgba(255, 255, 255, 0.1);
  467. }
  468. .search-name {
  469. flex: 1;
  470. font-size: 12px;
  471. color: rgba(255, 255, 255, 0.85);
  472. margin-left: 6px;
  473. overflow: hidden;
  474. text-overflow: ellipsis;
  475. white-space: nowrap;
  476. }
  477. .search-level {
  478. font-size: 11px;
  479. padding: 1px 6px;
  480. border-radius: 3px;
  481. }
  482. .search-level.level-severe {
  483. color: #f56c6c;
  484. background: rgba(245, 108, 108, 0.15);
  485. }
  486. .search-level.level-important {
  487. color: #e6a23c;
  488. background: rgba(230, 162, 60, 0.15);
  489. }
  490. .search-level.level-normal {
  491. color: #409eff;
  492. background: rgba(64, 158, 255, 0.15);
  493. }
  494. /* ==================== 详情弹窗样式 ==================== */
  495. .detail-dialog :deep(.el-dialog) {
  496. border-radius: 10px;
  497. overflow: hidden;
  498. }
  499. .detail-dialog :deep(.el-dialog__header) {
  500. background: #1a1a2e;
  501. padding: 14px 20px;
  502. margin: 0;
  503. }
  504. .detail-dialog :deep(.el-dialog__title) {
  505. color: #fff;
  506. font-size: 15px;
  507. }
  508. .detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
  509. color: rgba(255, 255, 255, 0.6);
  510. }
  511. .detail-dialog :deep(.el-dialog__body) {
  512. padding: 0;
  513. }
  514. .detail-content {
  515. padding: 20px;
  516. }
  517. .detail-header {
  518. display: flex;
  519. align-items: center;
  520. gap: 10px;
  521. margin-bottom: 16px;
  522. padding-bottom: 12px;
  523. border-bottom: 1px solid #f0f0f0;
  524. }
  525. .detail-name {
  526. font-size: 16px;
  527. font-weight: 600;
  528. color: #303133;
  529. }
  530. .detail-info {
  531. display: flex;
  532. flex-direction: column;
  533. }
  534. .info-row {
  535. display: flex;
  536. align-items: center;
  537. padding: 8px 0;
  538. border-bottom: 1px solid #f5f5f5;
  539. }
  540. .info-row:last-child {
  541. border-bottom: none;
  542. }
  543. .info-label {
  544. width: 80px;
  545. flex-shrink: 0;
  546. font-size: 13px;
  547. color: #909399;
  548. }
  549. .info-value {
  550. font-size: 13px;
  551. color: #303133;
  552. }
  553. .info-value.highlight {
  554. color: #f56c6c;
  555. font-weight: 600;
  556. }
  557. </style>