feat(iot): 新增设备地图功能并优化设备相关接口

- 新增设备地图页面,实现设备在地图上的展示和信息弹出
- 扩展设备接口,增加经纬度和图标字段
- 优化设备编辑页面,将产品名称改为所属产品下拉选择- 更新产品属性页面,调整属性类型为读写类型
- 添加高德地图相关依赖和类型定义
This commit is contained in:
Kven 2025-02-28 20:47:07 +08:00
parent 827bb69f22
commit ea1df9d55f
9 changed files with 161 additions and 19 deletions

View File

@ -31,6 +31,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@amap/amap-jsapi-types": "^0.0.15",
"@arco-design/web-vue": "^2.44.7", "@arco-design/web-vue": "^2.44.7",
"@vueuse/core": "^9.3.0", "@vueuse/core": "^9.3.0",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
@ -58,6 +60,7 @@
"@arco-plugins/vite-vue": "^1.4.5", "@arco-plugins/vite-vue": "^1.4.5",
"@commitlint/cli": "^17.1.2", "@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0", "@commitlint/config-conventional": "^17.1.0",
"@types/amap-js-api": "^1.4.16",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.186",
"@types/mockjs": "^1.0.7", "@types/mockjs": "^1.0.7",

View File

@ -10,6 +10,9 @@ export interface DeviceRecord {
status?: string; status?: string;
isOnline?: boolean; isOnline?: boolean;
pageable?: string; pageable?: string;
longitude?: number;
latitude?: number;
icon?: string;
} }
export interface DeviceCreateRecord { export interface DeviceCreateRecord {

2
src/env.d.ts vendored
View File

@ -9,3 +9,5 @@ declare module '*.vue' {
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string; readonly VITE_API_BASE_URL: string;
} }

View File

@ -98,6 +98,18 @@ const IOT: AppRouteRecordRaw = {
permissions: ['iot:tsl'], permissions: ['iot:tsl'],
}, },
}, },
{
path: 'deviceMap',
name: 'DeviceMap',
component: () => import('@/views/iot/deviceMap/index.vue'),
meta: {
locale: '设备地图',
title: '设备地图',
requiresAuth: true,
showInMenu: true,
permissions: ['iot:device'],
},
},
], ],
}; };

View File

@ -34,22 +34,22 @@
> >
<!-- 产品名称 --> <!-- 产品名称 -->
<a-form-item <a-form-item
field="productName" field="productId"
label="产品名称" label="所属产品"
:rules="[{ required: true, message: '产品名称不能为空' }]" :rules="[{ required: true, message: '产品名称不能为空' }]"
:validate-trigger="['change']" :validate-trigger="['change']"
> >
<a-select <a-select
v-model="formData.productName" v-model="formData.productId"
placeholder="请选择产品名称"
:loading="loading" :loading="loading"
:filter-option="false" :filter-option="false"
allow-search allow-search
placeholder="请选择所属产品"
@search="handleSearch" @search="handleSearch"
> >
<a-option v-for="item of options" :key="item.id" :value="item.name">{{ <a-option v-for="item of options" :key="item.id" :value="item.id">{{
item.name item.name
}}</a-option> }}</a-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<!-- 设备名称 --> <!-- 设备名称 -->
@ -238,17 +238,15 @@
const loading = ref(false); const loading = ref(false);
// //
const handleSearch = (value: any) => { const handleSearch = (value: string) => {
if (value) { if (value) {
loading.value = true; loading.value = true;
options.value = []; options.value = [];
window.setTimeout(async () => { window.setTimeout(async () => {
const res = await queryDeviceByName({ const res = await queryDeviceByName({
name: value, name: value,
page: 1,
size: 10,
}); });
options.value = res.data.records.map((item: any) => { options.value = res.data.map((item: any) => {
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@ -298,8 +296,6 @@
if (!valid) { if (!valid) {
// //
formData.value.extendParams = paramsData.value; formData.value.extendParams = paramsData.value;
const productId = await queryDeviceByName(formData.value.productName);
formData.value.productId = productId.data.records[0].id;
if (props.isCreate) { if (props.isCreate) {
// formData.value.username = formData.value.email; // formData.value.username = formData.value.email;

View File

@ -331,7 +331,7 @@
}; };
// //
const handleSearch = (value: any) => { const handleSearch = (value: string) => {
if (value) { if (value) {
loading.value = true; loading.value = true;
options.value = []; options.value = [];
@ -392,6 +392,7 @@
}; };
onMounted(() => { onMounted(() => {
search(); search();
handleSearch('');
}); });
watch(() => columns.value, deepClone, { deep: true, immediate: true }); watch(() => columns.value, deepClone, { deep: true, immediate: true });
</script> </script>

View File

@ -0,0 +1,117 @@
<template>
<a-layout>
<a-layout-sider :resize-directions="['right']">
<a-row>
</a-row>
</a-layout-sider>
<a-layout-content>
<div id="container"></div>
<!-- 标记点信息弹出框 -->
<a-modal v-model:visible="infoVisible" title="设备信息" :footer="null">
<p>ID: {{ selectedDevice.id }}</p>
<p>名称: {{ selectedDevice.name }}</p>
<p>状态: {{ selectedDevice.state }}</p>
<p>在线状态: {{ selectedDevice.online ? '是' : '否' }}</p>
</a-modal>
</a-layout-content>
</a-layout>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader';
import { DeviceRecord, queryDeviceList } from '@/api/device';
import "@amap/amap-jsapi-types";
let map: any = null;
const selectedDevice = ref<DeviceRecord | null>(null);
const infoVisible = ref(false);
const deviceList = ref<DeviceRecord[]>([]);
const markerContent = `<div class="custom-content-marker">
<img src="//a.amap.com/jsapi_demos/static/demo-center/icons/dir-via-marker.png">
<div class="close-btn">X</div>
</div>`;
onMounted(() => {
AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
version: '2.0', // JSAPI 1.4.15
plugins: [], // 使'AMap.Scale'
})
.then((AMap) => {
map = new AMap.Map('container', {
// id
viewMode: '3D', // 3D
zoom: 11, //
center: [116.397428, 39.90923], //
});
//
const position = new AMap.LngLat(116.397428, 39.90923);
const marker = new AMap.Marker({
position,
content: markerContent,
offset: new AMap.Pixel(-13, -30),
});
map.add(marker);
})
.catch((e) => {
console.log(e);
});
//
queryDeviceList({
current: 1,
size: 10,
})
.then((response) => {
deviceList.value = response.data.records;
})
.catch((error) => {
console.error('获取设备列表失败', error);
});
});
onUnmounted(() => {
map?.destroy();
});
</script>
<style scoped>
#container {
width: 100%;
height: 800px;
}
.custom-content-marker {
position: relative;
width: 25px;
height: 34px;
}
.custom-content-marker img {
width: 100%;
height: 100%;
}
.custom-content-marker .close-btn {
position: absolute;
top: -6px;
right: -8px;
width: 15px;
height: 15px;
font-size: 12px;
background: #ccc;
border-radius: 50%;
color: #fff;
text-align: center;
line-height: 15px;
box-shadow: -1px 1px 1px rgba(10, 10, 10, .2);
}
.custom-content-marker .close-btn:hover{
background: #666;
}
</style>

View File

@ -37,6 +37,9 @@
删除 删除
</a-button> </a-button>
</template> </template>
<template #ioType="{ record }">
<div>{{ record.ioType === 'READ_WRITE' ? '读写' : '只读' }}</div>
</template>
</a-table> </a-table>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" title="服务"> <a-tab-pane key="2" title="服务">
@ -105,11 +108,11 @@
:model="propertyAddData" :model="propertyAddData"
:style="{ width: '800px', height: '420px' }" :style="{ width: '800px', height: '420px' }"
> >
<!-- 设备名称 --> <!-- 属性名称 -->
<a-form-item <a-form-item
field="name" field="name"
label="属性名称" label="属性名称"
:rules="[{ required: true, message: '设备名称不能为空' }]" :rules="[{ required: true, message: '属性名称不能为空' }]"
:validate-trigger="['change']" :validate-trigger="['change']"
> >
<a-input <a-input
@ -146,8 +149,8 @@
<!-- 读写类型 --> <!-- 读写类型 -->
<a-form-item field="ioType" label="读写类型"> <a-form-item field="ioType" label="读写类型">
<a-radio-group v-model="propertyAddData.ioType"> <a-radio-group v-model="propertyAddData.ioType">
<a-radio value="1">读写</a-radio> <a-radio value="READ_WRITE">读写</a-radio>
<a-radio value="2">只读</a-radio> <a-radio value="READ_ONLY">只读</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<!-- 备注 --> <!-- 备注 -->
@ -439,6 +442,11 @@
dataIndex: 'identifier', dataIndex: 'identifier',
slotName: 'identifier', slotName: 'identifier',
}, },
{
title: '类型',
dataIndex: 'ioType',
slotName: 'ioType',
},
{ {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',

View File

@ -136,7 +136,7 @@
</template> </template>
</a-modal> </a-modal>
<!-- 部门树模态框--> <!-- 用户模态框-->
<a-modal width="900px" :visible="deptVisible" @cancel="deptTreeCancel"> <a-modal width="900px" :visible="deptVisible" @cancel="deptTreeCancel">
<template #title>发送用户</template> <template #title>发送用户</template>
<div style="display: flex"> <div style="display: flex">