gaoluyang
2026-06-24 712aa51536236d43e87273e4ce45ac5691dffad8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<script lang="ts" setup>
import type { NumberDictDataType } from '..\..\..\..\packages\effects\hooks\src';
 
import type { IotDeviceApi } from '#/api/iot/device/device';
 
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRouter } from 'vue-router';
 
import { DeviceStateEnum, DICT_TYPE } from '..\..\..\..\packages\constants\src';
import { getDictLabel, getDictOptions } from '..\..\..\..\packages\effects\hooks\src';
 
import { Card, Empty, Spin } from 'ant-design-vue';
 
import { getDeviceLocationList } from '#/api/iot/device/device';
import { loadBaiduMapSdk } from '#/components/map';
 
defineOptions({ name: 'DeviceMapCard' });
 
const router = useRouter();
const mapContainerRef = ref<HTMLElement>(); // 地图容器节点
let mapInstance: any = null; // 百度地图实例
const loading = ref(true); // 加载状态
const deviceList = ref<IotDeviceApi.Device[]>([]); // 设备分布列表
 
const hasData = computed(() => deviceList.value.length > 0); // 是否有数据
 
const stateOptions = computed(
  () =>
    getDictOptions(
      DICT_TYPE.IOT_DEVICE_STATE,
      'number',
    ) as NumberDictDataType[],
); // 状态图例列表(从字典获取)
 
const stateColorMap: Record<number, string> = {
  [DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
  [DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
  [DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
}; // 设备状态颜色映射
 
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
function getStateConfig(state: number): { color: string; name: string } {
  return {
    name: getDictLabel(DICT_TYPE.IOT_DEVICE_STATE, state) || '未知',
    color: stateColorMap[state] || '#909399',
  };
}
 
/** 创建自定义标记点图标 */
function createMarkerIcon(color: string, isOnline: boolean) {
  const size = isOnline ? 24 : 20;
  const svg = `
    <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24">
      <circle cx="12" cy="12" r="8" fill="${color}" stroke="white" stroke-width="2"/>
      ${isOnline ? `<circle cx="12" cy="12" r="10" fill="none" stroke="${color}" stroke-width="2" opacity="0.5"/>` : ''}
    </svg>
  `;
  const blob = new Blob([svg], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);
  return new window.BMapGL.Icon(url, new window.BMapGL.Size(size, size), {
    anchor: new window.BMapGL.Size(size / 2, size / 2),
  });
}
 
/** 初始化地图 */
function initMap() {
  if (!mapContainerRef.value || !window.BMapGL) {
    return;
  }
 
  // 销毁旧实例
  if (mapInstance) {
    mapInstance.destroy?.();
    mapInstance = null;
  }
 
  // 创建地图实例,默认以中国为中心
  mapInstance = new window.BMapGL.Map(mapContainerRef.value);
  mapInstance.centerAndZoom(new window.BMapGL.Point(106, 37.5), 5);
  mapInstance.enableScrollWheelZoom();
 
  // 添加控件
  mapInstance.addControl(new window.BMapGL.ScaleControl());
  mapInstance.addControl(new window.BMapGL.ZoomControl());
 
  // 添加设备标记点
  deviceList.value.forEach((device) => {
    const config = getStateConfig(device.state!);
    const isOnline = device.state === DeviceStateEnum.ONLINE;
    const point = new window.BMapGL.Point(device.longitude, device.latitude);
 
    // 创建标记
    const marker = new window.BMapGL.Marker(point, {
      icon: createMarkerIcon(config.color, isOnline),
    });
 
    // 创建信息窗口内容
    const infoContent = `
      <div style="padding: 8px; min-width: 180px;">
        <div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${device.nickname || device.deviceName}</div>
        <div style="color: #666; font-size: 12px; line-height: 1.8;">
          <div>产品: ${device.productName || '-'}</div>
          <div>状态: <span style="color: ${config.color}; font-weight: 500;">${config.name}</span></div>
        </div>
        <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee;">
          <a href="javascript:void(0)" class="device-link" data-id="${device.id}" style="color: #1890ff; font-size: 12px; text-decoration: none;">点击查看详情 →</a>
        </div>
      </div>
    `;
 
    // 点击标记显示信息窗口
    marker.addEventListener('click', () => {
      const infoWindow = new window.BMapGL.InfoWindow(infoContent, {
        width: 220,
        height: 140,
        title: '',
      });
 
      // 信息窗口打开后绑定链接点击事件
      infoWindow.addEventListener('open', () => {
        setTimeout(() => {
          const link = document.querySelector(
            '.BMap_bubble_content .device-link',
          );
          if (!link) {
            return;
          }
          link.addEventListener('click', (e) => {
            e.preventDefault();
            if (device.id === undefined || device.id === null) {
              return;
            }
            router.push({
              name: 'IoTDeviceDetail',
              params: { id: device.id },
            });
          });
        }, 100);
      });
 
      mapInstance.openInfoWindow(infoWindow, point);
    });
 
    mapInstance.addOverlay(marker);
  });
}
 
/** 加载设备数据 */
async function loadDeviceData() {
  loading.value = true;
  try {
    deviceList.value = await getDeviceLocationList();
  } finally {
    loading.value = false;
  }
}
 
/** 初始化 */
async function init() {
  await loadDeviceData();
  if (!hasData.value) {
    return;
  }
  await loadBaiduMapSdk();
  // 等待 v-show 容器渲染完成;SDK 缓存命中时上一行会同步 resolve,DOM 来不及切换
  await nextTick();
  initMap();
}
 
/** 组件挂载时初始化 */
onMounted(() => {
  init();
});
 
/** 组件卸载时销毁地图实例 */
onUnmounted(() => {
  if (mapInstance) {
    mapInstance.destroy?.();
    mapInstance = null;
  }
});
</script>
 
<template>
  <Card class="h-full" title="设备分布地图">
    <template #extra>
      <div class="flex items-center gap-4 text-sm">
        <span
          v-for="item in stateOptions"
          :key="item.value"
          class="flex items-center gap-1"
        >
          <span
            class="inline-block h-3 w-3 rounded-full"
            :style="{
              backgroundColor: stateColorMap[item.value],
            }"
          ></span>
          <span class="text-gray-500">{{ item.label }}</span>
        </span>
      </div>
    </template>
    <Spin v-if="loading" class="flex h-[500px] items-center justify-center" />
    <Empty
      v-else-if="!hasData"
      class="h-[500px]"
      description="暂无设备位置数据"
    />
    <div
      v-show="hasData && !loading"
      ref="mapContainerRef"
      class="h-[500px] w-full"
    ></div>
  </Card>
</template>