feat(iot):【设备订单:50%】简化设备定位功能,支持 GeoLocation 自动更新,基于 calm-roaming-pillow.md

pull/856/head
YunaiV 2026-01-20 21:41:56 +08:00
parent 3821b32b03
commit 3620278360
7 changed files with 47 additions and 96 deletions

View File

@ -21,7 +21,6 @@ export interface DeviceVO {
mqttUsername: string // MQTT 用户名 mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码 mqttPassword: string // MQTT 密码
authType: string // 认证类型 authType: string // 认证类型
locationType: number // 定位类型
latitude?: number // 设备位置的纬度 latitude?: number // 设备位置的纬度
longitude?: number // 设备位置的经度 longitude?: number // 设备位置的经度
areaId: number // 地区编码 areaId: number // 地区编码
@ -49,7 +48,7 @@ export interface IotDevicePropertyRespVO {
updateTime: Date // 更新时间 updateTime: Date // 更新时间
} }
// TODO @芋艿:调整到 constants // TODO @AI:调整到 constants
// IoT 设备状态枚举 // IoT 设备状态枚举
export enum DeviceStateEnum { export enum DeviceStateEnum {
INACTIVE = 0, // 未激活 INACTIVE = 0, // 未激活

View File

@ -13,7 +13,6 @@ export interface ProductVO {
description: string // 产品描述 description: string // 产品描述
status: number // 产品状态 status: number // 产品状态
deviceType: number // 设备类型 deviceType: number // 设备类型
locationType: number // 设备类型
netType: number // 联网方式 netType: number // 联网方式
codecType: string // 数据格式(编解码器类型) codecType: string // 数据格式(编解码器类型)
deviceCount: number // 设备数量 deviceCount: number // 设备数量
@ -26,12 +25,6 @@ export enum DeviceTypeEnum {
GATEWAY_SUB = 1, // 网关子设备 GATEWAY_SUB = 1, // 网关子设备
GATEWAY = 2 // 网关设备 GATEWAY = 2 // 网关设备
} }
// IOT 产品定位类型枚举类 0: 手动定位, 1: IP 定位, 2: 定位模块定位
export enum LocationTypeEnum {
IP = 1, // IP 定位
MODULE = 2, // 设备定位
MANUAL = 3 // 手动定位
}
// IOT 数据格式(编解码器类型)枚举类 // IOT 数据格式(编解码器类型)枚举类
export enum CodecTypeEnum { export enum CodecTypeEnum {
ALINK = 'Alink' // 阿里云 Alink 协议 ALINK = 'Alink' // 阿里云 Alink 协议

View File

@ -1,5 +1,5 @@
<!-- 地图组件基于百度地图GL实现 --> <!-- 地图组件基于百度地图 GL 实现 -->
<!-- TODO @super还存在两个没解决的小bug,一个是修改手动定位时一次加载 不知道为何定位点在地图左上角 调了半天没解决 第二个是检索地址确定定位的功能参照百度的文档没也搞好 回头再解决一下 --> <!-- TODO @AI还存在两个没解决的小bug,一个是修改手动定位时一次加载 不知道为何定位点在地图左上角 调了半天没解决 第二个是检索地址确定定位的功能参照百度的文档没也搞好 回头再解决一下 -->
<template> <template>
<div v-if="props.isWrite"> <div v-if="props.isWrite">
<el-form ref="form" label-width="120px"> <el-form ref="form" label-width="120px">
@ -73,6 +73,7 @@ const props = defineProps({
watch( watch(
() => props.center, () => props.center,
// TODO @AI linter
(newVal, oldVal) => { (newVal, oldVal) => {
if (newVal) { if (newVal) {
// center mark // center mark
@ -100,7 +101,7 @@ const loadMap = () => {
initGeocoder() initGeocoder()
initAutoComplete() initAutoComplete()
// TODO @super // TODO @AI
if (props.clickMap) { if (props.clickMap) {
state.map.addEventListener('click', (e: any) => { state.map.addEventListener('click', (e: any) => {
console.log(e) console.log(e)
@ -194,9 +195,10 @@ const handleAddressSelect = (value: string) => {
* 添加标记点 * 添加标记点
* @param lnglat 经纬度数组 * @param lnglat 经纬度数组
*/ */
// TODO @super idea 绿
const setMarker = (lnglat: any) => { const setMarker = (lnglat: any) => {
if (!lnglat) return if (!lnglat) {
return
}
// //
if (state.mapMarker !== null) { if (state.mapMarker !== null) {
@ -217,14 +219,14 @@ const setMarker = (lnglat: any) => {
* 经纬度转化为地址添加标记点 * 经纬度转化为地址添加标记点
* @param lonLat 经度,纬度字符串 * @param lonLat 经度,纬度字符串
*/ */
// TODO @super idea 绿
const regeoCode = (lonLat: string) => { const regeoCode = (lonLat: string) => {
if (!lonLat) return if (!lonLat) {
return
// TODO @super idea 绿 }
const lnglat = lonLat.split(',') const lnglat = lonLat.split(',')
if (lnglat.length !== 2) return if (lnglat.length !== 2) {
return
}
state.longitude = lnglat[0] state.longitude = lnglat[0]
state.latitude = lnglat[1] state.latitude = lnglat[1]
@ -241,7 +243,6 @@ const regeoCode = (lonLat: string) => {
getAddress(lnglat) getAddress(lnglat)
} }
// TODO @superlnglat
/** /**
* 根据经纬度获取地址信息 * 根据经纬度获取地址信息
* *
@ -266,6 +267,7 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
// @AI unocss
.mapContainer { .mapContainer {
width: 100%; width: 100%;
height: 400px; height: 400px;

View File

@ -66,44 +66,31 @@
<el-form-item label="设备序列号" prop="serialNumber"> <el-form-item label="设备序列号" prop="serialNumber">
<el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" /> <el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" />
</el-form-item> </el-form-item>
<el-form-item label="定位类型" prop="locationType"> <el-form-item label="设备经度" prop="longitude" type="number">
<el-radio-group v-model="formData.locationType"> <el-input
<el-radio v-model="formData.longitude"
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)" placeholder="请输入设备经度"
:key="dict.value" @blur="updateLocationFromCoordinates"
:label="dict.value" />
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item> </el-form-item>
<!-- LocationTypeEnum.MANUAL手动定位 --> <el-form-item label="设备维度" prop="latitude" type="number">
<template v-if="LocationTypeEnum.MANUAL === formData.locationType"> <el-input
<el-form-item label="设备经度" prop="longitude" type="number"> v-model="formData.latitude"
<el-input placeholder="请输入设备维度"
v-model="formData.longitude" @blur="updateLocationFromCoordinates"
placeholder="请输入设备经度" />
@blur="updateLocationFromCoordinates" </el-form-item>
/> <!-- TODO @AI然后后面有个按钮标注地图可以手动按需调整 -->
</el-form-item> <div class="pl-0 h-[400px] w-full ml-[-18px]" v-if="showMap">
<el-form-item label="设备维度" prop="latitude" type="number"> <Map
<el-input :isWrite="true"
v-model="formData.latitude" :clickMap="true"
placeholder="请输入设备维度" :center="formData.location"
@blur="updateLocationFromCoordinates" @locate-change="handleLocationChange"
/> ref="mapRef"
</el-form-item> class="h-full w-full"
<div class="pl-0 h-[400px] w-full ml-[-18px]" v-if="showMap"> />
<Map </div>
:isWrite="true"
:clickMap="true"
:center="formData.location"
@locate-change="handleLocationChange"
ref="mapRef"
class="h-full w-full"
/>
</div>
</template>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</el-form> </el-form>
@ -116,9 +103,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device/device' import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceGroupApi } from '@/api/iot/device/group' import { DeviceGroupApi } from '@/api/iot/device/group'
import { DeviceTypeEnum, LocationTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product' import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
import { UploadImg } from '@/components/UploadFile' import { UploadImg } from '@/components/UploadFile'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import Map from '@/components/Map/index.vue' import Map from '@/components/Map/index.vue'
import { ref } from 'vue' import { ref } from 'vue'
@ -144,14 +130,14 @@ const formData = ref({
gatewayId: undefined, gatewayId: undefined,
deviceType: undefined as number | undefined, deviceType: undefined as number | undefined,
serialNumber: undefined, serialNumber: undefined,
locationType: undefined as number | undefined,
longitude: undefined, longitude: undefined,
latitude: undefined, latitude: undefined,
location: '', // : "," location: '', // : "," // TODO @AI formData
groupIds: [] as number[] groupIds: [] as number[]
}) })
/** 监听经纬度变化更新location */ /** 监听经纬度变化,更新 location */
// TODO @AI longitudelatitude
watch([() => formData.value.longitude, () => formData.value.latitude], ([newLong, newLat]) => { watch([() => formData.value.longitude, () => formData.value.latitude], ([newLong, newLat]) => {
if (newLong && newLat) { if (newLong && newLat) {
formData.value.location = `${newLong},${newLat}` formData.value.location = `${newLong},${newLat}`
@ -197,6 +183,7 @@ const formRules = reactive({
trigger: 'blur' trigger: 'blur'
} }
] ]
// TODO @AI longitudelatitude
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
const products = ref<ProductVO[]>([]) // const products = ref<ProductVO[]>([]) //
@ -248,16 +235,6 @@ const submitForm = async () => {
formLoading.value = true formLoading.value = true
try { try {
const data = formData.value as unknown as DeviceVO const data = formData.value as unknown as DeviceVO
//
if (data.locationType !== LocationTypeEnum.MANUAL) {
data.longitude = undefined
data.latitude = undefined
}
// TODO @address areaId
// 1. longitude + latitude + areaId + addressaddress
// 2. IP IotDeviceMessage buildStateUpdateOnline ip areaId https://lbsyun.baidu.com/faq/api?title=webapi/ip-api-base location ok
// 3. haohao
if (formType.value === 'create') { if (formType.value === 'create') {
await DeviceApi.createDevice(data) await DeviceApi.createDevice(data)
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))
@ -284,10 +261,8 @@ const resetForm = () => {
gatewayId: undefined, gatewayId: undefined,
deviceType: undefined, deviceType: undefined,
serialNumber: undefined, serialNumber: undefined,
locationType: undefined,
longitude: undefined, longitude: undefined,
latitude: undefined, latitude: undefined,
// TODO @location formData
location: '', location: '',
groupIds: [] groupIds: []
} }
@ -304,10 +279,10 @@ const handleProductChange = (productId: number) => {
} }
const product = products.value?.find((item) => item.id === productId) const product = products.value?.find((item) => item.id === productId)
formData.value.deviceType = product?.deviceType formData.value.deviceType = product?.deviceType
formData.value.locationType = product?.locationType
} }
/** 处理位置变化 */ /** 处理位置变化 */
// todo @AI linter TS7044: Parameter lnglat implicitly has an any type, but a better type may be inferred from usage.
const handleLocationChange = (lnglat) => { const handleLocationChange = (lnglat) => {
formData.value.longitude = lnglat[0] formData.value.longitude = lnglat[0]
formData.value.latitude = lnglat[1] formData.value.latitude = lnglat[1]
@ -319,6 +294,7 @@ const updateLocationFromCoordinates = () => {
if (formData.value.longitude && formData.value.latitude) { if (formData.value.longitude && formData.value.latitude) {
// location // location
formData.value.location = `${formData.value.longitude},${formData.value.latitude}` formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
// TODO @AI
mapRef.value.regeoCode(formData.value.location) mapRef.value.regeoCode(formData.value.location)
} }
} }

View File

@ -20,9 +20,6 @@
<el-descriptions-item label="设备类型"> <el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" /> <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="定位类型">
<dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="device.locationType" />
</el-descriptions-item>
<el-descriptions-item label="DeviceName"> <el-descriptions-item label="DeviceName">
{{ device.deviceName }} {{ device.deviceName }}
</el-descriptions-item> </el-descriptions-item>
@ -67,6 +64,7 @@
</div> </div>
</template> </template>
<div class="h-[400px] w-full"> <div class="h-[400px] w-full">
<!-- TODO @AI是不是可以通过 getLocationString() 简化判断 -->
<Map v-if="showMap" :center="getLocationString()" class="h-full w-full" /> <Map v-if="showMap" :center="getLocationString()" class="h-full w-full" />
<div <div
v-else v-else

View File

@ -62,17 +62,6 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="定位类型" prop="locationType">
<el-radio-group v-model="formData.locationType" :disabled="formType === 'update'">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="数据格式" prop="codecType"> <el-form-item label="数据格式" prop="codecType">
<el-radio-group v-model="formData.codecType" :disabled="formType === 'update'"> <el-radio-group v-model="formData.codecType" :disabled="formType === 'update'">
<el-radio <el-radio
@ -130,7 +119,6 @@ const formData = ref({
picUrl: undefined, picUrl: undefined,
description: undefined, description: undefined,
deviceType: undefined, deviceType: undefined,
locationType: undefined,
netType: undefined, netType: undefined,
codecType: CodecTypeEnum.ALINK codecType: CodecTypeEnum.ALINK
}) })
@ -139,7 +127,6 @@ const formRules = reactive({
name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }], name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
categoryId: [{ required: true, message: '产品分类不能为空', trigger: 'change' }], categoryId: [{ required: true, message: '产品分类不能为空', trigger: 'change' }],
deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }], deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
locationType: [{ required: true, message: '定位类型不能为空', trigger: 'change' }],
netType: [ netType: [
{ {
required: true, required: true,
@ -206,7 +193,6 @@ const resetForm = () => {
picUrl: undefined, picUrl: undefined,
description: undefined, description: undefined,
deviceType: undefined, deviceType: undefined,
locationType: undefined,
netType: undefined, netType: undefined,
codecType: CodecTypeEnum.ALINK codecType: CodecTypeEnum.ALINK
} }

View File

@ -6,9 +6,6 @@
<el-descriptions-item label="设备类型"> <el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" /> <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="定位类型">
<dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="product.locationType" />
</el-descriptions-item>
<el-descriptions-item label="创建时间"> <el-descriptions-item label="创建时间">
{{ formatDate(product.createTime) }} {{ formatDate(product.createTime) }}
</el-descriptions-item> </el-descriptions-item>