Browse Source

Merge branch 'feature/iot' of https://gitee.com/alwayssuper/yudao-ui-admin-vue3 into feature/iot

# Conflicts:
#	pnpm-lock.yaml
YunaiV 7 months ago
parent
commit
f2f414a965

+ 1 - 1
.vscode/settings.json

@@ -87,7 +87,7 @@
     "source.fixAll.stylelint": "explicit"
   },
   "[vue]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
+    "editor.defaultFormatter": "octref.vetur"
   },
   "i18n-ally.localesPaths": ["src/locales"],
   "i18n-ally.keystyle": "nested",

+ 19 - 0
src/api/iot/device/device/index.ts

@@ -63,6 +63,16 @@ export enum DeviceStatusEnum {
   DISABLED = 3  // 已禁用
 }
 
+// IoT 模拟设备数据
+export interface SimulatorDataVO {
+  productKey: string
+  deviceKey: string
+  type: string
+  subType: string
+  reportTime: number // 时间戳
+  content: string  // 存储 JSON 字符串
+}
+
 // 设备 API
 export const DeviceApi = {
   // 查询设备分页
@@ -136,5 +146,14 @@ export const DeviceApi = {
   // 获取导入模板
   importDeviceTemplate: async () => {
     return await request.download({ url: `/iot/device/get-import-template` })
+  },
+
+  // 模拟设备
+  simulatorDevice: async (data: SimulatorDataVO) => {
+    return await request.post({ url: `/iot/device/data/simulator`, data })
+  },
+  //查询设备日志分页
+  getDeviceLogPage: async (params: any) => {
+    return await request.get({ url: `/iot/device/data/log/page`, params })
   }
 }

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

@@ -17,6 +17,13 @@ export interface ThingModelData {
   service?: ThingModelService // 服务
 }
 
+/**
+ * IoT 模拟设备
+ */
+export interface SimulatorData extends ThingModelData {
+  simulateValue?: string | number // 用于存储模拟值
+}
+
 /**
  * ThingModelProperty 类型
  */
@@ -45,6 +52,11 @@ export const ThingModelApi = {
     return await request.get({ url: `/iot/thing-model/page`, params })
   },
 
+  // 获得产品物模型列表
+  getThingModelList: async (params: any) => {
+    return await request.get({ url: `/iot/thing-model/list`, params })
+  },
+
   // 获得产品物模型
   getThingModelListByProductId: async (params: any) => {
     return await request.get({

+ 164 - 0
src/views/iot/device/device/detail/DeviceDetailsLog.vue

@@ -0,0 +1,164 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索区域 -->
+    <el-form :model="queryParams" inline>
+      <el-form-item>
+        <el-select v-model="queryParams.type" placeholder="所有" class="!w-120px">
+          <el-option label="所有" value="" />
+          <el-option label="状态" value="state" />
+          <el-option label="事件" value="event" />
+          <el-option label="属性" value="property" />
+          <el-option label="服务" value="service" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-input v-model="queryParams.keyword" placeholder="日志识符" class="!w-200px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-switch v-model="autoRefresh" class="ml-10px" /> 定时刷新
+      </el-form-item>
+    </el-form>
+
+    <!-- 日志列表 -->
+    <el-table v-loading="loading" :data="logList" :stripe="true" class="whitespace-nowrap">
+      <el-table-column label="时间" align="center" prop="time" width="180">
+        <template #default="scope">
+          {{ formatDate(scope.row.time) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="类型" align="center" prop="type" width="120" />
+      <el-table-column label="名称(标识符)" align="center" prop="subType" width="120" />
+      <el-table-column label="内容" align="center" prop="content" :show-overflow-tooltip="true" />
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="mt-10px flex justify-end">
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getLogList"
+      />
+    </div>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+
+const props = defineProps<{
+  deviceKey: number
+}>()
+
+//TODO:后续看看使用什么查询条件  目前后端是留了时间范围  type  subType
+// 查询参数 
+const queryParams = reactive({
+  deviceKey: props.deviceKey,
+  // type: '',
+  // keyword: '',
+  pageNo: 1,
+  pageSize: 10
+})
+
+// 列表数据
+const loading = ref(false)
+const total = ref(0)
+const logList = ref([])
+const autoRefresh = ref(false)
+let timer: any = null
+
+// 类型映射
+const typeMap = {
+  lifetime: '生命周期',
+  state: '设备状态',
+  property: '属性',
+  event: '事件',
+  service: '服务'
+}
+
+/** 查询日志列表 */
+const getLogList = async () => {
+  if (!props.deviceKey) return
+  loading.value = true
+  try {
+    const res = await DeviceApi.getDeviceLogPage(queryParams)
+    total.value = res.total
+    logList.value = res.list.map((item: any) => {
+      const log = {
+        time: item.reportTime,
+        type: item.type,
+        subType: item.subType,
+        content: item.content
+      }
+      return log
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取日志名称 */
+const getLogName = (log: any) => {
+  const { type, identifier } = log
+  let name = '未知'
+
+  if (type === 'property') {
+    if (identifier === 'set_reply') name = '设置回复'
+    else if (identifier === 'report') name = '上报'
+    else if (identifier === 'set') name = '设置'
+  } else if (type === 'state') {
+    name = identifier === 'online' ? '上线' : '下线'
+  } else if (type === 'lifetime') {
+    name = identifier === 'register' ? '注册' : name
+  }
+
+  return `${name}(${identifier})`
+}
+
+/** 搜索操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getLogList()
+}
+
+/** 监听自动刷新 */
+watch(autoRefresh, (newValue) => {
+  if (newValue) {
+    timer = setInterval(() => {
+      getLogList()
+    }, 5000)
+  } else {
+    clearInterval(timer)
+    timer = null
+  }
+})
+
+/** 监听设备ID变化 */
+watch(
+  () => props.deviceKey,
+  (newValue) => {
+    if (newValue) {
+      handleQuery()
+    }
+  }
+)
+
+/** 组件卸载时清除定时器 */
+onBeforeUnmount(() => {
+  if (timer) {
+    clearInterval(timer)
+  }
+})
+
+/** 初始化 */
+onMounted(() => {
+  if (props.deviceKey) {
+    getLogList()
+  }
+})
+</script>

+ 387 - 0
src/views/iot/device/device/detail/DeviceDetailsSimulator.vue

@@ -0,0 +1,387 @@
+<template>
+  <ContentWrap>
+    <el-row :gutter="20">
+      <!-- 左侧指令调试区域 -->
+      <el-col :span="12">
+        <el-tabs v-model="activeTab" type="border-card">
+          <!-- 上行指令调试 -->
+          <el-tab-pane label="上行指令调试" name="up">
+            <el-tabs v-model="subTab" v-if="activeTab === 'up'">
+              <!-- 属性上报 -->
+              <el-tab-pane label="属性上报" name="property">
+                <ContentWrap>
+                  <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column align="center" label="功能名称" prop="name" />
+                    <el-table-column align="center" label="标识符" prop="identifier" />
+                    <el-table-column align="center" label="数据类型" prop="identifier">
+                      <template #default="{ row }">
+                        {{ dataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="left" label="数据定义" prop="identifier">
+                      <template #default="{ row }">
+                        <!-- 属性 -->
+                        <template v-if="row.type === ThingModelType.PROPERTY">
+                          <!-- 非列表型:数值 -->
+                          <div
+                            v-if="
+                              [
+                                DataSpecsDataType.INT,
+                                DataSpecsDataType.DOUBLE,
+                                DataSpecsDataType.FLOAT
+                              ].includes(row.property.dataType)
+                            "
+                          >
+                            取值范围:{{
+                              `${row.property.dataSpecs.min}~${row.property.dataSpecs.max}`
+                            }}
+                          </div>
+                          <!-- 非列表型:文本 -->
+                          <div v-if="DataSpecsDataType.TEXT === row.property.dataType">
+                            数据长度:{{ row.property.dataSpecs.length }}
+                          </div>
+                          <!-- 列表型: 数组、结构、时间(特殊) -->
+                          <div
+                            v-if="
+                              [
+                                DataSpecsDataType.ARRAY,
+                                DataSpecsDataType.STRUCT,
+                                DataSpecsDataType.DATE
+                              ].includes(row.property.dataType)
+                            "
+                          >
+                            -
+                          </div>
+                          <!-- 列表型: 布尔值、枚举 -->
+                          <div
+                            v-if="
+                              [DataSpecsDataType.BOOL, DataSpecsDataType.ENUM].includes(
+                                row.property.dataType
+                              )
+                            "
+                          >
+                            <div>
+                              {{
+                                DataSpecsDataType.BOOL === row.property.dataType
+                                  ? '布尔值'
+                                  : '枚举值'
+                              }}:
+                            </div>
+                            <div v-for="item in row.property.dataSpecsList" :key="item.value">
+                              {{ `${item.name}-${item.value}` }}
+                            </div>
+                          </div>
+                        </template>
+                        <!-- 服务 -->
+                        <div v-if="row.type === ThingModelType.SERVICE">
+                          调用方式:{{ getCallTypeByValue(row.service.callType) }}
+                        </div>
+                        <!-- 事件 -->
+                        <div v-if="row.type === ThingModelType.EVENT">
+                          事件类型:{{ getEventTypeByValue(row.event.type) }}
+                        </div>
+                      </template>
+                    </el-table-column>
+                    <el-table-column label="值" align="center" width="80">
+                      <template #default="scope">
+                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                      </template>
+                    </el-table-column>
+                  </el-table>
+                  <div class="mt-10px">
+                    <el-button type="primary" @click="handlePropertyReport">发送</el-button>
+                  </div>
+                </ContentWrap>
+              </el-tab-pane>
+
+              <!-- 事件上报 -->
+              <el-tab-pane label="事件上报" name="event">
+                <ContentWrap>
+                  <!-- <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-column
+                      label="数据定义"
+                      align="center"
+                      prop="specs"
+                      :show-overflow-tooltip="true"
+                    />
+                    <el-table-column label="值" align="center" width="80">
+                      <template #default="scope">
+                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                      </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">
+                <ContentWrap>
+                  <div class="flex gap-4">
+                    <el-button type="primary" @click="handleDeviceState('online')"
+                      >设备上线</el-button
+                    >
+                    <el-button type="primary" @click="handleDeviceState('offline')"
+                      >设备下线</el-button
+                    >
+                  </div>
+                </ContentWrap>
+              </el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+
+          <!-- 下行指令调试 -->
+          <el-tab-pane label="下行指令调试" name="down">
+            <el-tabs v-model="subTab" v-if="activeTab === 'down'">
+              <!-- 属性调试 -->
+              <el-tab-pane label="属性调试" name="propertyDebug">
+                <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-column
+                      label="数据定义"
+                      align="center"
+                      prop="specs"
+                      :show-overflow-tooltip="true"
+                    />
+                    <el-table-column label="值" align="center" width="80">
+                      <template #default="scope">
+                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                      </template>
+                    </el-table-column>
+                  </el-table>
+                  <div class="mt-10px">
+                    <el-button type="primary" @click="handlePropertyGet">获取</el-button>
+                  </div> -->
+                </ContentWrap>
+              </el-tab-pane>
+
+              <!-- 服务调用 -->
+              <el-tab-pane label="服务调用" name="service">
+                <ContentWrap>
+                  <!-- 服务调用相关内容 -->
+                </ContentWrap>
+              </el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+        </el-tabs>
+      </el-col>
+
+      <!-- 右侧设备日志区域 -->
+      <el-col :span="12">
+        <el-tabs type="border-card">
+          <el-tab-pane label="设备日志">
+            <DeviceDetailsLog :deviceKey="device.deviceKey" />
+          </el-tab-pane>
+        </el-tabs>
+      </el-col>
+    </el-row>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { ProductVO } from '@/api/iot/product/product'
+import { ThingModelApi, ThingModelData,SimulatorData } from '@/api/iot/thingmodel'
+import { DeviceApi, DeviceVO,SimulatorDataVO } from '@/api/iot/device/device'
+import DeviceDetailsLog from './DeviceDetailsLog.vue'
+import {
+  DataSpecsDataType,
+  getCallTypeByValue,
+  getDataTypeOptionsLabel,
+  getEventTypeByValue,
+  ThingModelType
+} from '@/views/iot/thingmodel/config'
+
+const message = useMessage() // 消息弹窗
+const loading = ref(false)
+const activeTab = ref('up')
+const subTab = ref('property')
+
+const queryParams = reactive({
+  type: undefined,
+  productId: -1
+})
+const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
+const props = defineProps<{ product: ProductVO; device: DeviceVO }>()
+const list = ref<SimulatorData[]>([]) // 物模型列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.productId = props.product?.id || -1
+    const data = await ThingModelApi.getThingModelList(queryParams)
+    // 转换数据,添加 simulateValue 字段
+    list.value = data.map(item => ({
+      ...item,
+      simulateValue: ''
+    }))
+  } finally {
+    loading.value = false
+  }
+}
+
+// // 功能列表数据结构定义
+// 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
+        // case 'status':
+        //   queryParams.type = 'status'
+        //   break
+      }
+    } else if (newActiveTab === 'down') {
+      switch (newSubTab) {
+        case 'propertyDebug':
+          queryParams.type = 1
+          break
+        case 'service':
+          queryParams.type = 2
+          break
+      }
+    }
+    getList() // 切换标签时重新获取数据
+  },
+  { immediate: true }
+)
+
+// interface ReportData {
+//   productKey: string
+//   deviceKey: string
+//   type: string
+//   subType: string
+//   reportTime: string
+//   content: string  // 改为 string 类型,存储 JSON 字符串
+// }
+
+// 处理属性上报    TODO:数据类型效验
+const handlePropertyReport = async () => {
+  const contentObj: Record<string, any> = {}
+  list.value.forEach((item) => {
+    // 只有当 simulateValue 有值时才添加到 content 中
+    if (item.simulateValue !== undefined && item.simulateValue !== '') {
+      contentObj[item.identifier] = item.simulateValue
+    }
+  })
+
+  const reportData: SimulatorDataVO = {
+    productKey: props.product.productKey,
+    deviceKey: props.device.deviceKey,
+    type: 'property',
+    subType: 'report',
+    reportTime: Date.now(), // 将 reportTime 变为数字类型的时间戳
+    content: JSON.stringify(contentObj)  // 转换为 JSON 字符串
+  }
+
+  try {
+    await DeviceApi.simulatorDevice(reportData)
+    message.success('属性上报成功')
+  } 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 字符串
+//   }
+
+//   try {
+//     // TODO: 调用API发送数据
+//     console.log('上报数据:', reportData)
+//     message.success('事件上报成功')
+//   } catch (error) {
+//     message.error('事件上报失败')
+//   }
+// }
+
+// // 处理设备状态变更
+// const handleDeviceState = async (state: 'online' | 'offline') => {
+//   const reportData: ReportData = {
+//     productKey: props.product.productKey,
+//     deviceKey: props.device.deviceKey,
+//     type: 'status',
+//     subType: state,
+//     reportTime: new Date().toISOString(),
+//     content: JSON.stringify({ status: state })  // 转换为 JSON 字符串
+//   }
+
+//   try {
+//     // TODO: 调用API发送数据
+//     console.log('状态变更数据:', reportData)
+//     console.log('reportData.content111111111', reportData.content)
+//     message.success(`设备${state === 'online' ? '上线' : '下线'}成功`)
+//   } catch (error) {
+//     message.error(`设备${state === 'online' ? '上线' : '下线'}失败`)
+//   }
+// }
+
+// 处理属性获取
+const handlePropertyGet = async () => {
+  // TODO: 实现属性获取逻辑
+  message.success('属性获取成功')
+}
+
+// 初始化
+onMounted(() => {
+  getList()
+})
+</script>

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

@@ -16,6 +16,12 @@
       </el-tab-pane>
       <el-tab-pane label="子设备管理" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
       <el-tab-pane label="设备影子" />
+      <el-tab-pane label="设备日志" name="log">
+        <DeviceDetailsLog v-if="activeTab === 'log'"  :deviceKey="device.deviceKey" />
+      </el-tab-pane>
+      <el-tab-pane label="模拟设备" name="simulator">
+        <DeviceDetailsSimulator v-if="activeTab === 'simulator'" :product="product" :device="device" />
+     </el-tab-pane>
     </el-tabs>
   </el-col>
 </template>
@@ -26,7 +32,8 @@ import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product
 import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
 import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
 import DeviceDetailsModel from './DeviceDetailsModel.vue'
-
+import DeviceDetailsLog from './DeviceDetailsLog.vue'
+import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
 defineOptions({ name: 'IoTDeviceDetail' })
 
 const route = useRoute()