Просмотр исходного кода

feat:【IoT 物联网】继续实现“数据流转”功能

YunaiV 11 месяцев назад
Родитель
Сommit
b1ae56281d

+ 2 - 2
src/api/iot/rule/data/rule/index.ts

@@ -6,8 +6,8 @@ export interface DataRule {
   name?: string // 场景名称
   description: string // 场景描述
   status?: number // 场景状态
-  sourceConfigs?: string // 数据源配置数组
-  sinkIds?: string // 数据目的编号数组
+  sourceConfigs?: any[] // 数据源配置数组
+  sinkIds?: number[] // 数据目的编号数组
 }
 
 // IoT 数据流转规则 API

+ 57 - 22
src/views/iot/rule/data/rule/DataRuleForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="860">
     <el-form
       ref="formRef"
       :model="formData"
@@ -7,13 +7,13 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="场景名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入场景名称" />
+      <el-form-item label="规则名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入规则名称" />
       </el-form-item>
-      <el-form-item label="场景描述" prop="description">
-        <Editor v-model="formData.description" height="150px" />
+      <el-form-item label="规则描述" prop="description">
+        <el-input v-model="formData.description" height="150px" type="textarea" />
       </el-form-item>
-      <el-form-item label="场景状态" prop="status">
+      <el-form-item label="规则状态" prop="status">
         <el-radio-group v-model="formData.status">
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -24,11 +24,24 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="数据源配置数组" prop="sourceConfigs">
-        <el-input v-model="formData.sourceConfigs" placeholder="请输入数据源配置数组" />
+      <el-form-item label="数据目的" prop="sinkIds">
+        <el-select
+          v-model="formData.sinkIds"
+          placeholder="请选择数据目的"
+          multiple
+          clearable
+          class="w-1/1"
+        >
+          <el-option
+            v-for="sink in dataSinkList"
+            :key="sink.id"
+            :label="sink.name"
+            :value="sink.id"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="数据目的编号数组" prop="sinkIds">
-        <el-input v-model="formData.sinkIds" placeholder="请输入数据目的编号数组" />
+      <el-form-item label="数据源" prop="sourceConfigs">
+        <SourceConfigForm ref="sourceConfigRef" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -40,8 +53,11 @@
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import { DataRuleApi, DataRule } from '@/api/iot/rule/data/rule'
+import { DataSinkApi } from '@/api/iot/rule/data/sink'
+import { CommonStatusEnum } from '@/utils/constants'
+import SourceConfigForm from './components/SourceConfigForm.vue'
 
-/** IoT 数据流转规则 表单 */
+/** IoT 数据流转规则表单 */
 defineOptions({ name: 'DataRuleForm' })
 
 const { t } = useI18n() // 国际化
@@ -55,17 +71,19 @@ const formData = ref({
   id: undefined,
   name: undefined,
   description: undefined,
-  status: undefined,
-  sourceConfigs: undefined,
-  sinkIds: undefined
+  status: CommonStatusEnum.ENABLE,
+  sourceConfigs: [],
+  sinkIds: []
 })
 const formRules = reactive({
-  name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '场景状态不能为空', trigger: 'blur' }],
-  sourceConfigs: [{ required: true, message: '数据源配置数组不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '规则状态不能为空', trigger: 'blur' }],
+  // sourceConfigs: [{ required: true, message: '数据源配置数组不能为空', trigger: 'blur' }],
   sinkIds: [{ required: true, message: '数据目的编号数组不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
+const dataSinkList = ref<any[]>([]) // 数据目的列表
+const sourceConfigRef = ref() // 数据源配置组件引用
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -73,15 +91,24 @@ const open = async (type: string, id?: number) => {
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await DataRuleApi.getDataRule(id)
+      const data = await DataRuleApi.getDataRule(id)
+      formData.value = data
+      // 设置数据源配置
+      nextTick(() => {
+        sourceConfigRef.value?.setData(data.sourceConfigs || [])
+      })
     } finally {
       formLoading.value = false
     }
   }
+
+  // 加载数据目的列表
+  dataSinkList.value = await DataSinkApi.getDataSinkSimpleList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -90,10 +117,14 @@ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成
 const submitForm = async () => {
   // 校验表单
   await formRef.value.validate()
+  // 校验数据源配置
+  await sourceConfigRef.value?.validate()
+
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as DataRule
+    const data = { ...formData.value } as unknown as DataRule
+    data.sourceConfigs = sourceConfigRef.value?.getData() || []
     if (formType.value === 'create') {
       await DataRuleApi.createDataRule(data)
       message.success(t('common.createSuccess'))
@@ -115,10 +146,14 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     description: undefined,
-    status: undefined,
-    sourceConfigs: undefined,
-    sinkIds: undefined
+    status: CommonStatusEnum.ENABLE,
+    sourceConfigs: [],
+    sinkIds: []
   }
   formRef.value?.resetFields()
+  // 重置数据源配置
+  nextTick(() => {
+    sourceConfigRef.value?.setData([])
+  })
 }
 </script>

+ 272 - 0
src/views/iot/rule/data/rule/components/SourceConfigForm.vue

@@ -0,0 +1,272 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="产品" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+            <el-select
+              v-model="row.productId"
+              placeholder="请选择产品"
+              @change="handleProductChange(row, $index)"
+              clearable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="product in productList"
+                :key="product.id"
+                :label="product.name"
+                :value="product.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.deviceId`" :rules="formRules.deviceId" class="mb-0px!">
+            <el-select
+              v-model="row.deviceId"
+              placeholder="请选择设备"
+              @change="handleDeviceChange(row, $index)"
+              clearable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="device in getFilteredDevices(row.productId)"
+                :key="device.id"
+                :label="device.deviceName"
+                :value="device.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="消息方法" min-width="180">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.method`" :rules="formRules.method" class="mb-0px!">
+            <el-select
+              v-model="row.method"
+              placeholder="请选择消息方法"
+              @change="handleMethodChange(row, $index)"
+              clearable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="method in upstreamMethods"
+                :key="method.method"
+                :label="method.name"
+                :value="method.method"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="标识符" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.identifier`" class="mb-0px!">
+            <el-select
+              v-if="shouldShowIdentifierSelect(row)"
+              v-model="row.identifier"
+              placeholder="请选择标识符"
+              clearable
+              style="width: 100%"
+              v-loading="row.identifierLoading"
+            >
+              <el-option
+                v-for="item in getThingModelOptions(row)"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+            <span v-else>-</span>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link type="danger">—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-row justify="center" class="mt-3">
+      <el-button @click="handleAdd" type="primary" plain round>+ 添加数据源</el-button>
+    </el-row>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ProductApi } from '@/api/iot/product/product'
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
+
+const formData = ref<any[]>([])
+const productList = ref<any[]>([]) // 产品列表
+const deviceList = ref<any[]>([]) // 设备列表
+const thingModelMap = ref<Map<number, any[]>>(new Map()) // 缓存物模型数据,key 为 productId
+
+const formRules = reactive({
+  productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
+  deviceId: [{ required: true, message: '设备不能为空', trigger: 'change' }],
+  method: [{ required: true, message: '消息方法不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 获取上行消息方法列表
+const upstreamMethods = computed(() => {
+  return Object.values(IotDeviceMessageMethodEnum).filter((item) => item.upstream)
+})
+
+/** 根据产品ID过滤设备 */
+const getFilteredDevices = (productId: number) => {
+  if (!productId) return []
+  return deviceList.value.filter((device: any) => device.productId === productId)
+}
+
+/** 判断是否需要显示标识符选择器 */
+const shouldShowIdentifierSelect = (row: any) => {
+  return (
+    row.method === IotDeviceMessageMethodEnum.EVENT_POST.method ||
+    row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method
+  )
+}
+
+/** 获取物模型选项 */
+const getThingModelOptions = (row: any) => {
+  if (!row.productId || !shouldShowIdentifierSelect(row)) return []
+
+  const thingModels: any[] = thingModelMap.value.get(row.productId) || []
+  let filteredModels: any[] = []
+
+  if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) {
+    // 事件类型,type = 3
+    filteredModels = thingModels.filter((item: any) => item.type === 3)
+  } else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) {
+    // 属性类型,type = 1
+    filteredModels = thingModels.filter((item: any) => item.type === 1)
+  }
+
+  return filteredModels.map((item: any) => ({
+    label: `${item.name} (${item.identifier})`,
+    value: item.identifier
+  }))
+}
+
+/** 加载产品列表 */
+const loadProductList = async () => {
+  try {
+    productList.value = await ProductApi.getSimpleProductList()
+  } catch (error) {
+    console.error('加载产品列表失败:', error)
+  }
+}
+
+/** 加载设备列表 */
+const loadDeviceList = async () => {
+  try {
+    deviceList.value = await DeviceApi.getSimpleDeviceList()
+  } catch (error) {
+    console.error('加载设备列表失败:', error)
+  }
+}
+
+/** 加载物模型数据 */
+const loadThingModel = async (productId: number) => {
+  if (thingModelMap.value.has(productId)) {
+    return // 已缓存,无需重复加载
+  }
+
+  try {
+    const thingModels = await ThingModelApi.getThingModelList({ productId })
+    thingModelMap.value.set(productId, thingModels)
+  } catch (error) {
+    console.error('加载物模型失败:', error)
+    thingModelMap.value.set(productId, [])
+  }
+}
+
+/** 产品变化时处理 */
+const handleProductChange = async (row: any, _index: number) => {
+  // 清空其他字段
+  row.deviceId = undefined
+  row.method = undefined
+  row.identifier = undefined
+
+  // 根据产品ID过滤设备列表不需要额外处理,计算属性会自动过滤
+}
+
+/** 设备变化时处理 */
+const handleDeviceChange = (row: any, _index: number) => {
+  // 设备变化时可以做一些额外处理
+}
+
+/** 消息方法变化时处理 */
+const handleMethodChange = async (row: any, _index: number) => {
+  // 清空标识符
+  row.identifier = undefined
+
+  // 如果需要加载物模型数据
+  if (shouldShowIdentifierSelect(row) && row.productId) {
+    row.identifierLoading = true
+    await loadThingModel(row.productId)
+    row.identifierLoading = false
+  }
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    productId: undefined,
+    deviceId: undefined,
+    method: undefined,
+    identifier: undefined,
+    identifierLoading: false
+  }
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+/** 设置表单值 */
+const setData = (data: any[]) => {
+  // 确保每个项都有必要的字段
+  formData.value = (data || []).map((item) => ({
+    ...item,
+    identifierLoading: false
+  }))
+
+  // 为已有数据预加载物模型
+  data?.forEach(async (item) => {
+    if (item.productId && shouldShowIdentifierSelect(item)) {
+      await loadThingModel(item.productId)
+    }
+  })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await Promise.all([loadProductList(), loadDeviceList()])
+})
+
+defineExpose({ validate, getData, setData })
+</script>

+ 8 - 8
src/views/iot/rule/data/rule/index.vue

@@ -8,19 +8,19 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="场景名称" prop="name">
+      <el-form-item label="规则名称" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输入场景名称"
+          placeholder="请输入规则名称"
           clearable
           @keyup.enter="handleQuery"
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="场景状态" prop="status">
+      <el-form-item label="规则状态" prop="status">
         <el-select
           v-model="queryParams.status"
-          placeholder="请选择场景状态"
+          placeholder="请选择规则状态"
           clearable
           class="!w-240px"
         >
@@ -68,10 +68,10 @@
       :show-overflow-tooltip="true"
     >
       <el-table-column type="selection" width="55" />
-      <el-table-column label="场景编号" align="center" prop="id" />
-      <el-table-column label="场景名称" align="center" prop="name" />
-      <el-table-column label="场景描述" align="center" prop="description" />
-      <el-table-column label="场景状态" align="center" prop="status">
+      <el-table-column label="规则编号" align="center" prop="id" />
+      <el-table-column label="规则名称" align="center" prop="name" />
+      <el-table-column label="规则描述" align="center" prop="description" />
+      <el-table-column label="规则状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>

+ 7 - 11
src/views/iot/rule/data/sink/DataSinkForm.vue

@@ -10,8 +10,11 @@
       <el-form-item label="目的名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入目的名称" />
       </el-form-item>
+      <el-form-item label="目的描述" prop="description">
+        <el-input v-model="formData.description" height="150px" type="textarea" />
+      </el-form-item>
       <el-form-item label="目的类型" prop="type">
-        <el-select v-model="formData.type">
+        <el-select v-model="formData.type" @change="handleTypeChange">
           <el-option
             v-for="item in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
             :key="item.value"
@@ -49,9 +52,6 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="目的描述" prop="description">
-        <el-input v-model="formData.description" height="150px" type="textarea" />
-      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -60,7 +60,7 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE, getDictObj, getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { CommonStatusEnum } from '@/utils/constants'
 import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
 import {
@@ -126,10 +126,6 @@ const formRules = reactive({
 })
 
 const formRef = ref() // 表单 Ref
-const showConfig = computed(() => (val: number) => {
-  const dict = getDictObj(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, formData.value.type)
-  return dict && dict.value + '' === val + ''
-}) // 显示对应的 Config 配置项
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -174,8 +170,8 @@ const submitForm = async () => {
 }
 
 /** 处理类型切换事件 */
-const handleTypeChange = (val: number) => {
-  formData.value.type = val
+const handleTypeChange = (type: number) => {
+  formData.value.type = type
   // 切换类型时重置配置
   formData.value.config = {} as any
 }