feat(iot):【设备定位】添加设备位置功能,支持地图展示和坐标选择
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DeviceTypeEnum, DICT_TYPE, LocationTypeEnum } from '@vben/constants';
|
||||
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
@@ -133,16 +133,6 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
},
|
||||
{
|
||||
fieldName: 'locationType',
|
||||
label: '定位类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'longitude',
|
||||
label: '设备经度',
|
||||
@@ -150,11 +140,16 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
placeholder: '请输入设备经度',
|
||||
class: 'w-full',
|
||||
min: -180,
|
||||
max: 180,
|
||||
precision: 6,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
rules: z
|
||||
.number()
|
||||
.min(-180, '经度范围为 -180 到 180')
|
||||
.max(180, '经度范围为 -180 到 180')
|
||||
.optional()
|
||||
.nullable(),
|
||||
},
|
||||
{
|
||||
fieldName: 'latitude',
|
||||
@@ -163,11 +158,16 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
placeholder: '请输入设备纬度',
|
||||
class: 'w-full',
|
||||
min: -90,
|
||||
max: 90,
|
||||
precision: 6,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
rules: z
|
||||
.number()
|
||||
.min(-90, '纬度范围为 -90 到 90')
|
||||
.max(90, '纬度范围为 -90 到 90')
|
||||
.optional()
|
||||
.nullable(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,17 +11,16 @@ import { formatDateTime } from '@vben/utils';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDeviceAuthInfo } from '#/api/iot/device/device';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { MapDialog } from '#/components/map';
|
||||
|
||||
interface Props {
|
||||
device: IotDeviceApi.Device;
|
||||
@@ -35,12 +34,18 @@ const authPasswordVisible = ref(false);
|
||||
const authInfo = ref<IotDeviceApi.DeviceAuthInfoRespVO>(
|
||||
{} as IotDeviceApi.DeviceAuthInfoRespVO,
|
||||
);
|
||||
const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
|
||||
|
||||
/** 控制地图显示的标志 */
|
||||
const showMap = computed(() => {
|
||||
/** 是否有位置信息 */
|
||||
const hasLocation = computed(() => {
|
||||
return !!(props.device.longitude && props.device.latitude);
|
||||
});
|
||||
|
||||
/** 打开地图弹窗 */
|
||||
function openMapDialog() {
|
||||
mapDialogRef.value?.open(props.device.longitude, props.device.latitude);
|
||||
}
|
||||
|
||||
/** 复制到剪贴板 */
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
@@ -67,106 +72,63 @@ function handleAuthInfoDialogClose() {
|
||||
authDialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧设备信息 -->
|
||||
<Col :span="12">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon class="mr-2 text-primary" icon="lucide:info" />
|
||||
<span>设备信息</span>
|
||||
</div>
|
||||
<Card title="设备信息">
|
||||
<Descriptions :column="3" bordered size="small">
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ product.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="product.deviceType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="DeviceName">
|
||||
{{ device.deviceName }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注名称">
|
||||
{{ device.nickname || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前状态">
|
||||
<DictTag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDateTime(device.createTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="激活时间">
|
||||
{{ formatDateTime(device.activeTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后上线时间">
|
||||
{{ formatDateTime(device.onlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后离线时间">
|
||||
{{ formatDateTime(device.offlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备位置">
|
||||
<template v-if="hasLocation">
|
||||
<span class="mr-2">
|
||||
{{ device.longitude }}, {{ device.latitude }}
|
||||
</span>
|
||||
<Button type="link" size="small" @click="openMapDialog">
|
||||
<IconifyIcon icon="lucide:map-pin" class="mr-1" />
|
||||
查看地图
|
||||
</Button>
|
||||
</template>
|
||||
<Descriptions :column="1" bordered size="small">
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ props.product.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ props.product.productKey }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="props.product.deviceType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="定位类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_LOCATION_TYPE"
|
||||
:value="props.product.locationType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="DeviceName">
|
||||
{{ props.device.deviceName }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注名称">
|
||||
{{ props.device.nickname || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||
:value="props.device.state"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDateTime(props.device.createTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="激活时间">
|
||||
{{ formatDateTime(props.device.activeTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后上线时间">
|
||||
{{ formatDateTime(props.device.onlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后离线时间">
|
||||
{{ formatDateTime(props.device.offlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleAuthInfoDialogOpen"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧地图 -->
|
||||
<Col :span="12">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon class="mr-2 text-primary" icon="lucide:map-pin" />
|
||||
<span>设备位置</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
最后上线:{{ formatDateTime(props.device.onlineTime) || '--' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-[500px] w-full">
|
||||
<div
|
||||
v-if="showMap"
|
||||
class="flex h-full w-full items-center justify-center rounded bg-gray-100"
|
||||
>
|
||||
<span class="text-gray-400">地图组件</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
|
||||
>
|
||||
<IconifyIcon class="mr-2" icon="lucide:alert-triangle" />
|
||||
<span>暂无位置信息</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<span v-else class="text-gray-400">暂无位置信息</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button size="small" type="link" @click="handleAuthInfoDialogOpen">
|
||||
查看
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 认证信息弹框 -->
|
||||
<Modal
|
||||
@@ -226,5 +188,8 @@ function handleAuthInfoDialogClose() {
|
||||
<Button @click="handleAuthInfoDialogClose">关闭</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 地图弹窗 -->
|
||||
<MapDialog ref="mapDialogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DeviceStateEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
|
||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||
import {
|
||||
DeviceStateEnum,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
@@ -6,11 +6,12 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Collapse, message } from 'ant-design-vue';
|
||||
import { Button, Collapse, message, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { MapDialog } from '#/components/map';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
||||
@@ -21,6 +22,7 @@ const emit = defineEmits(['success']);
|
||||
const formData = ref<IotDeviceApi.Device>();
|
||||
const products = ref<IotProductApi.Product[]>([]);
|
||||
const activeKey = ref<string[]>([]);
|
||||
const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
@@ -78,12 +80,38 @@ async function getAdvancedFormValues() {
|
||||
picUrl: formData.value?.picUrl,
|
||||
groupIds: formData.value?.groupIds,
|
||||
serialNumber: formData.value?.serialNumber,
|
||||
locationType: formData.value?.locationType,
|
||||
longitude: formData.value?.longitude,
|
||||
latitude: formData.value?.latitude,
|
||||
};
|
||||
}
|
||||
|
||||
/** 打开地图选择弹窗 */
|
||||
async function openMapDialog() {
|
||||
// 如果高级表单未挂载,先展开 Collapse
|
||||
if (!advancedFormApi.isMounted) {
|
||||
activeKey.value = ['advanced'];
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
}
|
||||
const values = await advancedFormApi.getValues();
|
||||
mapDialogRef.value?.open(
|
||||
values.longitude ? Number(values.longitude) : undefined,
|
||||
values.latitude ? Number(values.latitude) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理地图选择确认 */
|
||||
async function handleMapConfirm(data: {
|
||||
address: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
}) {
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setFieldValue('longitude', Number(data.longitude));
|
||||
await advancedFormApi.setFieldValue('latitude', Number(data.latitude));
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
@@ -160,8 +188,13 @@ onMounted(async () => {
|
||||
<Collapse v-model:active-key="activeKey" class="mt-4">
|
||||
<Collapse.Panel key="advanced" header="更多设置">
|
||||
<AdvancedForm />
|
||||
<Space class="mt-2">
|
||||
<Button type="primary" @click="openMapDialog">坐标拾取</Button>
|
||||
</Space>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</Modal>
|
||||
<!-- 地图选择弹窗 -->
|
||||
<MapDialog ref="mapDialogRef" @confirm="handleMapConfirm" />
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getStatisticsSummary } from '#/api/iot/statistics';
|
||||
|
||||
import { defaultStatsData } from './data';
|
||||
import DeviceCountCard from './modules/device-count-card.vue';
|
||||
import DeviceMapCard from './modules/device-map-card.vue';
|
||||
import DeviceStateCountCard from './modules/device-state-count-card.vue';
|
||||
import MessageTrendCard from './modules/message-trend-card.vue';
|
||||
|
||||
@@ -97,10 +98,17 @@ onMounted(() => {
|
||||
</Row>
|
||||
|
||||
<!-- 第三行:消息统计 -->
|
||||
<Row :gutter="16">
|
||||
<Row :gutter="16" class="mb-4">
|
||||
<Col :span="24">
|
||||
<MessageTrendCard />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第四行:设备分布地图 -->
|
||||
<Row :gutter="16">
|
||||
<Col :span="24">
|
||||
<DeviceMapCard />
|
||||
</Col>
|
||||
</Row>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
215
apps/web-antd/src/views/iot/home/modules/device-map-card.vue
Normal file
215
apps/web-antd/src/views/iot/home/modules/device-map-card.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Card, Empty, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getDeviceLocationList } from '#/api/iot/device/device';
|
||||
import { loadBaiduMapSdk } from '#/components/map';
|
||||
import { DeviceStateEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
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 stateColorMap: Record<number, string> = {
|
||||
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
||||
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
||||
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
||||
};
|
||||
|
||||
/** 获取设备状态配置 */
|
||||
function getStateConfig(state: number): { color: string; name: string } {
|
||||
const stateNames: Record<number, string> = {
|
||||
[DeviceStateEnum.INACTIVE]: '待激活',
|
||||
[DeviceStateEnum.ONLINE]: '在线',
|
||||
[DeviceStateEnum.OFFLINE]: '离线',
|
||||
};
|
||||
return {
|
||||
name: stateNames[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('.device-link');
|
||||
if (link) {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const deviceId = e.target as HTMLElement.dataset.id;
|
||||
if (deviceId) {
|
||||
router.push({
|
||||
name: 'IoTDeviceDetail',
|
||||
params: { id: deviceId },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 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();
|
||||
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 class="flex items-center gap-1">
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{ backgroundColor: stateColorMap[DeviceStateEnum.ONLINE] }"
|
||||
></span>
|
||||
<span class="text-gray-500">在线</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{ backgroundColor: stateColorMap[DeviceStateEnum.OFFLINE] }"
|
||||
></span>
|
||||
<span class="text-gray-500">离线</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: stateColorMap[DeviceStateEnum.INACTIVE],
|
||||
}"
|
||||
></span>
|
||||
<span class="text-gray-500">待激活</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>
|
||||
@@ -137,6 +137,7 @@ export function useBasicFormSchema(
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
// TODO @haohao:这个貌似不需要?!
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '产品状态',
|
||||
@@ -149,16 +150,6 @@ export function useBasicFormSchema(
|
||||
defaultValue: 0,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'locationType',
|
||||
label: '定位类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
|
||||
placeholder: '请选择定位类型',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,6 @@ function formatDate(date?: Date | string) {
|
||||
:value="product.deviceType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="定位类型">
|
||||
{{ product.locationType ?? '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDate(product.createTime) }}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -8,6 +8,13 @@ export const IOT_PROVIDE_KEY = {
|
||||
PRODUCT: 'IOT_PRODUCT',
|
||||
};
|
||||
|
||||
/** IoT 设备状态枚举 */
|
||||
export enum DeviceStateEnum {
|
||||
INACTIVE = 0, // 未激活
|
||||
OFFLINE = 2, // 离线
|
||||
ONLINE = 1, // 在线
|
||||
}
|
||||
|
||||
/** IoT 产品物模型类型枚举类 */
|
||||
export const IoTThingModelTypeEnum = {
|
||||
PROPERTY: 1, // 属性
|
||||
|
||||
Reference in New Issue
Block a user