admin-vben/apps/web-antd/src/views/iot/home/modules/device-map-card.vue

215 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts" setup>
import type { NumberDictDataType } from '@vben/hooks';
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 '@vben/constants';
import { getDictLabel, getDictOptions } from '@vben/hooks';
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 缓存命中时上一行会同步 resolveDOM 来不及切换
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>