From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/iot/device/device/DeviceForm.vue |  325 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 325 insertions(+), 0 deletions(-)

diff --git a/src/views/iot/device/device/DeviceForm.vue b/src/views/iot/device/device/DeviceForm.vue
new file mode 100644
index 0000000..dfed0c6
--- /dev/null
+++ b/src/views/iot/device/device/DeviceForm.vue
@@ -0,0 +1,325 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="浜у搧" prop="productId">
+        <el-select
+          v-model="formData.productId"
+          placeholder="璇烽�夋嫨浜у搧"
+          :disabled="formType === 'update'"
+          clearable
+          @change="handleProductChange"
+        >
+          <el-option
+            v-for="product in products"
+            :key="product.id"
+            :label="product.name"
+            :value="product.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="DeviceName" prop="deviceName">
+        <el-input
+          v-model="formData.deviceName"
+          placeholder="璇疯緭鍏� DeviceName"
+          :disabled="formType === 'update'"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB"
+        label="缃戝叧璁惧"
+        prop="gatewayId"
+      >
+        <el-select v-model="formData.gatewayId" placeholder="瀛愯澶囧彲閫夋嫨鐖惰澶�" clearable>
+          <el-option
+            v-for="gateway in gatewayDevices"
+            :key="gateway.id"
+            :label="gateway.nickname || gateway.deviceName"
+            :value="gateway.id"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-collapse>
+        <el-collapse-item title="鏇村閰嶇疆">
+          <el-form-item label="澶囨敞鍚嶇О" prop="nickname">
+            <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ュ娉ㄥ悕绉�" />
+          </el-form-item>
+          <el-form-item label="璁惧鍥剧墖" prop="picUrl">
+            <UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" />
+          </el-form-item>
+          <el-form-item label="璁惧鍒嗙粍" prop="groupIds">
+            <el-select v-model="formData.groupIds" placeholder="璇烽�夋嫨璁惧鍒嗙粍" multiple clearable>
+              <el-option
+                v-for="group in deviceGroups"
+                :key="group.id"
+                :label="group.name"
+                :value="group.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="璁惧搴忓垪鍙�" prop="serialNumber">
+            <el-input v-model="formData.serialNumber" placeholder="璇疯緭鍏ヨ澶囧簭鍒楀彿" />
+          </el-form-item>
+          <el-form-item label="瀹氫綅绫诲瀷" prop="locationType">
+            <el-radio-group v-model="formData.locationType">
+              <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>
+          <!-- LocationTypeEnum.MANUAL锛氭墜鍔ㄥ畾浣� -->
+          <template v-if="LocationTypeEnum.MANUAL === formData.locationType">
+            <el-form-item label="璁惧缁忓害" prop="longitude" type="number">
+              <el-input
+                v-model="formData.longitude"
+                placeholder="璇疯緭鍏ヨ澶囩粡搴�"
+                @blur="updateLocationFromCoordinates"
+              />
+            </el-form-item>
+            <el-form-item label="璁惧缁村害" prop="latitude" type="number">
+              <el-input
+                v-model="formData.latitude"
+                placeholder="璇疯緭鍏ヨ澶囩淮搴�"
+                @blur="updateLocationFromCoordinates"
+              />
+            </el-form-item>
+            <div class="pl-0 h-[400px] w-full ml-[-18px]" v-if="showMap">
+              <Map
+                :isWrite="true"
+                :clickMap="true"
+                :center="formData.location"
+                @locate-change="handleLocationChange"
+                ref="mapRef"
+                class="h-full w-full"
+              />
+            </div>
+          </template>
+        </el-collapse-item>
+      </el-collapse>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+      <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { DeviceGroupApi } from '@/api/iot/device/group'
+import { DeviceTypeEnum, LocationTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
+import { UploadImg } from '@/components/UploadFile'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import Map from '@/components/Map/index.vue'
+import { ref } from 'vue'
+
+/** IoT 璁惧琛ㄥ崟 */
+defineOptions({ name: 'IoTDeviceForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅绐�
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const showMap = ref(false) // 鏄惁鏄剧ず鍦板浘缁勪欢
+const mapRef = ref(null)
+
+const formData = ref({
+  id: undefined,
+  productId: undefined,
+  deviceName: undefined,
+  nickname: undefined,
+  picUrl: undefined,
+  gatewayId: undefined,
+  deviceType: undefined as number | undefined,
+  serialNumber: undefined,
+  locationType: undefined as number | undefined,
+  longitude: undefined,
+  latitude: undefined,
+  location: '', // 鏍煎紡: "缁忓害,绾害"
+  groupIds: [] as number[]
+})
+
+/** 鐩戝惉缁忕含搴﹀彉鍖栵紝鏇存柊location */
+watch([() => formData.value.longitude, () => formData.value.latitude], ([newLong, newLat]) => {
+  if (newLong && newLat) {
+    formData.value.location = `${newLong},${newLat}`
+    // 鏈変簡缁忕含搴︽暟鎹悗鏄剧ず鍦板浘
+    showMap.value = true
+  }
+})
+
+const formRules = reactive({
+  productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+  deviceName: [
+    { required: true, message: 'DeviceName 涓嶈兘涓虹┖', trigger: 'blur' },
+    {
+      pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
+      message:
+        '鏀寔鑻辨枃瀛楁瘝銆佹暟瀛椼�佷笅鍒掔嚎锛坃锛夈�佷腑鍒掔嚎锛�-锛夈�佺偣鍙凤紙.锛夈�佸崐瑙掑啋鍙凤紙:锛夊拰鐗规畩瀛楃@锛岄暱搴﹂檺鍒朵负 4~32 涓瓧绗�',
+      trigger: 'blur'
+    }
+  ],
+  nickname: [
+    {
+      validator: (_rule, value: any, callback) => {
+        if (value === undefined || value === null) {
+          callback()
+          return
+        }
+        const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
+        if (length < 4 || length > 64) {
+          callback(new Error('澶囨敞鍚嶇О闀垮害闄愬埗涓� 4~64 涓瓧绗︼紝涓枃鍙婃棩鏂囩畻 2 涓瓧绗�'))
+        } else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
+          callback(new Error('澶囨敞鍚嶇О鍙兘鍖呭惈涓枃銆佽嫳鏂囧瓧姣嶃�佹棩鏂囥�佹暟瀛楀拰涓嬪垝绾匡紙_锛�'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  serialNumber: [
+    {
+      pattern: /^[a-zA-Z0-9-_]+$/,
+      message: '搴忓垪鍙峰彧鑳藉寘鍚瓧姣嶃�佹暟瀛椼�佷腑鍒掔嚎鍜屼笅鍒掔嚎',
+      trigger: 'blur'
+    }
+  ]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const products = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const gatewayDevices = ref<DeviceVO[]>([]) // 缃戝叧璁惧鍒楄〃
+const deviceGroups = ref<any[]>([])
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  // 榛樿涓嶆樉绀哄湴鍥撅紝绛夊緟鏁版嵁鍔犺浇
+  showMap.value = false
+
+  // 淇敼鏃讹紝璁剧疆鏁版嵁
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DeviceApi.getDevice(id)
+
+      // 濡傛灉鏈夌粡绾害锛岃缃� location 瀛楁鐢ㄤ簬鍦板浘鏄剧ず
+      if (formData.value.longitude && formData.value.latitude) {
+        formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 濡傛灉鏈夌粡绾俊鎭紝鍒欐暟鎹姞杞藉畬鎴愬悗锛屾樉绀哄湴鍥�
+  showMap.value = true
+
+  // 鍔犺浇缃戝叧璁惧鍒楄〃
+  gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY)
+  // 鍔犺浇浜у搧鍒楄〃
+  products.value = await ProductApi.getSimpleProductList()
+  // 鍔犺浇璁惧鍒嗙粍鍒楄〃
+  deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+  // 鏍¢獙琛ㄥ崟
+  await formRef.value.validate()
+  // 鎻愪氦璇锋眰
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DeviceVO
+    // 濡傛灉闈炴墜鍔ㄥ畾浣嶏紝涓嶈繘琛屾彁浜よ瀛楁
+    if (data.locationType !== LocationTypeEnum.MANUAL) {
+      data.longitude = undefined
+      data.latitude = undefined
+    }
+    // TODO @瀹楄秴锛氥�愯澶囧畾浣嶃�慳ddress 鍜� areaId 涔熻澶勭悊锛�
+    // 1. 鎵嬪姩瀹氫綅鏃讹細longitude + latitude + areaId + address锛氳绋嶅井娉ㄦ剰锛宎ddress 鍙兘瑕佸幓鎺夌渷甯傚尯閮ㄥ垎锛燂紒
+    // 2. IP 瀹氫綅鏃讹細IotDeviceMessage 鐨� buildStateUpdateOnline 鏃讹紝澧炲姞 ip 瀛楁銆傝繖鏍凤紝瑙f瀽鍒� areaId锛涘彟澶栫湅鐪嬭兘涓嶈兘閫氳繃 https://lbsyun.baidu.com/faq/api?title=webapi/ip-api-base锛堝彧鑾峰彇 location 灏� ok 鍟︼級
+    // 3. 璁惧瀹氫綅鏃讹細闂棶 haohao锛屼竴鑸�庝箞鍋氥��
+
+    if (formType.value === 'create') {
+      await DeviceApi.createDevice(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeviceApi.updateDevice(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    productId: undefined,
+    deviceName: undefined,
+    nickname: undefined,
+    picUrl: undefined,
+    gatewayId: undefined,
+    deviceType: undefined,
+    serialNumber: undefined,
+    locationType: undefined,
+    longitude: undefined,
+    latitude: undefined,
+    // TODO @瀹楄秴锛氥�愯澶囧畾浣嶃�憀ocation 鏄笉鏄嬁鍑烘潵锛屼笉鏀惧湪 formData 閲�
+    location: '',
+    groupIds: []
+  }
+  formRef.value?.resetFields()
+  // 閲嶇疆琛ㄥ崟鏃讹紝闅愯棌鍦板浘
+  showMap.value = false
+}
+
+/** 浜у搧閫夋嫨鍙樺寲 */
+const handleProductChange = (productId: number) => {
+  if (!productId) {
+    formData.value.deviceType = undefined
+    return
+  }
+  const product = products.value?.find((item) => item.id === productId)
+  formData.value.deviceType = product?.deviceType
+  formData.value.locationType = product?.locationType
+}
+
+/** 澶勭悊浣嶇疆鍙樺寲 */
+const handleLocationChange = (lnglat) => {
+  formData.value.longitude = lnglat[0]
+  formData.value.latitude = lnglat[1]
+}
+
+/** 鏍规嵁缁忕含搴︽洿鏂板湴鍥句綅缃� */
+const updateLocationFromCoordinates = () => {
+  // 楠岃瘉缁忕含搴︽槸鍚︽湁鏁�
+  if (formData.value.longitude && formData.value.latitude) {
+    // 鏇存柊 location 瀛楁锛屽湴鍥剧粍浠朵細鏍规嵁姝ゅ瓧娈垫洿鏂�
+    formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+    mapRef.value.regeoCode(formData.value.location)
+  }
+}
+</script>

--
Gitblit v1.8.0