diff --git a/apps/web-antd/.env b/apps/web-antd/.env index 778a9fa1f..a0bfde11e 100644 --- a/apps/web-antd/.env +++ b/apps/web-antd/.env @@ -33,3 +33,6 @@ VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395 VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883 # VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB # VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== + +# 百度地图 +VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN \ No newline at end of file diff --git a/apps/web-antd/src/api/iot/device/device/index.ts b/apps/web-antd/src/api/iot/device/device/index.ts index 8d615a5d0..1ce230f94 100644 --- a/apps/web-antd/src/api/iot/device/device/index.ts +++ b/apps/web-antd/src/api/iot/device/device/index.ts @@ -13,6 +13,7 @@ export namespace IotDeviceApi { groupIds?: number[]; // 设备分组编号数组 productId: number; // 产品编号 productKey?: string; // 产品标识 + productName?: string; // 产品名称(只有部分接口返回,例如 getDeviceLocationList) deviceType?: number; // 设备类型 gatewayId?: number; // 网关设备 ID state?: number; // 设备状态 @@ -22,7 +23,6 @@ export namespace IotDeviceApi { deviceSecret?: string; // 设备密钥,用于设备认证 authType?: string; // 认证类型(如一机一密、动态注册) config?: string; // 设备配置 - locationType?: number; // 定位类型 latitude?: number; // 设备位置的纬度 longitude?: number; // 设备位置的经度 createTime?: Date; // 创建时间 @@ -138,6 +138,11 @@ export function getDeviceListByProductId(productId: number) { }); } +/** 获取设备位置列表(用于地图展示) */ +export function getDeviceLocationList() { + return requestClient.get('/iot/device/location-list'); +} + /** 获取导入模板 */ export function importDeviceTemplate() { return requestClient.download('/iot/device/get-import-template'); diff --git a/apps/web-antd/src/api/iot/product/product/index.ts b/apps/web-antd/src/api/iot/product/product/index.ts index 2833b1e5d..4109db34b 100644 --- a/apps/web-antd/src/api/iot/product/product/index.ts +++ b/apps/web-antd/src/api/iot/product/product/index.ts @@ -17,7 +17,6 @@ export namespace IotProductApi { description?: string; // 产品描述 status?: number; // 产品状态 deviceType?: number; // 设备类型 - locationType?: number; // 定位类型 netType?: number; // 联网方式 codecType?: string; // 数据格式(编解码器类型) dataFormat?: number; // 数据格式 diff --git a/apps/web-antd/src/components/map/index.ts b/apps/web-antd/src/components/map/index.ts new file mode 100644 index 000000000..c8b84da5a --- /dev/null +++ b/apps/web-antd/src/components/map/index.ts @@ -0,0 +1,3 @@ +export { default as MapDialog } from './src/map-dialog.vue'; + +export { loadBaiduMapSdk } from './src/utils'; diff --git a/apps/web-antd/src/components/map/src/map-dialog.vue b/apps/web-antd/src/components/map/src/map-dialog.vue new file mode 100644 index 000000000..7b8c0bd87 --- /dev/null +++ b/apps/web-antd/src/components/map/src/map-dialog.vue @@ -0,0 +1,287 @@ + + + + diff --git a/apps/web-antd/src/components/map/src/utils.ts b/apps/web-antd/src/components/map/src/utils.ts new file mode 100644 index 000000000..6fc66fa91 --- /dev/null +++ b/apps/web-antd/src/components/map/src/utils.ts @@ -0,0 +1,62 @@ +/** + * 百度地图 SDK 加载工具 + */ + +// 扩展 Window 接口以包含百度地图 GL API +declare global { + interface Window { + BMapGL: any; + } +} + +// 全局回调名称 +const CALLBACK_NAME = '__BAIDU_MAP_LOAD_CALLBACK__'; + +// SDK 加载状态 +let loadPromise: null | Promise = null; + +/** + * 加载百度地图 GL SDK + * @param timeout 超时时间(毫秒),默认 10000 + * @returns Promise + */ +export const loadBaiduMapSdk = (timeout = 10_000): Promise => { + // 已加载完成 + if (window.BMapGL) { + return Promise.resolve(); + } + + // 正在加载中,返回同一个 Promise + if (loadPromise) { + return loadPromise; + } + + loadPromise = new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + loadPromise = null; + reject(new Error('百度地图 SDK 加载超时')); + }, timeout); + + // 全局回调 + (window as any)[CALLBACK_NAME] = () => { + clearTimeout(timeoutId); + delete (window as any)[CALLBACK_NAME]; + resolve(); + }; + + // 创建 script 标签 + const script = document.createElement('script'); + script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${ + import.meta.env.VITE_BAIDU_MAP_KEY + }&callback=${CALLBACK_NAME}`; + script.onerror = () => { + clearTimeout(timeoutId); + loadPromise = null; + delete (window as any)[CALLBACK_NAME]; + reject(new Error('百度地图 SDK 加载失败')); + }; + document.body.append(script); + }); + + return loadPromise; +}; diff --git a/apps/web-antd/src/views/iot/device/device/data.ts b/apps/web-antd/src/views/iot/device/device/data.ts index 70a562449..456adb657 100644 --- a/apps/web-antd/src/views/iot/device/device/data.ts +++ b/apps/web-antd/src/views/iot/device/device/data.ts @@ -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(), }, ]; } diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/info.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/info.vue index 634daddf1..ded227804 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/info.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/info.vue @@ -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( {} as IotDeviceApi.DeviceAuthInfoRespVO, ); +const mapDialogRef = ref>(); -/** 控制地图显示的标志 */ -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; } + diff --git a/apps/web-antd/src/views/iot/home/index.vue b/apps/web-antd/src/views/iot/home/index.vue index 4a51e6873..e05863fe9 100644 --- a/apps/web-antd/src/views/iot/home/index.vue +++ b/apps/web-antd/src/views/iot/home/index.vue @@ -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(() => { - + + + + + + + + diff --git a/apps/web-antd/src/views/iot/home/modules/device-map-card.vue b/apps/web-antd/src/views/iot/home/modules/device-map-card.vue new file mode 100644 index 000000000..baa2eec75 --- /dev/null +++ b/apps/web-antd/src/views/iot/home/modules/device-map-card.vue @@ -0,0 +1,215 @@ + + + diff --git a/apps/web-antd/src/views/iot/product/product/data.ts b/apps/web-antd/src/views/iot/product/product/data.ts index abe84da71..116ad06d9 100644 --- a/apps/web-antd/src/views/iot/product/product/data.ts +++ b/apps/web-antd/src/views/iot/product/product/data.ts @@ -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', - }, ]; } diff --git a/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue b/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue index 5e5604766..aa762fec1 100644 --- a/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue +++ b/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue @@ -35,9 +35,6 @@ function formatDate(date?: Date | string) { :value="product.deviceType" /> - - {{ product.locationType ?? '-' }} - {{ formatDate(product.createTime) }} diff --git a/apps/web-antd/src/views/iot/utils/constants.ts b/apps/web-antd/src/views/iot/utils/constants.ts index 33d8a51bb..64d293866 100644 --- a/apps/web-antd/src/views/iot/utils/constants.ts +++ b/apps/web-antd/src/views/iot/utils/constants.ts @@ -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, // 属性