diff --git a/src/api/iot/rule/scene/scene.types.ts b/src/api/iot/rule/scene/scene.types.ts index 9c0ee6f1f..d8ba01faa 100644 --- a/src/api/iot/rule/scene/scene.types.ts +++ b/src/api/iot/rule/scene/scene.types.ts @@ -175,7 +175,7 @@ interface Action { productId?: number // 产品编号 deviceId?: number // 设备编号 identifier?: string // 物模型标识符(服务调用时使用) - params?: Record // 请求参数 + params?: string // 请求参数 alertConfigId?: number // 告警配置编号 } diff --git a/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue b/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue index 0598c4540..9ab82e721 100644 --- a/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue +++ b/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([]) // 物模型属性列表 +const thingModelProperties = ref([]) // 物模型属性列表 const loadingThingModel = ref(false) // 物模型加载状态 const selectedService = ref(null) // 选中的服务对象 const serviceList = ref([]) // 服务列表 @@ -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) + +/** + * 初始化组件数据 + */ +const initializeComponent = async () => { + if (isInitialized.value) return + + const currentAction = action.value + if (!currentAction) return + + // 如果已经选择了产品且是属性设置类型,加载物模型 + if (currentAction.productId && isPropertySetAction.value) { + await loadThingModelProperties(currentAction.productId) + } + + // 如果是服务调用类型且已有标识符,初始化服务选择 + if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) { + // 加载物模型TSL以获取服务信息 + await loadServiceFromTSL(currentAction.productId, currentAction.identifier) + } + + isInitialized.value = true +} + /** * 组件初始化 */ onMounted(() => { - // 如果已经选择了产品且是属性设置类型,加载物模型 - if (action.value.productId && isPropertySetAction.value) { - loadThingModelProperties(action.value.productId) - } - - // 如果是服务调用类型且已有标识符,初始化服务选择 - if (action.value.productId && isServiceInvokeAction.value && action.value.identifier) { - // 加载物模型TSL以获取服务信息 - loadServiceFromTSL(action.value.productId, action.value.identifier) - } + 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 + } + } + } ) diff --git a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue index 3ef84848a..9bdf89134 100644 --- a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue +++ b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue @@ -59,8 +59,14 @@ - - + + - + @@ -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 diff --git a/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue b/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue index 8abff7914..ac94cb096 100644 --- a/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue +++ b/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 = '' + } + } } ) diff --git a/src/views/iot/rule/scene/form/selectors/PropertySelector.vue b/src/views/iot/rule/scene/form/selectors/PropertySelector.vue index 5012c77e6..c8d237a4d 100644 --- a/src/views/iot/rule/scene/form/selectors/PropertySelector.vue +++ b/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 = [] diff --git a/src/views/iot/utils/constants.ts b/src/views/iot/utils/constants.ts index f33f398fc..c5412c0ef 100644 --- a/src/views/iot/utils/constants.ts +++ b/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',