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 5e7581274..1ce230f94 100644 --- a/apps/web-antd/src/api/iot/device/device/index.ts +++ b/apps/web-antd/src/api/iot/device/device/index.ts @@ -3,51 +3,17 @@ import type { PageParam, PageResult } from '@vben/request'; import { requestClient } from '#/api/request'; export namespace IotDeviceApi { - /** 设备新增/修改 Request VO */ - // TODO @haohao:可以降低一些 VO 哈:DeviceSaveReqVO、DeviceRespVO 合并成 Device 就好,类似别的模块 - export interface DeviceSaveReqVO { + /** 设备 */ + export interface Device { id?: number; // 设备编号 deviceName: string; // 设备名称 nickname?: string; // 备注名称 serialNumber?: string; // 设备序列号 picUrl?: string; // 设备图片 groupIds?: number[]; // 设备分组编号数组 - productId: number; // 产品编号(必填) - gatewayId?: number; // 网关设备 ID - config?: string; // 设备配置 - locationType: number; // 定位类型(必填) - latitude?: number; // 设备位置的纬度 - longitude?: number; // 设备位置的经度 - } - - /** 设备更新分组 Request VO */ - export interface DeviceUpdateGroupReqVO { - ids: number[]; // 设备编号列表(必填) - groupIds: number[]; // 分组编号列表(必填) - } - - /** 设备分页 Request VO */ - // TODO @haohao:可以不用 DevicePageReqVO,直接 PageParam 即可,简洁一点。这里的强类型,收益不大; - export interface DevicePageReqVO extends PageParam { - deviceName?: string; // 设备名称 - nickname?: string; // 备注名称 - productId?: number; // 产品编号 - deviceType?: number; // 设备类型 - status?: number; // 设备状态 - groupId?: number; // 设备分组编号 - gatewayId?: number; // 网关设备 ID - } - - /** 设备 Response VO */ - export interface DeviceRespVO { - id: number; // 设备编号 - deviceName: string; // 设备名称 - nickname?: string; // 设备备注名称 - serialNumber?: string; // 设备序列号 - picUrl?: string; // 设备图片 - groupIds?: number[]; // 设备分组编号数组 productId: number; // 产品编号 productKey?: string; // 产品标识 + productName?: string; // 产品名称(只有部分接口返回,例如 getDeviceLocationList) deviceType?: number; // 设备类型 gatewayId?: number; // 网关设备 ID state?: number; // 设备状态 @@ -57,12 +23,17 @@ export namespace IotDeviceApi { deviceSecret?: string; // 设备密钥,用于设备认证 authType?: string; // 认证类型(如一机一密、动态注册) config?: string; // 设备配置 - locationType?: number; // 定位方式 latitude?: number; // 设备位置的纬度 longitude?: number; // 设备位置的经度 createTime?: Date; // 创建时间 } + /** 设备更新分组 Request VO */ + export interface DeviceUpdateGroupReqVO { + ids: number[]; // 设备编号列表(必填) + groupIds: number[]; // 分组编号列表(必填) + } + /** 设备认证信息 Response VO */ export interface DeviceAuthInfoRespVO { clientId: string; // 客户端 ID @@ -104,8 +75,8 @@ export namespace IotDeviceApi { } /** 查询设备分页 */ -export function getDevicePage(params: IotDeviceApi.DevicePageReqVO) { - return requestClient.get>( +export function getDevicePage(params: PageParam) { + return requestClient.get>( '/iot/device/page', { params }, ); @@ -113,18 +84,16 @@ export function getDevicePage(params: IotDeviceApi.DevicePageReqVO) { /** 查询设备详情 */ export function getDevice(id: number) { - return requestClient.get( - `/iot/device/get?id=${id}`, - ); + return requestClient.get(`/iot/device/get?id=${id}`); } /** 新增设备 */ -export function createDevice(data: IotDeviceApi.DeviceSaveReqVO) { +export function createDevice(data: IotDeviceApi.Device) { return requestClient.post('/iot/device/create', data); } /** 修改设备 */ -export function updateDevice(data: IotDeviceApi.DeviceSaveReqVO) { +export function updateDevice(data: IotDeviceApi.Device) { return requestClient.put('/iot/device/update', data); } @@ -146,7 +115,7 @@ export function deleteDeviceList(ids: number[]) { } /** 导出设备 */ -export function exportDeviceExcel(params: IotDeviceApi.DevicePageReqVO) { +export function exportDeviceExcel(params: PageParam) { return requestClient.download('/iot/device/export-excel', { params }); } @@ -157,22 +126,21 @@ export function getDeviceCount(productId: number) { /** 获取设备的精简信息列表 */ export function getSimpleDeviceList(deviceType?: number, productId?: number) { - return requestClient.get( - '/iot/device/simple-list', - { - params: { deviceType, productId }, - }, - ); + return requestClient.get('/iot/device/simple-list', { + params: { deviceType, productId }, + }); } /** 根据产品编号,获取设备的精简信息列表 */ export function getDeviceListByProductId(productId: number) { - return requestClient.get( - '/iot/device/simple-list', - { - params: { productId }, - }, - ); + return requestClient.get('/iot/device/simple-list', { + params: { productId }, + }); +} + +/** 获取设备位置列表(用于地图展示) */ +export function getDeviceLocationList() { + return requestClient.get('/iot/device/location-list'); } /** 获取导入模板 */ 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/bpm/model/modules/category-draggable-model.vue b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue index d9a1aee5b..4071c1e16 100644 --- a/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue +++ b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue @@ -464,8 +464,6 @@ function handleRenameSuccess() { >
-
diff --git a/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue b/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue index 37e7937ef..bd4ff8353 100644 --- a/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue +++ b/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue @@ -168,10 +168,6 @@ async function initProcessInfo(row: any, formVariables?: any) { await router.push({ path: row.formCustomCreatePath, }); - // 返回选择流程 - // 这里为啥要有个 cancel 事件哈?目前看 vue3 + element-plus 貌似不需要呀; - // @芋艿 不加貌似会有点问题。 - emit('cancel'); } } diff --git a/apps/web-antd/src/views/infra/codegen/modules/import-table.vue b/apps/web-antd/src/views/infra/codegen/modules/import-table.vue index 38a78bae2..d7040011f 100644 --- a/apps/web-antd/src/views/infra/codegen/modules/import-table.vue +++ b/apps/web-antd/src/views/infra/codegen/modules/import-table.vue @@ -26,8 +26,17 @@ const formData = reactive({ tableNames: [], // 已选择的表列表 }); +/** 处理选择变化 */ +function handleCheckboxChange({ + records, +}: { + records: InfraCodegenApi.DatabaseTable[]; +}) { + formData.tableNames = records.map((item) => item.name); +} + /** 表格实例 */ -const [Grid] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions: { schema: useImportTableFormSchema(), submitOnChange: true, @@ -67,13 +76,8 @@ const [Grid] = useVbenVxeGrid({ }, } as VxeTableGridOptions, gridEvents: { - checkboxChange: ({ - records, - }: { - records: InfraCodegenApi.DatabaseTable[]; - }) => { - formData.tableNames = records.map((item) => item.name); - }, + checkboxChange: handleCheckboxChange, + checkboxAll: handleCheckboxChange, }, }); @@ -81,6 +85,13 @@ const [Grid] = useVbenVxeGrid({ const [Modal, modalApi] = useVbenModal({ title: '导入表', class: 'w-1/2', + async onOpenChange(isOpen: boolean) { + if (!isOpen) { + // 关闭时清空选择状态 + formData.tableNames = []; + await gridApi.grid?.clearCheckboxRow(); + } + }, async onConfirm() { modalApi.lock(); // 1.1 获取表单值 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/index.vue b/apps/web-antd/src/views/iot/device/device/detail/index.vue index 4ba957410..4b549a363 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/index.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/index.vue @@ -31,7 +31,7 @@ const router = useRouter(); const id = Number(route.params.id); const loading = ref(true); const product = ref({} as IotProductApi.Product); -const device = ref({} as IotDeviceApi.DeviceRespVO); +const device = ref({} as IotDeviceApi.Device); const activeTab = ref('info'); const thingModelList = ref([]); diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/config.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/config.vue index 72ad3a463..072a1ff77 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/config.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/config.vue @@ -12,7 +12,7 @@ import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'; defineOptions({ name: 'DeviceDetailConfig' }); const props = defineProps<{ - device: IotDeviceApi.DeviceRespVO; + device: IotDeviceApi.Device; }>(); const emit = defineEmits<{ @@ -114,7 +114,7 @@ async function updateDeviceConfig() { await updateDevice({ id: props.device.id, config: JSON.stringify(config.value), - } as IotDeviceApi.DeviceSaveReqVO); + } as IotDeviceApi.Device); message.success({ content: '更新成功!' }); // 触发 success 事件 emit('success'); diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/header.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/header.vue index 1cc42b791..8a739a180 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/header.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/header.vue @@ -12,7 +12,7 @@ import DeviceForm from '../../modules/form.vue'; interface Props { product: IotProductApi.Product; - device: IotDeviceApi.DeviceRespVO; + device: IotDeviceApi.Device; loading?: boolean; } @@ -50,7 +50,7 @@ function goToProductDetail(productId: number | undefined) { } /** 打开编辑表单 */ -function openEditForm(row: IotDeviceApi.DeviceRespVO) { +function openEditForm(row: IotDeviceApi.Device) { formModalApi.setData(row).open(); } 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 d1f5d66b7..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,20 +11,19 @@ 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.DeviceRespVO; + device: IotDeviceApi.Device; product: IotProductApi.Product; } @@ -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; } +