Преглед изворни кода

feat:【IoT 物联网】重构设备模拟器,优化属性和事件上报逻辑,增强用户交互体验

YunaiV пре 9 месеци
родитељ
комит
a37c168efe

+ 0 - 8
src/api/iot/thingmodel/index.ts

@@ -18,14 +18,6 @@ export interface ThingModelData {
   service?: ThingModelService // 服务
 }
 
-/**
- * IoT 模拟设备
- */
-// TODO @super:和 ThingModelSimulatorData 会不会好点
-export interface SimulatorData extends ThingModelData {
-  simulateValue?: string | number // 用于存储模拟值 TODO @super:字段使用 value 会不会好点
-}
-
 /**
  * ThingModelProperty 类型
  */

+ 281 - 190
src/views/iot/device/device/detail/DeviceDetailsSimulator.vue

@@ -6,75 +6,109 @@
       <el-col :span="12">
         <el-tabs v-model="activeTab" type="border-card">
           <!-- 上行指令调试 -->
-          <el-tab-pane label="上行指令调试" name="up">
-            <el-tabs v-if="activeTab === 'up'" v-model="subTab">
+          <el-tab-pane label="上行指令调试" name="upstream">
+            <el-tabs v-if="activeTab === 'upstream'" v-model="upstreamTab">
               <!-- 属性上报 -->
-              <el-tab-pane label="属性上报" name="property">
+              <el-tab-pane label="属性上报" :name="IotDeviceMessageMethodEnum.PROPERTY_POST.method">
                 <ContentWrap>
-                  <el-table
-                    v-loading="loading"
-                    :data="list"
-                    :show-overflow-tooltip="true"
-                    :stripe="true"
-                  >
-                    <!-- TODO @super:每个 colum 搞下宽度,避免 table 每一列最后有个 . -->
-                    <!-- TODO @super:可以左侧 fixed -->
-                    <el-table-column align="center" label="功能名称" prop="name" />
-                    <el-table-column align="center" label="标识符" prop="identifier" />
-                    <el-table-column align="center" label="数据类型" prop="identifier">
-                      <!-- TODO @super:不用翻译,可以减少宽度的占用 -->
+                  <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
+                    />
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="center" label="数据类型" width="100">
                       <template #default="{ row }">
-                        {{ getDataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
+                        {{ row.property?.dataType ?? '-' }}
                       </template>
                     </el-table-column>
-                    <el-table-column align="left" label="数据定义" prop="identifier">
+                    <el-table-column align="left" label="数据定义" min-width="200">
                       <template #default="{ row }">
                         <DataDefinition :data="row" />
                       </template>
                     </el-table-column>
-                    <!-- TODO @super:可以右侧 fixed -->
-                    <el-table-column align="center" label="值" width="80">
+                    <el-table-column fixed="right" align="center" label="值" width="150">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          placeholder="输入值"
+                          size="small"
+                        />
                       </template>
                     </el-table-column>
                   </el-table>
-                  <!-- TODO @super:发送按钮,可以放在右侧哈。因为我们的 simulateValue 就在最右侧 -->
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handlePropertyPost"> 发送</el-button>
+                  <div class="flex justify-between items-center mt-4">
+                    <span class="text-sm text-gray-600">
+                      设置属性值后,点击「发送属性上报」按钮
+                    </span>
+                    <el-button type="primary" @click="handlePropertyPost">发送属性上报</el-button>
                   </div>
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 事件上报 -->
-              <!-- TODO @super:待实现 -->
-              <el-tab-pane label="事件上报" name="event">
+              <el-tab-pane label="事件上报" :name="IotDeviceMessageMethodEnum.EVENT_POST.method">
                 <ContentWrap>
-                  <!-- TODO @super:因为事件是每个 event 去模拟,而不是类似属性的批量上传。所以,可以每一列后面有个"模拟"按钮。另外,"值"使用 textarea,高度 3 -->
-                  <!-- <el-table v-loading="loading" :data="eventList" :stripe="true">
-                    <el-table-column label="功能名称" align="center" prop="name" />
-                    <el-table-column label="标识符" align="center" prop="identifier" />
-                    <el-table-column label="数据类型" align="center" prop="dataType" />
+                  <el-table :data="eventList" :show-overflow-tooltip="true" :stripe="true">
                     <el-table-column
-                      label="数据定义"
+                      fixed="left"
                       align="center"
-                      prop="specs"
-                      :show-overflow-tooltip="true"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
                     />
-                    <el-table-column label="值" align="center" width="80">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="center" label="数据类型" width="100">
+                      <template #default="{ row }">
+                        {{ row.event?.dataType ?? '-' }}
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="left" label="数据定义" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="center" label="值" width="200">
+                      <template #default="scope">
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          type="textarea"
+                          :rows="3"
+                          placeholder="输入事件参数(JSON格式)"
+                          size="small"
+                        />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="操作" width="100">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-button type="primary" size="small" @click="handleEventPost(scope.row)">
+                          上报事件
+                        </el-button>
                       </template>
                     </el-table-column>
                   </el-table>
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handleEventReport">发送</el-button>
-                  </div> -->
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 状态变更 -->
-              <el-tab-pane label="状态变更" name="status">
+              <el-tab-pane label="状态变更" :name="IotDeviceMessageMethodEnum.STATE_UPDATE.method">
                 <ContentWrap>
                   <div class="flex gap-4">
                     <el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
@@ -90,39 +124,106 @@
           </el-tab-pane>
 
           <!-- 下行指令调试 -->
-          <!-- TODO @super:待实现 -->
-          <el-tab-pane label="下行指令调试" name="down">
-            <el-tabs v-if="activeTab === 'down'" v-model="subTab">
+          <el-tab-pane label="下行指令调试" name="downstream">
+            <el-tabs v-if="activeTab === 'downstream'" v-model="downstreamTab">
               <!-- 属性调试 -->
-              <el-tab-pane label="属性调试" name="propertyDebug">
+              <el-tab-pane label="属性设置" :name="IotDeviceMessageMethodEnum.PROPERTY_SET.method">
                 <ContentWrap>
-                  <!-- <el-table v-loading="loading" :data="propertyList" :stripe="true">
-                    <el-table-column label="功能名称" align="center" prop="name" />
-                    <el-table-column label="标识符" align="center" prop="identifier" />
-                    <el-table-column label="数据类型" align="center" prop="dataType" />
+                  <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
+                    />
                     <el-table-column
-                      label="数据定义"
+                      fixed="left"
                       align="center"
-                      prop="specs"
-                      :show-overflow-tooltip="true"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
                     />
-                    <el-table-column label="值" align="center" width="80">
+                    <el-table-column align="center" label="数据类型" width="100">
+                      <template #default="{ row }">
+                        {{ row.property?.dataType ?? '-' }}
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="left" label="数据定义" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="值" width="150">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          placeholder="输入值"
+                          size="small"
+                        />
                       </template>
                     </el-table-column>
                   </el-table>
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handlePropertyGet">获取</el-button>
-                  </div> -->
+                  <div class="flex justify-between items-center mt-4">
+                    <span class="text-sm text-gray-600">
+                      设置属性值后,点击「发送属性设置」按钮
+                    </span>
+                    <el-button type="primary" @click="handlePropertySet">发送属性设置</el-button>
+                  </div>
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 服务调用 -->
-              <!-- TODO @super:待实现 -->
-              <el-tab-pane label="服务调用" name="service">
+              <el-tab-pane
+                label="设备服务调用"
+                :name="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
+              >
                 <ContentWrap>
-                  <!-- 服务调用相关内容 -->
+                  <el-table :data="serviceList" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="服务名称"
+                      prop="name"
+                      width="120"
+                    />
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="left" label="输入参数" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="center" label="参数值" width="200">
+                      <template #default="scope">
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          type="textarea"
+                          :rows="3"
+                          placeholder="输入服务参数(JSON格式)"
+                          size="small"
+                        />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="操作" width="100">
+                      <template #default="scope">
+                        <el-button
+                          type="primary"
+                          size="small"
+                          @click="handleServiceInvoke(scope.row)"
+                        >
+                          服务调用
+                        </el-button>
+                      </template>
+                    </el-table-column>
+                  </el-table>
                 </ContentWrap>
               </el-tab-pane>
             </el-tabs>
@@ -142,119 +243,60 @@
 
 <script lang="ts" setup>
 import { ProductVO } from '@/api/iot/product/product'
-import { SimulatorData, ThingModelApi } from '@/api/iot/thingmodel'
+import { ThingModelData } from '@/api/iot/thingmodel'
 import { DeviceApi, DeviceStateEnum, DeviceVO } from '@/api/iot/device/device'
 import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
 import { DataDefinition } from '@/views/iot/thingmodel/components'
-import { getDataTypeOptionsLabel, IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
+import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 
 const props = defineProps<{
   product: ProductVO
   device: DeviceVO
+  thingModelList: ThingModelData[]
 }>()
 
 const message = useMessage() // 消息弹窗
-const activeTab = ref('up') // TODO @super:upstream 上行、downstream 下行
-const subTab = ref('property') // TODO @super:upstreamTab
+const activeTab = ref('upstream') // 上行upstream、下行downstream
+const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) // 上行子标签
+const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) // 下行子标签
 const deviceMessageRef = ref() // 设备消息组件引用
-const deviceMessageRefresnhDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理
+const deviceMessageRefreshDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理
 
-const loading = ref(false)
-const queryParams = reactive({
-  type: undefined as number | undefined, // TODO @super:type 默认给个第一个 tab 对应的,避免下面 watch 爆红
-  productId: -1
-})
-const list = ref<SimulatorData[]>([]) // 物模型列表的数据 TODO @super:thingModelList
+// 表单数据:存储用户输入的模拟值
+const formData = ref<Record<string, string>>({})
 
-/** 查询物模型列表 */
-// TODO @super:getThingModelList 更精准
-// TODO @haohao:目前 index.vue 已经有了 thingModels,可以考虑服用下;
-const getList = async () => {
-  loading.value = true
-  try {
-    queryParams.productId = props.product?.id || -1
-    const data = await ThingModelApi.getThingModelList(queryParams)
-    // 转换数据,添加 simulateValue 字段
-    // TODO @super:貌似下面的 simulateValue 不设置也可以?
-    list.value = data.map((item) => ({
-      ...item,
-      simulateValue: ''
-    }))
-  } finally {
-    loading.value = false
-  }
+// 根据类型过滤物模型数据
+const getFilteredThingModelList = (type: number) => {
+  return props.thingModelList.filter((item) => item.type === type)
 }
+const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
+const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
+const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
 
-// // 功能列表数据结构定义
-// interface TableItem {
-//   name: string
-//   identifier: string
-//   value: string | number
-// }
-
-// // 添加计算属性来过滤物模型数据
-// const propertyList = computed(() => {
-//   return list.value
-//     .filter((item) => item.type === 'property')
-//     .map((item) => ({
-//       name: item.name,
-//       identifier: item.identifier,
-//       value: ''
-//     }))
-// })
-
-// const eventList = computed(() => {
-//   return list.value
-//     .filter((item) => item.type === 'event')
-//     .map((item) => ({
-//       name: item.name,
-//       identifier: item.identifier,
-//       value: ''
-//     }))
-// })
-
-/** 监听标签页变化 */
-// todo:后续改成查询字典
-watch(
-  [activeTab, subTab],
-  ([newActiveTab, newSubTab]) => {
-    // 根据标签页设置查询类型
-    if (newActiveTab === 'up') {
-      switch (newSubTab) {
-        case 'property':
-          queryParams.type = 1
-          break
-        case 'event':
-          queryParams.type = 3
-          break
-      }
-    } else if (newActiveTab === 'down') {
-      switch (newSubTab) {
-        case 'propertyDebug':
-          queryParams.type = 1
-          break
-        case 'service':
-          queryParams.type = 2
-          break
-      }
-    }
-    getList() // 切换标签时重新获取数据
-  },
-  { immediate: true }
-)
+/** 获取表单值的辅助函数 */
+const getFormValue = (identifier: string | number | undefined) => {
+  if (!identifier) return ''
+  return formData.value[String(identifier)] || ''
+}
+/** 设置表单值的辅助函数 */
+const setFormValue = (identifier: string | number | undefined, value: string) => {
+  if (!identifier) return
+  formData.value[String(identifier)] = value
+}
 
-/** 处理属性上报 */
+/** 模拟属性上报 */
 const handlePropertyPost = async () => {
-  // TODO @super:数据类型效验
   const data: Record<string, any> = {}
-  list.value.forEach((item) => {
-    // 只有当 simulateValue 有值时才添加到 content 中
-    // TODO @super:直接 if (item.simulateValue) 就可以哈,js 这块还是比较灵活的
-    if (item.simulateValue !== undefined && item.simulateValue !== '' && item.identifier) {
-      // TODO @super:这里有个红色的 idea 告警,觉得去除下
-      data[item.identifier] = item.simulateValue
+  propertyList.value.forEach((item) => {
+    const value = getFormValue(item.identifier)
+    if (value && item.identifier) {
+      data[String(item.identifier)] = value
     }
   })
+  if (Object.keys(data).length === 0) {
+    message.warning('请至少设置一个属性值')
+    return
+  }
 
   try {
     await DeviceApi.sendDeviceMessage({
@@ -263,42 +305,45 @@ const handlePropertyPost = async () => {
       params: data
     })
     message.success('属性上报成功')
-    deviceMessageRef.value.refresh(deviceMessageRefresnhDelay)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
   } catch (error) {
     message.error('属性上报失败')
   }
 }
 
-// // 处理事件上报
-// const handleEventReport = async () => {
-//   const contentObj: Record<string, any> = {}
-//   list.value
-//     .filter(item => item.type === 'event')
-//     .forEach((item) => {
-//       if (item.simulateValue !== undefined && item.simulateValue !== '') {
-//         contentObj[item.identifier] = item.simulateValue
-//       }
-//     })
-
-//   const reportData: ReportData = {
-//     productKey: props.product.productKey,
-//     deviceKey: props.device.deviceKey,
-//     type: 'event',
-//     subType: list.value.find(item => item.type === 'event')?.identifier || '',
-//     reportTime: new Date().toISOString(),
-//     content: JSON.stringify(contentObj)  // 转换为 JSON 字符串
-//   }
+/** 模拟事件上报 */
+const handleEventPost = async (eventItem: ThingModelData) => {
+  const value = getFormValue(eventItem.identifier)
+  if (!value) {
+    message.warning('请输入事件参数')
+    return
+  }
+  let eventParams: any
+  try {
+    eventParams = JSON.parse(value)
+  } catch {
+    message.error('事件参数格式不正确,请输入有效的JSON格式')
+    return
+  }
 
-//   try {
-//     // TODO: 调用API发送数据
-//     console.log('上报数据:', reportData)
-//     message.success('事件上报成功')
-//   } catch (error) {
-//     message.error('事件上报失败')
-//   }
-// }
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.EVENT_POST.method,
+      params: {
+        identifier: String(eventItem.identifier),
+        value: eventParams,
+        time: Date.now()
+      }
+    })
+    message.success(`事件【${String(eventItem.name)}】上报成功`)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error(`事件【${String(eventItem.name)}】上报失败`)
+  }
+}
 
-/** 处理设备状态 */
+/** 模拟设备状态 */
 const handleDeviceState = async (state: number) => {
   try {
     await DeviceApi.sendDeviceMessage({
@@ -309,21 +354,67 @@ const handleDeviceState = async (state: number) => {
       }
     })
     message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`)
-    deviceMessageRef.value.refresh(deviceMessageRefresnhDelay)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
   } catch (error) {
     message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`)
   }
 }
 
-// 处理属性获取
-const handlePropertyGet = async () => {
-  // TODO: 实现属性获取逻辑
-  message.success('属性获取成功')
+/** 模拟属性设置 */
+const handlePropertySet = async () => {
+  const data: Record<string, any> = {}
+  propertyList.value.forEach((item) => {
+    const value = getFormValue(item.identifier)
+    if (value && item.identifier) {
+      data[String(item.identifier)] = value
+    }
+  })
+  if (Object.keys(data).length === 0) {
+    message.warning('请至少设置一个属性值')
+    return
+  }
+
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
+      params: data
+    })
+    message.success('属性设置成功')
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error('属性设置失败')
+  }
 }
 
-// 初始化
-onMounted(() => {
-  getList()
-})
-// TODO @芋艿:后续再详细 review 下;
+/** 模拟服务调用 */
+const handleServiceInvoke = async (serviceItem: ThingModelData) => {
+  const value = getFormValue(serviceItem.identifier)
+  if (!value) {
+    message.warning('请输入服务参数')
+    return
+  }
+  let serviceParams: any
+  try {
+    serviceParams = JSON.parse(value)
+  } catch {
+    message.error('服务参数格式不正确,请输入有效的JSON格式')
+    return
+  }
+
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
+      params: {
+        identifier: String(serviceItem.identifier),
+        inputParams: serviceParams
+      }
+    })
+    message.success(`服务【${String(serviceItem.name)}】调用成功`)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error(`服务【${String(serviceItem.name)}】调用失败`)
+  }
+}
 </script>

+ 1 - 0
src/views/iot/device/device/detail/index.vue

@@ -26,6 +26,7 @@
           v-if="activeTab === 'simulator'"
           :product="product"
           :device="device"
+          :thing-model-list="thingModelList"
         />
       </el-tab-pane>
       <el-tab-pane label="设备配置" name="config">