Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -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
|
||||
@@ -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<PageResult<IotDeviceApi.DeviceRespVO>>(
|
||||
export function getDevicePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotDeviceApi.Device>>(
|
||||
'/iot/device/page',
|
||||
{ params },
|
||||
);
|
||||
@@ -113,18 +84,16 @@ export function getDevicePage(params: IotDeviceApi.DevicePageReqVO) {
|
||||
|
||||
/** 查询设备详情 */
|
||||
export function getDevice(id: number) {
|
||||
return requestClient.get<IotDeviceApi.DeviceRespVO>(
|
||||
`/iot/device/get?id=${id}`,
|
||||
);
|
||||
return requestClient.get<IotDeviceApi.Device>(`/iot/device/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增设备 */
|
||||
export function createDevice(data: IotDeviceApi.DeviceSaveReqVO) {
|
||||
export function createDevice(data: IotDeviceApi.Device) {
|
||||
return requestClient.post<number>('/iot/device/create', data);
|
||||
}
|
||||
|
||||
/** 修改设备 */
|
||||
export function updateDevice(data: IotDeviceApi.DeviceSaveReqVO) {
|
||||
export function updateDevice(data: IotDeviceApi.Device) {
|
||||
return requestClient.put<boolean>('/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<IotDeviceApi.DeviceRespVO[]>(
|
||||
'/iot/device/simple-list',
|
||||
{
|
||||
params: { deviceType, productId },
|
||||
},
|
||||
);
|
||||
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
|
||||
params: { deviceType, productId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据产品编号,获取设备的精简信息列表 */
|
||||
export function getDeviceListByProductId(productId: number) {
|
||||
return requestClient.get<IotDeviceApi.DeviceRespVO[]>(
|
||||
'/iot/device/simple-list',
|
||||
{
|
||||
params: { productId },
|
||||
},
|
||||
);
|
||||
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
|
||||
params: { productId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取设备位置列表(用于地图展示) */
|
||||
export function getDeviceLocationList() {
|
||||
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/location-list');
|
||||
}
|
||||
|
||||
/** 获取导入模板 */
|
||||
|
||||
@@ -17,7 +17,6 @@ export namespace IotProductApi {
|
||||
description?: string; // 产品描述
|
||||
status?: number; // 产品状态
|
||||
deviceType?: number; // 设备类型
|
||||
locationType?: number; // 定位类型
|
||||
netType?: number; // 联网方式
|
||||
codecType?: string; // 数据格式(编解码器类型)
|
||||
dataFormat?: number; // 数据格式
|
||||
|
||||
3
apps/web-antd/src/components/map/index.ts
Normal file
3
apps/web-antd/src/components/map/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as MapDialog } from './src/map-dialog.vue';
|
||||
|
||||
export { loadBaiduMapSdk } from './src/utils';
|
||||
287
apps/web-antd/src/components/map/src/map-dialog.vue
Normal file
287
apps/web-antd/src/components/map/src/map-dialog.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<!-- 地图选择弹窗组件:基于百度地图 GL 实现 -->
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Form, Input, Select, Space } from 'ant-design-vue';
|
||||
|
||||
import { loadBaiduMapSdk } from './utils';
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [
|
||||
data: {
|
||||
address: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
},
|
||||
];
|
||||
}>();
|
||||
|
||||
const mapContainerRef = ref<HTMLElement>();
|
||||
const state = reactive({
|
||||
lonLat: '', // 经纬度字符串,格式为 "经度,纬度"
|
||||
address: '', // 地址信息
|
||||
loading: false, // 地址搜索加载状态
|
||||
latitude: '', // 纬度
|
||||
longitude: '', // 经度
|
||||
map: null as any, // 百度地图实例
|
||||
mapAddressOptions: [] as any[], // 地址搜索选项
|
||||
mapMarker: null as any, // 地图标记点
|
||||
geocoder: null as any, // 地理编码器实例
|
||||
mapContainerReady: false, // 地图容器是否准备好
|
||||
});
|
||||
|
||||
// 初始经纬度(打开弹窗时传入)
|
||||
const initLongitude = ref<number | undefined>();
|
||||
const initLatitude = ref<number | undefined>();
|
||||
|
||||
/** 弹窗打开动画完成后初始化地图 */
|
||||
async function handleDialogOpened() {
|
||||
// 先显示地图容器
|
||||
state.mapContainerReady = true;
|
||||
|
||||
// 等待下一个 DOM 更新周期,确保地图容器已渲染
|
||||
await nextTick();
|
||||
// 加载百度地图 SDK
|
||||
await loadBaiduMapSdk();
|
||||
initMapInstance();
|
||||
}
|
||||
|
||||
/** 弹窗关闭后清理地图 */
|
||||
function handleDialogClosed() {
|
||||
// 销毁地图实例
|
||||
if (state.map) {
|
||||
state.map.destroy?.();
|
||||
state.map = null;
|
||||
}
|
||||
state.mapMarker = null;
|
||||
state.geocoder = null;
|
||||
state.mapContainerReady = false;
|
||||
}
|
||||
|
||||
/** 初始化地图实例 */
|
||||
function initMapInstance() {
|
||||
if (!mapContainerRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化地图和地理编码器
|
||||
initMap();
|
||||
initGeocoder();
|
||||
|
||||
// 监听地图点击事件
|
||||
state.map.addEventListener('click', (e: any) => {
|
||||
const point = e.latlng;
|
||||
state.lonLat = `${point.lng},${point.lat}`;
|
||||
regeoCode(state.lonLat);
|
||||
});
|
||||
|
||||
// 如果有初始经纬度,加载标记点
|
||||
if (initLongitude.value && initLatitude.value) {
|
||||
const lonLat = `${initLongitude.value},${initLatitude.value}`;
|
||||
regeoCode(lonLat);
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化地图 */
|
||||
function initMap() {
|
||||
state.map = new window.BMapGL.Map(mapContainerRef.value);
|
||||
state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11);
|
||||
state.map.enableScrollWheelZoom();
|
||||
state.map.disableDoubleClickZoom();
|
||||
|
||||
state.map.addControl(new window.BMapGL.NavigationControl());
|
||||
state.map.addControl(new window.BMapGL.ScaleControl());
|
||||
state.map.addControl(new window.BMapGL.ZoomControl());
|
||||
}
|
||||
|
||||
/** 初始化地理编码器 */
|
||||
function initGeocoder() {
|
||||
state.geocoder = new window.BMapGL.Geocoder();
|
||||
}
|
||||
|
||||
/** 搜索地址 */
|
||||
function autoSearch(queryValue: string) {
|
||||
if (!queryValue) {
|
||||
state.mapAddressOptions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
const localSearch = new window.BMapGL.LocalSearch(state.map, {
|
||||
onSearchComplete: (results: any) => {
|
||||
state.loading = false;
|
||||
const temp: any[] = [];
|
||||
|
||||
if (results && results._pois) {
|
||||
results._pois.forEach((p: any) => {
|
||||
const point = p.point;
|
||||
if (point && point.lng && point.lat) {
|
||||
temp.push({
|
||||
name: p.title,
|
||||
value: `${point.lng},${point.lat}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.mapAddressOptions = temp;
|
||||
},
|
||||
});
|
||||
|
||||
localSearch.search(queryValue);
|
||||
}
|
||||
|
||||
/** 处理地址选择 */
|
||||
function handleAddressSelect(value: string) {
|
||||
if (value) {
|
||||
regeoCode(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加标记点 */
|
||||
function setMarker(lnglat: string[]) {
|
||||
if (!lnglat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mapMarker !== null) {
|
||||
state.map.removeOverlay(state.mapMarker);
|
||||
}
|
||||
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.mapMarker = new window.BMapGL.Marker(point);
|
||||
|
||||
state.map.addOverlay(state.mapMarker);
|
||||
state.map.centerAndZoom(point, 16);
|
||||
}
|
||||
|
||||
/** 经纬度转地址、添加标记点 */
|
||||
function regeoCode(lonLat: string) {
|
||||
if (!lonLat) {
|
||||
return;
|
||||
}
|
||||
const lnglat = lonLat.split(',');
|
||||
if (lnglat.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.longitude = lnglat[0]!;
|
||||
state.latitude = lnglat[1]!;
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.map.centerAndZoom(point, 16);
|
||||
|
||||
setMarker(lnglat);
|
||||
getAddress(lnglat);
|
||||
}
|
||||
|
||||
/** 根据经纬度获取地址信息 */
|
||||
function getAddress(lnglat: string[]) {
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.geocoder.getLocation(point, (result: any) => {
|
||||
if (result && result.address) {
|
||||
state.address = result.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm() {
|
||||
if (state.longitude && state.latitude) {
|
||||
emit('confirm', {
|
||||
longitude: state.longitude,
|
||||
latitude: state.latitude,
|
||||
address: state.address,
|
||||
});
|
||||
}
|
||||
modalApi.close();
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
handleDialogOpened();
|
||||
} else {
|
||||
handleDialogClosed();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
function open(longitude?: number, latitude?: number) {
|
||||
initLongitude.value = longitude;
|
||||
initLatitude.value = latitude;
|
||||
state.longitude = longitude ? String(longitude) : '';
|
||||
state.latitude = latitude ? String(latitude) : '';
|
||||
state.address = '';
|
||||
state.mapAddressOptions = [];
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :footer="false" class="w-[700px]" title="百度地图">
|
||||
<div class="w-full">
|
||||
<!-- 第一行:位置搜索 -->
|
||||
<Form :label-col="{ span: 4 }">
|
||||
<Form.Item label="定位位置">
|
||||
<Select
|
||||
v-model:value="state.address"
|
||||
:filter-option="false"
|
||||
:loading="state.loading"
|
||||
:options="
|
||||
state.mapAddressOptions.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
allow-clear
|
||||
class="w-full"
|
||||
placeholder="可输入地址查询经纬度"
|
||||
show-search
|
||||
@search="autoSearch"
|
||||
@select="handleAddressSelect"
|
||||
/>
|
||||
</Form.Item>
|
||||
<!-- 第二行:坐标显示 -->
|
||||
<Form.Item label="当前坐标">
|
||||
<Space>
|
||||
<Input
|
||||
:value="state.longitude"
|
||||
addon-before="经度"
|
||||
disabled
|
||||
style="width: 180px"
|
||||
/>
|
||||
<Input
|
||||
:value="state.latitude"
|
||||
addon-before="纬度"
|
||||
disabled
|
||||
style="width: 180px"
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<!-- 第三行:地图 -->
|
||||
<div
|
||||
v-if="state.mapContainerReady"
|
||||
ref="mapContainerRef"
|
||||
class="mt-[10px] h-[400px] w-full"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-[10px] flex h-[400px] w-full items-center justify-center"
|
||||
>
|
||||
<span class="text-gray-400">地图加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<Button type="primary" @click="handleConfirm">确 定</Button>
|
||||
<Button @click="modalApi.close()">取 消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
62
apps/web-antd/src/components/map/src/utils.ts
Normal file
62
apps/web-antd/src/components/map/src/utils.ts
Normal file
@@ -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<void> = null;
|
||||
|
||||
/**
|
||||
* 加载百度地图 GL SDK
|
||||
* @param timeout 超时时间(毫秒),默认 10000
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const loadBaiduMapSdk = (timeout = 10_000): Promise<void> => {
|
||||
// 已加载完成
|
||||
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;
|
||||
};
|
||||
@@ -464,8 +464,6 @@ function handleRenameSuccess() {
|
||||
>
|
||||
<div class="flex h-12 items-center">
|
||||
<!-- 头部:分类名 -->
|
||||
<!-- 2)拖动后,直接请求排序,不用有个【保存】;排序模型分类,和排序分类里的模型,交互有点不同哈。
|
||||
@芋艿 好像 yudao-ui-admin-vue3 交互也是这样的,需要改吗? -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip v-if="isCategorySorting" title="拖动排序">
|
||||
<!-- drag-handle 标识可以拖动,不能删掉 -->
|
||||
|
||||
@@ -168,10 +168,6 @@ async function initProcessInfo(row: any, formVariables?: any) {
|
||||
await router.push({
|
||||
path: row.formCustomCreatePath,
|
||||
});
|
||||
// 返回选择流程
|
||||
// 这里为啥要有个 cancel 事件哈?目前看 vue3 + element-plus 貌似不需要呀;
|
||||
// @芋艿 不加貌似会有点问题。
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,17 @@ const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
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<InfraCodegenApi.DatabaseTable>,
|
||||
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 获取表单值
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const router = useRouter();
|
||||
const id = Number(route.params.id);
|
||||
const loading = ref(true);
|
||||
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
|
||||
const device = ref<IotDeviceApi.DeviceRespVO>({} as IotDeviceApi.DeviceRespVO);
|
||||
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
|
||||
const activeTab = ref('info');
|
||||
const thingModelList = ref<ThingModelData[]>([]);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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<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';
|
||||
@@ -34,7 +34,7 @@ import DataDefinition from '../../../../thingmodel/modules/components/data-defin
|
||||
import DeviceDetailsMessage from './message.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.DeviceRespVO;
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Input, Select, Space } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDevicePage } from '#/api/iot/device/device';
|
||||
@@ -25,10 +25,6 @@ const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
|
||||
const products = ref<IotProductApi.Product[]>([]); // 产品列表
|
||||
const queryParams = reactive({
|
||||
deviceName: '',
|
||||
status: undefined as number | undefined,
|
||||
}); // 查询参数
|
||||
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
@@ -72,7 +68,35 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
];
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
/** 搜索表单 schema */
|
||||
function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'deviceName',
|
||||
label: 'DeviceName',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 DeviceName',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '设备状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number'),
|
||||
placeholder: '请选择设备状态',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.Device>({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
@@ -82,11 +106,14 @@ const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
query: async (
|
||||
{
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
},
|
||||
formValues?: { deviceName?: string; status?: number },
|
||||
) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
@@ -95,15 +122,15 @@ const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
pageSize: page.pageSize,
|
||||
gatewayId: props.deviceId,
|
||||
deviceType: DeviceTypeEnum.GATEWAY_SUB,
|
||||
deviceName: queryParams.deviceName || undefined,
|
||||
status: queryParams.status,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
deviceName: formValues?.deviceName || undefined,
|
||||
status: formValues?.status,
|
||||
} as PageParam);
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: false,
|
||||
search: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
@@ -111,18 +138,6 @@ const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
},
|
||||
});
|
||||
|
||||
/** 搜索操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置搜索 */
|
||||
function resetQuery() {
|
||||
queryParams.deviceName = '';
|
||||
queryParams.status = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取产品名称 */
|
||||
function getProductName(productId: number) {
|
||||
const product = products.value.find((p) => p.id === productId);
|
||||
@@ -139,7 +154,7 @@ watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
gridApi.query();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -151,49 +166,13 @@ onMounted(async () => {
|
||||
|
||||
// 如果设备ID存在,则查询列表
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
gridApi.query();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<!-- TODO @haohao:这个 search 能不能融合到 Grid 里; -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
v-model:value="queryParams.deviceName"
|
||||
placeholder="请输入设备名称"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@press-enter="handleQuery"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="请选择设备状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<IconifyIcon icon="ep:refresh-right" class="mr-5px" />
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 子设备列表 -->
|
||||
<Grid>
|
||||
<template #product="{ row }">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
@@ -68,7 +70,7 @@ const [DeviceImportFormModal, deviceImportFormModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
|
||||
const queryParams = ref<Partial<PageParam>>({
|
||||
deviceName: '',
|
||||
nickname: '',
|
||||
productId: undefined,
|
||||
@@ -118,7 +120,7 @@ async function handleExport() {
|
||||
...queryParams.value,
|
||||
pageNo: 1,
|
||||
pageSize: 999_999,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
} as PageParam);
|
||||
downloadFileFromBlobPart({ fileName: '物联网设备.xls', source: data });
|
||||
}
|
||||
|
||||
@@ -147,12 +149,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑设备 */
|
||||
function handleEdit(row: IotDeviceApi.DeviceRespVO) {
|
||||
function handleEdit(row: IotDeviceApi.Device) {
|
||||
deviceFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除设备 */
|
||||
async function handleDelete(row: IotDeviceApi.DeviceRespVO) {
|
||||
async function handleDelete(row: IotDeviceApi.Device) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.deviceName]),
|
||||
duration: 0,
|
||||
@@ -203,12 +205,12 @@ function handleImport() {
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: IotDeviceApi.DeviceRespVO[];
|
||||
records: IotDeviceApi.Device[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.Device>({
|
||||
gridOptions: {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
@@ -228,7 +230,7 @@ const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...queryParams.value,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
} as PageParam);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
@@ -46,9 +48,9 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<IotDeviceApi.DeviceRespVO[]>([]);
|
||||
const list = ref<IotDeviceApi.Device[]>([]);
|
||||
const total = ref(0);
|
||||
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
|
||||
const queryParams = ref<Partial<PageParam>>({
|
||||
pageNo: 1,
|
||||
pageSize: 12,
|
||||
});
|
||||
@@ -66,7 +68,7 @@ async function getList() {
|
||||
const data = await getDevicePage({
|
||||
...queryParams.value,
|
||||
...props.searchParams,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
} as PageParam);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
} finally {
|
||||
@@ -192,7 +194,7 @@ onMounted(() => {
|
||||
<Button
|
||||
size="small"
|
||||
class="action-btn action-btn-detail"
|
||||
@click="emit('detail', item.id)"
|
||||
@click="emit('detail', item.id!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
详情
|
||||
@@ -200,7 +202,7 @@ onMounted(() => {
|
||||
<Button
|
||||
size="small"
|
||||
class="action-btn action-btn-data"
|
||||
@click="emit('model', item.id)"
|
||||
@click="emit('model', item.id!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:database" class="mr-1" />
|
||||
数据
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
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';
|
||||
@@ -18,9 +19,10 @@ import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
||||
defineOptions({ name: 'IoTDeviceForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<IotDeviceApi.DeviceRespVO>();
|
||||
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();
|
||||
@@ -97,7 +125,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const data = {
|
||||
...basicValues,
|
||||
...advancedValues,
|
||||
} as IotDeviceApi.DeviceSaveReqVO;
|
||||
} as IotDeviceApi.Device;
|
||||
try {
|
||||
await (formData.value?.id ? updateDevice(data) : createDevice(data));
|
||||
// 关闭并提示
|
||||
@@ -115,11 +143,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotDeviceApi.DeviceRespVO>();
|
||||
const data = modalApi.getData<IotDeviceApi.Device>();
|
||||
if (!data || !data.id) {
|
||||
// 新增:确保 Collapse 折叠
|
||||
// TODO @haohao:是不是 activeKey 在上面的 112 到 115 就已经处理了哈;
|
||||
activeKey.value = [];
|
||||
return;
|
||||
}
|
||||
// 编辑模式:加载数据
|
||||
@@ -127,29 +152,29 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
formData.value = await getDevice(data.id);
|
||||
await formApi.setValues(formData.value);
|
||||
// 如果存在高级字段数据,自动展开 Collapse
|
||||
// TODO @haohao:默认不用展开哈;
|
||||
if (
|
||||
formData.value?.nickname ||
|
||||
formData.value?.picUrl ||
|
||||
formData.value?.groupIds?.length ||
|
||||
formData.value?.serialNumber ||
|
||||
formData.value?.locationType !== undefined
|
||||
) {
|
||||
activeKey.value = ['advanced'];
|
||||
// 等待 Collapse 展开后表单挂载
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听 Collapse 展开,自动设置高级表单的值 */
|
||||
watch(
|
||||
activeKey,
|
||||
async (newKeys) => {
|
||||
// 当用户手动展开 Collapse 且存在表单数据时,设置高级表单的值
|
||||
if (newKeys.includes('advanced') && formData.value) {
|
||||
// 等待表单挂载
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
/** 初始化产品列表 */
|
||||
onMounted(async () => {
|
||||
products.value = await getSimpleProductList();
|
||||
@@ -163,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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeviceRespVO, IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { OtaTask } from '#/api/iot/ota/task';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
@@ -57,7 +57,7 @@ const formRules = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const devices = ref<IotDeviceApi.DeviceRespVO[]>([]);
|
||||
const devices = ref<IotDeviceApi.Device[]>([]);
|
||||
|
||||
/** 设备选项 */
|
||||
const deviceOptions = computed(() => {
|
||||
|
||||
@@ -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, // 属性
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineConfig(async () => {
|
||||
application: {},
|
||||
vite: {
|
||||
server: {
|
||||
allowedHosts: true,
|
||||
proxy: {
|
||||
'/admin-api': {
|
||||
changeOrigin: true,
|
||||
|
||||
@@ -26,8 +26,17 @@ const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
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<InfraCodegenApi.DatabaseTable>,
|
||||
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 获取表单值
|
||||
|
||||
@@ -229,36 +229,32 @@ const [Modal, modalApi] = useVbenModal({
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验是否选择了商品
|
||||
if (spuList.value.length === 0) {
|
||||
ElMessage.warning('请选择活动商品');
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
try {
|
||||
// 获取折扣商品配置
|
||||
const products = cloneDeep(
|
||||
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [],
|
||||
) as MallDiscountActivityApi.DiscountProduct[];
|
||||
|
||||
// 转换金额为分
|
||||
products.forEach((item) => {
|
||||
item.discountPercent = convertToInteger(item.discountPercent);
|
||||
item.discountPrice = convertToInteger(item.discountPrice);
|
||||
});
|
||||
|
||||
const data = cloneDeep(
|
||||
await formApi.getValues(),
|
||||
) as MallDiscountActivityApi.DiscountActivity;
|
||||
data.products = products;
|
||||
|
||||
// 提交请求
|
||||
await (formData.value?.id
|
||||
? updateDiscountActivity(data)
|
||||
: createDiscountActivity(data));
|
||||
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
@@ -271,18 +267,15 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
const activityData = await getDiscountActivity(data.id);
|
||||
formData.value = activityData;
|
||||
|
||||
// 加载商品详情
|
||||
if (activityData.products && activityData.products.length > 0) {
|
||||
// 按 spuId 分组
|
||||
@@ -297,15 +290,13 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
spuProductsMap.get(spuId)!.push(product);
|
||||
}
|
||||
|
||||
// 加载每个 SPU 的详情
|
||||
for (const [spuId, products] of spuProductsMap) {
|
||||
const skuIdArr = products.map((p) => p.skuId);
|
||||
await getSpuDetails(spuId, skuIdArr, products, 'load');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
// 设置到 values
|
||||
await formApi.setValues(activityData);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
|
||||
@@ -117,7 +117,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
try {
|
||||
const result = await getReward(data.id);
|
||||
// valueFormat: 'x' 配置下,直接使用时间戳字符串
|
||||
result.startAndEndTime = [
|
||||
result.startTime ? String(result.startTime) : undefined,
|
||||
result.endTime ? String(result.endTime) : undefined,
|
||||
|
||||
@@ -25,8 +25,17 @@ const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
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,
|
||||
@@ -66,13 +75,8 @@ const [Grid] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.DatabaseTable>,
|
||||
gridEvents: {
|
||||
checkboxChange: ({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.DatabaseTable[];
|
||||
}) => {
|
||||
formData.tableNames = records.map((item) => item.name);
|
||||
},
|
||||
checkboxChange: handleCheckboxChange,
|
||||
checkboxAll: handleCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,6 +84,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 获取表单值
|
||||
|
||||
@@ -25,8 +25,17 @@ const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
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,
|
||||
@@ -66,13 +75,8 @@ const [Grid] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.DatabaseTable>,
|
||||
gridEvents: {
|
||||
checkboxChange: ({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.DatabaseTable[];
|
||||
}) => {
|
||||
formData.tableNames = records.map((item) => item.name);
|
||||
},
|
||||
checkboxChange: handleCheckboxChange,
|
||||
checkboxAll: handleCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,6 +84,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 获取表单值
|
||||
|
||||
@@ -108,8 +108,8 @@ function convertServerMenuToRouteRecordStringComponent(
|
||||
): RouteRecordStringComponent[] {
|
||||
const menus: RouteRecordStringComponent[] = [];
|
||||
menuList.forEach((menu) => {
|
||||
// 处理顶级链接菜单
|
||||
if (isHttpUrl(menu.path) && menu.parentId === 0) {
|
||||
// 处理外链菜单(顶级或子级)
|
||||
if (isHttpUrl(menu.path)) {
|
||||
// add by 芋艿:如果有 ?_iframe 参数,则作为内嵌页面处理
|
||||
// 如果有 _iframe 参数,则使用 iframeSrc;如果没有,则使用 link
|
||||
const url = new URL(menu.path);
|
||||
|
||||
Reference in New Issue
Block a user