Browse Source

perf: 【IoT 物联网】场景联动执行器和触发器的参数值类型都调整为了字符串类型

puhui999 6 months ago
parent
commit
d81c544ad9

+ 1 - 1
src/api/iot/rule/scene/scene.types.ts

@@ -175,7 +175,7 @@ interface Action {
   productId?: number // 产品编号
   deviceId?: number // 设备编号
   identifier?: string // 物模型标识符(服务调用时使用)
-  params?: Record<string, any> // 请求参数
+  params?: string // 请求参数
   alertConfigId?: number // 告警配置编号
 }
 

+ 106 - 78
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue

@@ -82,8 +82,12 @@ import { useVModel } from '@vueuse/core'
 import ProductSelector from '../selectors/ProductSelector.vue'
 import DeviceSelector from '../selectors/DeviceSelector.vue'
 import JsonParamsInput from '../inputs/JsonParamsInput.vue'
-import { Action, ThingModelService } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
+import { Action, ThingModelProperty, ThingModelService } from '@/api/iot/rule/scene/scene.types'
+import {
+  IotRuleSceneActionTypeEnum,
+  IoTThingModelAccessModeEnum
+} from '@/views/iot/utils/constants'
+import { ThingModelApi } from '@/api/iot/thingmodel'
 
 /** 设备控制配置组件 */
 defineOptions({ name: 'DeviceControlConfig' })
@@ -99,7 +103,7 @@ const emit = defineEmits<{
 const action = useVModel(props, 'modelValue', emit)
 
 // 简化后的状态变量
-const thingModelProperties = ref<any[]>([]) // 物模型属性列表
+const thingModelProperties = ref<ThingModelProperty[]>([]) // 物模型属性列表
 const loadingThingModel = ref(false) // 物模型加载状态
 const selectedService = ref<ThingModelService | null>(null) // 选中的服务对象
 const serviceList = ref<ThingModelService[]>([]) // 服务列表
@@ -108,21 +112,16 @@ const loadingServices = ref(false) // 服务加载状态
 // 参数值的计算属性,用于双向绑定
 const paramsValue = computed({
   get: () => {
+    // 如果 params 是对象,转换为 JSON 字符串(兼容旧数据)
     if (action.value.params && typeof action.value.params === 'object') {
       return JSON.stringify(action.value.params, null, 2)
     }
-    return ''
+    // 如果 params 已经是字符串,直接返回
+    return action.value.params || ''
   },
   set: (value: string) => {
-    try {
-      if (value.trim()) {
-        action.value.params = JSON.parse(value)
-      } else {
-        action.value.params = {}
-      }
-    } catch (error) {
-      console.error('JSON解析错误:', error)
-    }
+    // 直接保存为 JSON 字符串,不进行解析转换
+    action.value.params = value.trim() || ''
   }
 })
 
@@ -151,7 +150,7 @@ const handleProductChange = (productId?: number) => {
   if (action.value.productId !== productId) {
     action.value.deviceId = undefined
     action.value.identifier = undefined // 清空服务标识符
-    action.value.params = {}
+    action.value.params = '' // 清空参数,保存为空字符串
     selectedService.value = null // 清空选中的服务
     serviceList.value = [] // 清空服务列表
   }
@@ -173,7 +172,7 @@ const handleProductChange = (productId?: number) => {
 const handleDeviceChange = (deviceId?: number) => {
   // 当设备变化时,清空参数配置
   if (action.value.deviceId !== deviceId) {
-    action.value.params = {}
+    action.value.params = '' // 清空参数,保存为空字符串
   }
 }
 
@@ -187,7 +186,7 @@ const handleServiceChange = (serviceIdentifier?: string) => {
   selectedService.value = service
 
   // 当服务变化时,清空参数配置
-  action.value.params = {}
+  action.value.params = ''
 
   // 如果选择了服务且有输入参数,生成默认参数结构
   if (service && service.inputParams && service.inputParams.length > 0) {
@@ -195,12 +194,29 @@ const handleServiceChange = (serviceIdentifier?: string) => {
     service.inputParams.forEach((param) => {
       defaultParams[param.identifier] = getDefaultValueForParam(param)
     })
-    action.value.params = defaultParams
+    // 将默认参数转换为 JSON 字符串保存
+    action.value.params = JSON.stringify(defaultParams, null, 2)
+  }
+}
+
+/**
+ * 获取物模型TSL数据
+ * @param productId 产品ID
+ * @returns 物模型TSL数据
+ */
+const getThingModelTSL = async (productId: number) => {
+  if (!productId) return null
+
+  try {
+    return await ThingModelApi.getThingModelTSLByProductId(productId)
+  } catch (error) {
+    console.error('获取物模型TSL数据失败:', error)
+    return null
   }
 }
 
 /**
- * 加载物模型属性
+ * 加载物模型属性(可写属性)
  * @param productId 产品ID
  */
 const loadThingModelProperties = async (productId: number) => {
@@ -211,39 +227,22 @@ const loadThingModelProperties = async (productId: number) => {
 
   try {
     loadingThingModel.value = true
-    // TODO: 这里需要调用实际的物模型API
-    // const response = await ProductApi.getThingModel(productId)
-    // 暂时使用模拟数据
-    thingModelProperties.value = [
-      {
-        identifier: 'BatteryLevel',
-        name: '电池电量',
-        dataType: 'int',
-        description: '设备电池电量百分比'
-      },
-      {
-        identifier: 'WaterLeachState',
-        name: '漏水状态',
-        dataType: 'bool',
-        description: '设备漏水检测状态'
-      },
-      {
-        identifier: 'Temperature',
-        name: '温度',
-        dataType: 'float',
-        description: '环境温度值'
-      },
-      {
-        identifier: 'Humidity',
-        name: '湿度',
-        dataType: 'float',
-        description: '环境湿度值'
-      }
-    ]
+    const tslData = await getThingModelTSL(productId)
+
+    if (!tslData?.properties) {
+      thingModelProperties.value = []
+      return
+    }
 
-    // 属性加载完成,无需额外初始化
+    // 过滤出可写的属性(accessMode 包含 'w')
+    thingModelProperties.value = tslData.properties.filter(
+      (property: ThingModelProperty) =>
+        property.accessMode &&
+        (property.accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
+          property.accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value)
+    )
   } catch (error) {
-    console.error('加载物模型失败:', error)
+    console.error('加载物模型属性失败:', error)
     thingModelProperties.value = []
   } finally {
     loadingThingModel.value = false
@@ -260,11 +259,16 @@ const loadServiceList = async (productId: number) => {
     return
   }
 
-  loadingServices.value = true
   try {
-    const { ThingModelApi } = await import('@/api/iot/thingmodel')
-    const tslData = await ThingModelApi.getThingModelTSLByProductId(productId)
-    serviceList.value = tslData?.services || []
+    loadingServices.value = true
+    const tslData = await getThingModelTSL(productId)
+
+    if (!tslData?.services) {
+      serviceList.value = []
+      return
+    }
+
+    serviceList.value = tslData.services
   } catch (error) {
     console.error('加载服务列表失败:', error)
     serviceList.value = []
@@ -316,43 +320,67 @@ const getDefaultValueForParam = (param: any) => {
   }
 }
 
+// 防止重复初始化的标志
+const isInitialized = ref(false)
+
 /**
- * 组件初始化
+ * 初始化组件数据
  */
-onMounted(() => {
+const initializeComponent = async () => {
+  if (isInitialized.value) return
+
+  const currentAction = action.value
+  if (!currentAction) return
+
   // 如果已经选择了产品且是属性设置类型,加载物模型
-  if (action.value.productId && isPropertySetAction.value) {
-    loadThingModelProperties(action.value.productId)
+  if (currentAction.productId && isPropertySetAction.value) {
+    await loadThingModelProperties(currentAction.productId)
   }
 
   // 如果是服务调用类型且已有标识符,初始化服务选择
-  if (action.value.productId && isServiceInvokeAction.value && action.value.identifier) {
+  if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
     // 加载物模型TSL以获取服务信息
-    loadServiceFromTSL(action.value.productId, action.value.identifier)
+    await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
   }
+
+  isInitialized.value = true
+}
+
+/**
+ * 组件初始化
+ */
+onMounted(() => {
+  initializeComponent()
 })
 
-// 监听action.value变化,处理编辑模式的数据回显
+// 只监听关键字段的变化,避免深度监听导致的性能问题
 watch(
-  () => action.value,
-  async (newAction) => {
-    if (newAction) {
-      // 处理服务调用的数据回显
-      if (isServiceInvokeAction.value && newAction.productId) {
-        if (newAction.identifier) {
-          // 编辑模式:加载服务信息并设置选中的服务
-          await loadServiceFromTSL(newAction.productId, newAction.identifier)
-        } else {
-          // 新建模式:只加载服务列表
-          await loadServiceList(newAction.productId)
-        }
-      } else if (isServiceInvokeAction.value) {
-        // 清空服务选择
-        selectedService.value = null
-        serviceList.value = []
+  () => [action.value.productId, action.value.type, action.value.identifier],
+  async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
+    // 避免初始化时的重复调用
+    if (!isInitialized.value) return
+
+    // 产品变化时重新加载数据
+    if (newProductId !== oldProductId) {
+      if (newProductId && isPropertySetAction.value) {
+        await loadThingModelProperties(newProductId as number)
+      } else if (newProductId && isServiceInvokeAction.value) {
+        await loadServiceList(newProductId as number)
       }
     }
-  },
-  { deep: true, immediate: true }
+
+    // 服务标识符变化时更新选中的服务
+    if (
+      newIdentifier !== oldIdentifier &&
+      newProductId &&
+      isServiceInvokeAction.value &&
+      newIdentifier
+    ) {
+      const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
+      if (service) {
+        selectedService.value = service
+      }
+    }
+  }
 )
 </script>

+ 79 - 12
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue

@@ -59,8 +59,14 @@
           </el-form-item>
         </el-col>
 
-        <!-- 操作符选择 - 服务调用不需要操作符 -->
-        <el-col v-if="triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" :span="6">
+        <!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
+        <el-col
+          v-if="
+            triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+            triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+          "
+          :span="6"
+        >
           <el-form-item label="操作符" required>
             <OperatorSelector
               :model-value="condition.operator"
@@ -72,7 +78,14 @@
         </el-col>
 
         <!-- 值输入 -->
-        <el-col :span="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ? 18 : 12">
+        <el-col
+          :span="
+            triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ||
+            triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+              ? 18
+              : 12
+          "
+        >
           <el-form-item
             :label="
               triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
@@ -84,20 +97,18 @@
             <!-- 服务调用参数配置 -->
             <JsonParamsInput
               v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
-              :model-value="condition.value"
-              @update:model-value="(value) => updateConditionField('value', value)"
+              v-model="conditionValueAsString"
               type="service"
-              :config="{ service: propertyConfig }"
+              :config="serviceConfig"
               placeholder="请输入JSON格式的服务参数"
               @validate="handleValueValidate"
             />
             <!-- 事件上报参数配置 -->
             <JsonParamsInput
               v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
-              :model-value="condition.value"
-              @update:model-value="(value) => updateConditionField('value', value)"
+              v-model="conditionValueAsString"
               type="event"
-              :config="{ event: propertyConfig }"
+              :config="eventConfig"
               placeholder="请输入JSON格式的事件参数"
               @validate="handleValueValidate"
             />
@@ -217,6 +228,52 @@ const isDeviceStatusTrigger = computed(() => {
   return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
 })
 
+// 服务配置 - 用于 JsonParamsInput
+const serviceConfig = computed(() => {
+  if (
+    propertyConfig.value &&
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ) {
+    return {
+      service: {
+        name: propertyConfig.value.name || '服务',
+        inputParams: propertyConfig.value.inputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
+// 事件配置 - 用于 JsonParamsInput
+const eventConfig = computed(() => {
+  if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+    return {
+      event: {
+        name: propertyConfig.value.name || '事件',
+        outputParams: propertyConfig.value.outputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
+// 确保传递给 JsonParamsInput 的值始终是字符串类型
+const conditionValueAsString = computed({
+  get: () => {
+    const value = condition.value.value
+    if (value === null || value === undefined) {
+      return ''
+    }
+    if (typeof value === 'object') {
+      return JSON.stringify(value, null, 2)
+    }
+    return String(value)
+  },
+  set: (newValue: string) => {
+    condition.value.value = newValue
+  }
+})
+
 // 获取触发类型文本
 // TODO @puhui999:是不是有枚举可以服用哈;
 const getTriggerTypeText = (type: number) => {
@@ -264,6 +321,14 @@ const handlePropertyChange = (propertyInfo: any) => {
   if (propertyInfo) {
     propertyType.value = propertyInfo.type
     propertyConfig.value = propertyInfo.config
+
+    // 对于事件上报和服务调用,自动设置操作符为 '='
+    if (
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    ) {
+      condition.value.operator = '='
+    }
   }
   updateValidationResult()
 }
@@ -306,9 +371,10 @@ const updateValidationResult = () => {
       return
     }
 
-    // 服务调用不需要操作符
+    // 服务调用和事件上报不需要操作符
     if (
       props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+      props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST &&
       !condition.value.operator
     ) {
       isValid.value = false
@@ -336,8 +402,9 @@ watch(
     condition.value.productId,
     condition.value.deviceId,
     condition.value.identifier,
-    // 服务调用不需要监听操作符
-    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    // 服务调用和事件上报不需要监听操作符
+    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
       ? condition.value.operator
       : null,
     condition.value.value

+ 66 - 14
src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue

@@ -169,7 +169,6 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value: string): void
-
   (e: 'validate', result: { valid: boolean; message: string }): void
 }
 
@@ -441,37 +440,90 @@ const generateExampleJson = () => {
   return JSON.stringify(example, null, 2)
 }
 
-// 初始化
-onMounted(() => {
+// 初始化标志,防止重复初始化
+const isInitialized = ref(false)
+
+// 初始化数据
+const initializeData = () => {
+  if (isInitialized.value) return
+
   if (localValue.value) {
     try {
-      paramsJson.value = localValue.value
+      // modelValue 已经是字符串类型,直接使用
+      if (localValue.value.trim()) {
+        try {
+          // 尝试解析JSON,如果成功则格式化
+          const parsed = JSON.parse(localValue.value)
+          paramsJson.value = JSON.stringify(parsed, null, 2)
+        } catch {
+          // 如果不是有效的JSON,直接使用原字符串
+          paramsJson.value = localValue.value
+        }
+      } else {
+        paramsJson.value = ''
+      }
+
       jsonError.value = ''
     } catch (error) {
       console.error('初始化参数失败:', error)
       jsonError.value = '初始参数格式错误'
     }
   }
+
+  isInitialized.value = true
+}
+
+// 组件挂载时初始化
+onMounted(() => {
+  initializeData()
 })
 
-// 监听输入值变化
+// 监听外部值变化(编辑模式数据回显)
 watch(
   () => localValue.value,
-  (newValue) => {
-    if (newValue !== paramsJson.value) {
-      paramsJson.value = newValue || ''
+  (newValue, oldValue) => {
+    // 避免循环更新
+    if (newValue === oldValue) return
+
+    try {
+      let newJsonString = ''
+
+      if (newValue && newValue.trim()) {
+        try {
+          // 尝试解析JSON,如果成功则格式化
+          const parsed = JSON.parse(newValue)
+          newJsonString = JSON.stringify(parsed, null, 2)
+        } catch {
+          // 如果不是有效的JSON,直接使用原字符串
+          newJsonString = newValue
+        }
+      }
+
+      // 只有当JSON字符串真正改变时才更新
+      if (newJsonString !== paramsJson.value) {
+        paramsJson.value = newJsonString
+        jsonError.value = ''
+      }
+    } catch (error) {
+      console.error('数据回显失败:', error)
+      jsonError.value = '数据格式错误'
     }
-  }
+  },
+  { immediate: true }
 )
 
 // 监听配置变化
 watch(
   () => props.config,
-  () => {
-    // 配置变化时清空参数
-    paramsJson.value = ''
-    localValue.value = ''
-    jsonError.value = ''
+  (newConfig, oldConfig) => {
+    // 只有在配置真正变化时才清空数据
+    if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
+      // 如果没有外部传入的值,才清空数据
+      if (!localValue.value) {
+        paramsJson.value = ''
+        jsonError.value = ''
+      }
+    }
   }
 )
 </script>

+ 15 - 4
src/views/iot/rule/scene/form/selectors/PropertySelector.vue

@@ -288,7 +288,9 @@ const handleChange = (value: string) => {
   }
 }
 
-// 获取物模型TSL数据
+/**
+ * 获取物模型TSL数据
+ */
 const getThingModelTSL = async () => {
   if (!props.productId) {
     thingModelTSL.value = null
@@ -298,8 +300,15 @@ const getThingModelTSL = async () => {
 
   loading.value = true
   try {
-    thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
-    parseThingModelData()
+    const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
+
+    if (tslData) {
+      thingModelTSL.value = tslData
+      parseThingModelData()
+    } else {
+      // 如果TSL获取失败,尝试获取物模型列表
+      await getThingModelList()
+    }
   } catch (error) {
     console.error('获取物模型TSL失败:', error)
     // 如果TSL获取失败,尝试获取物模型列表
@@ -309,7 +318,9 @@ const getThingModelTSL = async () => {
   }
 }
 
-// 获取物模型列表(备用方案)
+/**
+ * 获取物模型列表(备用方案)
+ */
 const getThingModelList = async () => {
   if (!props.productId) {
     propertyList.value = []

+ 10 - 0
src/views/iot/utils/constants.ts

@@ -109,9 +109,19 @@ export const IoTThingModelAccessModeEnum = {
   READ_ONLY: {
     label: '只读',
     value: 'r'
+  },
+  WRITE_ONLY: {
+    label: '只写',
+    value: 'w'
   }
 } as const
 
+/** 获取访问模式标签 */
+export const getAccessModeLabel = (value: string): string => {
+  const mode = Object.values(IoTThingModelAccessModeEnum).find((mode) => mode.value === value)
+  return mode?.label || value
+}
+
 /** 属性值的数据类型 */
 export const IoTDataSpecsDataTypeEnum = {
   INT: 'int',