DeviceMapCard.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <template>
  2. <el-card class="chart-card" shadow="never">
  3. <template #header>
  4. <div class="flex items-center justify-between">
  5. <span class="text-base font-medium text-gray-600">设备分布地图</span>
  6. <div class="flex items-center gap-4 text-sm">
  7. <span v-for="item in stateOptions" :key="item.value" class="flex items-center gap-1">
  8. <span
  9. class="inline-block w-3 h-3 rounded-full"
  10. :style="{ backgroundColor: stateColorMap[item.value] }"
  11. ></span>
  12. <span class="text-gray-500">{{ item.label }}</span>
  13. </span>
  14. </div>
  15. </div>
  16. </template>
  17. <div v-if="loading" class="h-[500px] flex justify-center items-center">
  18. <el-empty description="加载中..." />
  19. </div>
  20. <div v-else-if="!hasData" class="h-[500px] flex justify-center items-center">
  21. <el-empty description="暂无设备位置数据" />
  22. </div>
  23. <div v-show="hasData && !loading" ref="mapContainerRef" class="h-[500px] w-full"></div>
  24. </el-card>
  25. </template>
  26. <script lang="ts" setup>
  27. import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
  28. import { useRouter } from 'vue-router'
  29. import { DICT_TYPE, getDictObj, getIntDictOptions } from '@/utils/dict'
  30. import { DeviceStateEnum } from '@/views/iot/utils/constants'
  31. import { loadBaiduMapSdk } from '@/components/Map/src/utils'
  32. defineOptions({ name: 'DeviceMapCard' })
  33. const router = useRouter()
  34. const mapContainerRef = ref<HTMLElement>()
  35. let mapInstance: any = null
  36. const loading = ref(true)
  37. const deviceList = ref<DeviceVO[]>([])
  38. /** 是否有数据 */
  39. const hasData = computed(() => deviceList.value.length > 0)
  40. /** 状态图例列表(从字典获取) */
  41. const stateOptions = computed(() => getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE))
  42. /** 设备状态颜色映射 */
  43. const stateColorMap: Record<number, string> = {
  44. [DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
  45. [DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
  46. [DeviceStateEnum.OFFLINE]: '#9CA3AF' // 离线 - 灰色
  47. }
  48. /** 获取设备状态配置(从字典获取) */
  49. const getStateConfig = (state: number): { name: string; color: string } => {
  50. const dict = getDictObj(DICT_TYPE.IOT_DEVICE_STATE, state)
  51. return {
  52. name: dict?.label || '未知',
  53. color: stateColorMap[state] || '#909399'
  54. }
  55. }
  56. /** 创建自定义标记点图标 */
  57. const createMarkerIcon = (color: string, isOnline: boolean) => {
  58. const size = isOnline ? 24 : 20
  59. const svg = `
  60. <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24">
  61. <circle cx="12" cy="12" r="8" fill="${color}" stroke="white" stroke-width="2"/>
  62. ${isOnline ? `<circle cx="12" cy="12" r="10" fill="none" stroke="${color}" stroke-width="2" opacity="0.5"/>` : ''}
  63. </svg>
  64. `
  65. const blob = new Blob([svg], { type: 'image/svg+xml' })
  66. const url = URL.createObjectURL(blob)
  67. return new window.BMapGL.Icon(url, new window.BMapGL.Size(size, size), {
  68. anchor: new window.BMapGL.Size(size / 2, size / 2)
  69. })
  70. }
  71. /** 初始化地图 */
  72. const initMap = () => {
  73. if (!mapContainerRef.value || !window.BMapGL) {
  74. return
  75. }
  76. // 销毁旧实例
  77. if (mapInstance) {
  78. mapInstance.destroy?.()
  79. mapInstance = null
  80. }
  81. // 创建地图实例,默认以中国为中心
  82. mapInstance = new window.BMapGL.Map(mapContainerRef.value)
  83. mapInstance.centerAndZoom(new window.BMapGL.Point(106, 37.5), 5)
  84. mapInstance.enableScrollWheelZoom()
  85. // 添加控件
  86. mapInstance.addControl(new window.BMapGL.ScaleControl())
  87. mapInstance.addControl(new window.BMapGL.ZoomControl())
  88. // 添加设备标记点
  89. deviceList.value.forEach((device) => {
  90. const config = getStateConfig(device.state)
  91. const isOnline = device.state === DeviceStateEnum.ONLINE
  92. const point = new window.BMapGL.Point(device.longitude, device.latitude)
  93. // 创建标记
  94. const marker = new window.BMapGL.Marker(point, {
  95. icon: createMarkerIcon(config.color, isOnline)
  96. })
  97. // 创建信息窗口内容
  98. const infoContent = `
  99. <div style="padding: 8px; min-width: 180px;">
  100. <div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${device.nickname || device.deviceName}</div>
  101. <div style="color: #666; font-size: 12px; line-height: 1.8;">
  102. <div>产品: ${device.productName || '-'}</div>
  103. <div>状态: <span style="color: ${config.color}; font-weight: 500;">${config.name}</span></div>
  104. </div>
  105. <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee;">
  106. <a href="javascript:void(0)" style="color: #409EFF; font-size: 12px; text-decoration: none;">点击查看详情 →</a>
  107. </div>
  108. </div>
  109. `
  110. // 点击标记显示信息窗口
  111. marker.addEventListener('click', () => {
  112. const infoWindow = new window.BMapGL.InfoWindow(infoContent, {
  113. width: 220,
  114. height: 140,
  115. title: ''
  116. })
  117. // 信息窗口打开后绑定链接点击事件
  118. infoWindow.addEventListener('open', () => {
  119. setTimeout(() => {
  120. const link = document.querySelector('.BMap_bubble_content a')
  121. if (link) {
  122. link.addEventListener('click', (e) => {
  123. e.preventDefault()
  124. router.push({ name: 'IoTDeviceDetail', params: { id: device.id } })
  125. })
  126. }
  127. }, 100)
  128. })
  129. mapInstance.openInfoWindow(infoWindow, point)
  130. })
  131. mapInstance.addOverlay(marker)
  132. })
  133. }
  134. /** 加载设备数据 */
  135. const loadDeviceData = async () => {
  136. loading.value = true
  137. try {
  138. deviceList.value = await DeviceApi.getDeviceLocationList()
  139. } finally {
  140. loading.value = false
  141. }
  142. }
  143. /** 初始化 */
  144. const init = async () => {
  145. await loadDeviceData()
  146. if (!hasData.value) {
  147. return
  148. }
  149. await loadBaiduMapSdk()
  150. await nextTick()
  151. initMap()
  152. }
  153. /** 组件挂载时初始化 */
  154. onMounted(() => {
  155. init()
  156. })
  157. /** 组件卸载时销毁地图实例 */
  158. onUnmounted(() => {
  159. if (mapInstance) {
  160. mapInstance.destroy?.()
  161. mapInstance = null
  162. }
  163. })
  164. </script>