perf:【IoT 物联网】场景联动重构优化

This commit is contained in:
puhui999
2025-07-17 21:54:40 +08:00
parent 1b6ba33921
commit 7f4d3d72f8
11 changed files with 29 additions and 1962 deletions

View File

@@ -1,192 +0,0 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="场景名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入场景名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="场景状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择场景状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:rule-scene:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<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>
</el-table-column>
<el-table-column label="触发器" align="center" prop="triggers">
<template #default="{ row }"> {{ row.triggers?.length }} </template>
</el-table-column>
<el-table-column label="执行器" align="center" prop="actions">
<template #default="{ row }"> {{ row.actions?.length }} </template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:rule-scene:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:rule-scene:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<RuleSceneForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import RuleSceneForm from './RuleSceneForm.vue'
import { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
/** IoT 场景联动 列表 */
defineOptions({ name: 'IotRuleScene' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<IotRuleScene[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
description: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await RuleSceneApi.getRuleScenePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await RuleSceneApi.deleteRuleScene(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -1,224 +0,0 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1080px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row>
<el-col :span="12">
<el-form-item label="场景名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入场景名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场景状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="场景描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入场景描述" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-divider content-position="left">触发器配置</el-divider>
<!-- 根据触发类型选择不同的监听器组件 -->
<template v-for="(trigger, index) in formData.triggers" :key="trigger.key">
<!-- 设备状态变更和定时触发使用简化的监听器 -->
<device-state-listener
v-if="
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE ||
trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
"
:model-value="trigger"
@update:model-value="(val) => (formData.triggers[index] = val)"
class="mb-10px"
>
<el-button type="danger" round size="small" @click="removeTrigger(index)">
<Icon icon="ep:delete" />
</el-button>
</device-state-listener>
<!-- 其他设备触发类型使用完整的监听器 -->
<device-listener
v-else
:model-value="trigger"
@update:model-value="(val) => (formData.triggers[index] = val)"
class="mb-10px"
>
<el-button type="danger" round size="small" @click="removeTrigger(index)">
<Icon icon="ep:delete" />
</el-button>
</device-listener>
</template>
<el-button class="ml-10px!" type="primary" size="small" @click="addTrigger">
添加触发器
</el-button>
</el-col>
<el-col :span="24">
<el-divider content-position="left">执行器配置</el-divider>
<action-executor
v-for="(action, index) in formData.actions"
:key="action.key"
:model-value="action"
@update:model-value="(val) => (formData.actions[index] = val)"
class="mb-10px"
>
<el-button type="danger" round size="small" @click="removeAction(index)">
<Icon icon="ep:delete" />
</el-button>
</action-executor>
<el-button class="ml-10px!" type="primary" size="small" @click="addAction">
添加执行器
</el-button>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import DeviceListener from './components/listener/DeviceListener.vue'
import DeviceStateListener from './components/listener/DeviceStateListener.vue'
import { CommonStatusEnum } from '@/utils/constants'
import {
ActionConfig,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleScene,
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerTypeEnum,
TriggerConfig
} from '@/api/iot/rule/scene/scene.types'
import ActionExecutor from './components/action/ActionExecutor.vue'
import { generateUUID } from '@/utils'
/** IoT 场景联动表单 */
defineOptions({ name: 'IotRuleSceneForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<IotRuleScene>({
status: CommonStatusEnum.ENABLE,
triggers: [] as TriggerConfig[],
actions: [] as ActionConfig[]
} as IotRuleScene)
const formRules = reactive({
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '场景状态不能为空', trigger: 'blur' }],
triggers: [{ required: true, message: '触发器数组不能为空', trigger: 'blur' }],
actions: [{ required: true, message: '执行器数组不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 添加触发器 */
const addTrigger = () => {
formData.value.triggers.push({
key: generateUUID(), // 解决组件索引重用
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, // 默认为物模型属性上报
productKey: '',
deviceNames: [],
conditions: [
{
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
parameters: []
}
]
})
}
/** 移除触发器 */
const removeTrigger = (index: number) => {
formData.value.triggers.splice(index, 1)
}
/** 添加执行器 */
const addAction = () => {
formData.value.actions.push({
key: generateUUID(), // 解决组件索引重用
type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
} as ActionConfig)
}
/** 移除执行器 */
const removeAction = (index: number) => {
formData.value.actions.splice(index, 1)
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const data = (await RuleSceneApi.getRuleScene(id)) as IotRuleScene
// 解决组件索引重用
data.triggers?.forEach((item) => (item.key = generateUUID()))
data.actions?.forEach((item) => (item.key = generateUUID()))
formData.value = data
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as IotRuleScene
if (formType.value === 'create') {
await RuleSceneApi.createRuleScene(data)
message.success(t('common.createSuccess'))
} else {
await RuleSceneApi.updateRuleScene(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
status: CommonStatusEnum.ENABLE,
triggers: [] as TriggerConfig[],
actions: [] as ActionConfig[]
} as IotRuleScene
formRef.value?.resetFields()
}
</script>

View File

@@ -19,22 +19,13 @@
class="form-container"
>
<!-- 基础信息配置 -->
<BasicInfoSection
v-model="formData"
:rules="formRules"
/>
<BasicInfoSection v-model="formData" :rules="formRules" />
<!-- 触发器配置 -->
<TriggerSection
v-model:triggers="formData.triggers"
@validate="handleTriggerValidate"
/>
<TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
<!-- 执行器配置 -->
<ActionSection
v-model:actions="formData.actions"
@validate="handleActionValidate"
/>
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
<!-- 预览区域 -->
<PreviewSection
@@ -71,17 +62,8 @@ import ActionSection from './sections/ActionSection.vue'
import PreviewSection from './sections/PreviewSection.vue'
import { RuleSceneFormData, IotRuleScene } from '@/api/iot/rule/scene/scene.types'
import { getBaseValidationRules } from '../utils/validation'
import {
transformFormToApi,
transformApiToForm,
createDefaultFormData
} from '../utils/transform'
import {
handleValidationError,
handleNetworkError,
showSuccess,
withErrorHandling
} from '../utils/errorHandler'
import { transformFormToApi, transformApiToForm, createDefaultFormData } from '../utils/transform'
import { handleValidationError, showSuccess, withErrorHandling } from '../utils/errorHandler'
/** IoT场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' })
@@ -114,14 +96,16 @@ const actionValidation = ref({ valid: true, message: '' })
// 计算属性
const isEdit = computed(() => !!props.ruleScene?.id)
const drawerTitle = computed(() => isEdit.value ? '编辑场景联动规则' : '新增场景联动规则')
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
const canSubmit = computed(() => {
return formData.value.name &&
formData.value.triggers.length > 0 &&
formData.value.actions.length > 0 &&
triggerValidation.value.valid &&
actionValidation.value.valid
return (
formData.value.name &&
formData.value.triggers.length > 0 &&
formData.value.actions.length > 0 &&
triggerValidation.value.valid &&
actionValidation.value.valid
)
})
// 事件处理
@@ -136,15 +120,15 @@ const handleActionValidate = (result: { valid: boolean; message: string }) => {
const handleValidate = async () => {
try {
await formRef.value?.validate()
if (!triggerValidation.value.valid) {
throw new Error(triggerValidation.value.message)
}
if (!actionValidation.value.valid) {
throw new Error(actionValidation.value.message)
}
validationResult.value = { valid: true, message: '验证通过' }
showSuccess('规则验证通过')
return true
@@ -164,16 +148,16 @@ const handleSubmit = async () => {
if (!isValid) {
throw new Error('表单验证失败')
}
// 转换数据格式
const apiData = transformFormToApi(formData.value)
// 这里应该调用API保存数据
console.log('提交数据:', apiData)
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
await new Promise((resolve) => setTimeout(resolve, 1000))
return apiData
},
{
@@ -184,7 +168,7 @@ const handleSubmit = async () => {
successMessage: isEdit.value ? '更新成功' : '创建成功'
}
)
if (result) {
emit('success')
handleClose()
@@ -216,11 +200,14 @@ watch(drawerVisible, (visible) => {
})
// 监听props变化
watch(() => props.ruleScene, () => {
if (drawerVisible.value) {
initFormData()
watch(
() => props.ruleScene,
() => {
if (drawerVisible.value) {
initFormData()
}
}
})
)
</script>
<style scoped>

View File

@@ -1,81 +0,0 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" :appendToBody="true" v-loading="loading">
<div class="flex h-600px">
<!-- 左侧物模型属性view 模式 -->
<div class="w-1/2 border-r border-gray-200 pr-2 overflow-auto">
<JsonEditor :model-value="thingModel" mode="view" height="600px" />
</div>
<!-- 右侧 JSON 编辑器code 模式 -->
<div class="w-1/2 pl-2 overflow-auto">
<JsonEditor v-model="editableModelTSL" mode="code" height="600px" @error="handleError" />
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave" :disabled="hasJsonError">保存</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'ThingModelDualView' })
const props = defineProps<{
modelValue: any // 物模型的值
thingModel: any[] // 物模型
}>()
const emits = defineEmits(['update:modelValue', 'change'])
const message = useMessage()
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('物模型编辑器') // 弹窗的标题
const editableModelTSL = ref([
{
identifier: '对应左侧 identifier 属性值',
value: '如果 identifier 是 int 类型则输入数字,具体查看产品物模型定义'
}
]) // 物模型数据
const hasJsonError = ref(false) // 是否有 JSON 格式错误
const loading = ref(false) // 加载状态
/** 打开弹窗 */
const open = () => {
try {
// 数据回显
if (props.modelValue) {
editableModelTSL.value = JSON.parse(props.modelValue)
}
} catch (e) {
message.error('物模型编辑器参数')
console.error(e)
} finally {
dialogVisible.value = true
// 重置状态
hasJsonError.value = false
}
}
defineExpose({ open }) // 暴露方法供父组件调用
/** 保存修改 */
const handleSave = async () => {
try {
await message.confirm('确定要保存物模型参数吗?')
emits('update:modelValue', JSON.stringify(editableModelTSL.value))
message.success('保存成功')
dialogVisible.value = false
} catch {}
}
/** 处理 JSON 编辑器错误的函数 */
const handleError = (errors: any) => {
if (isEmpty(errors)) {
hasJsonError.value = false
return
}
hasJsonError.value = true
}
</script>

View File

@@ -1,142 +0,0 @@
<template>
<div class="flex items-center">
<!-- 数值类型输入框 -->
<template v-if="isNumeric">
<el-input
v-model="value"
class="w-1/1!"
:placeholder="`请输入${dataSpecs.unitName ? dataSpecs.unitName : '数值'}`"
>
<template #append> {{ dataSpecs.unit }} </template>
</el-input>
</template>
<!-- 布尔类型使用开关 -->
<template v-else-if="isBool">
<el-switch
v-model="value"
size="large"
:active-text="dataSpecsList[1].name"
:active-value="dataSpecsList[1].value"
:inactive-text="dataSpecsList[0].name"
:inactive-value="dataSpecsList[0].value"
/>
</template>
<!-- 枚举类型使用下拉选择 -->
<template v-else-if="isEnum">
<el-select class="w-1/1!" v-model="value">
<el-option
v-for="(item, index) in dataSpecsList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</template>
<!-- 时间类型使用时间选择器 -->
<template v-else-if="isDate">
<el-date-picker
class="w-1/1!"
v-model="value"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间"
/>
</template>
<!-- 文本类型使用文本输入框 -->
<template v-else-if="isText">
<el-input
class="w-1/1!"
v-model="value"
:maxlength="dataSpecs?.length"
:show-word-limit="true"
placeholder="请输入文本"
/>
</template>
<!-- arraystruct 直接输入 -->
<template v-else>
<el-input class="w-1/1!" :model-value="value" disabled placeholder="请输入值">
<template #append>
<el-button type="primary" @click="openJsonEditor">编辑</el-button>
</template>
</el-input>
<!-- arraystruct 类型数据编辑 -->
<ThingModelDualView
ref="thingModelDualViewRef"
v-model="value"
:thing-model="dataSpecsList"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useVModel } from '@vueuse/core'
import ThingModelDualView from './ThingModelDualView.vue'
import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
/** 物模型属性参数输入组件 */
defineOptions({ name: 'ThingModelParamInput' })
const props = defineProps<{
modelValue: any // 物模型的值
thingModel: any // 物模型
}>()
const emits = defineEmits(['update:modelValue', 'change'])
const value = useVModel(props, 'modelValue', emits)
const thingModelDualViewRef = ref<InstanceType<typeof ThingModelDualView>>()
const openJsonEditor = () => {
thingModelDualViewRef.value?.open()
}
/** 计算属性:判断数据类型 */
const isNumeric = computed(() =>
[
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.FLOAT,
IoTDataSpecsDataTypeEnum.DOUBLE
].includes(props.thingModel?.dataType as any)
)
const isBool = computed(() => props.thingModel?.dataType === IoTDataSpecsDataTypeEnum.BOOL)
const isEnum = computed(() => props.thingModel?.dataType === IoTDataSpecsDataTypeEnum.ENUM)
const isDate = computed(() => props.thingModel?.dataType === IoTDataSpecsDataTypeEnum.DATE)
const isText = computed(() => props.thingModel?.dataType === IoTDataSpecsDataTypeEnum.TEXT)
/** 获取数据规格 */
const dataSpecs = computed(() => {
if (isNumeric.value || isDate.value || isText.value) {
return props.thingModel?.dataSpecs || {}
}
return {}
})
const dataSpecsList = computed(() => {
if (
isBool.value ||
isEnum.value ||
[IoTDataSpecsDataTypeEnum.ARRAY, IoTDataSpecsDataTypeEnum.STRUCT].includes(
props.thingModel?.dataType
)
) {
return props.thingModel?.dataSpecsList || []
}
return []
})
/** 物模型切换重置值 */
watch(
() => props.thingModel?.dataType,
(_, oldValue) => {
if (!oldValue) {
return
}
value.value = undefined
},
{ deep: true }
)
</script>

View File

@@ -1,307 +0,0 @@
<template>
<div>
<div class="m-10px">
<!-- 产品设备回显区域 -->
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">执行动作</span>
<el-select
v-model="actionConfig.type"
class="!w-240px"
clearable
placeholder="请选择执行类型"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_ACTION_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div v-if="isDeviceAction" class="flex items-center mr-60px">
<span class="mr-10px">产品</span>
<el-button type="primary" @click="handleSelectProduct" size="small" plain>
{{ product ? product.name : '选择产品' }}
</el-button>
</div>
<!-- TODO @puhui999单选设备默认不选就是全部设备 -->
<div v-if="isDeviceAction" class="flex items-center mr-60px">
<span class="mr-10px">设备</span>
<el-button type="primary" @click="handleSelectDevice" size="small" plain>
{{ isEmpty(deviceList) ? '选择设备' : deviceList.map((d) => d.deviceName).join(',') }}
</el-button>
</div>
<!-- 删除执行器 -->
<div class="absolute top-auto right-16px bottom-auto">
<el-tooltip content="删除执行器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 设备控制执行器 -->
<!-- TODO @puhui999服务调用时选择好某个物模型剩余直接填写一个 JSON 不用逐个添加~可以试试设备详情-设备调试有服务调用的模拟 -->
<DeviceControlAction
v-if="isDeviceAction"
:action-type="actionConfig.type"
:model-value="actionConfig.deviceControl"
:product-id="product?.id"
:product-key="product?.productKey"
@update:model-value="(val) => (actionConfig.deviceControl = val)"
/>
<!-- 告警执行器 -->
<div v-else-if="isAlertAction">
<!-- 告警触发 - 无需额外配置 -->
<div
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
class="bg-[#dbe5f6] flex items-center justify-center p-10px"
>
<el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
<span class="text-gray-600">触发告警通知,系统将自动发送告警信息</span>
</div>
<!-- 告警恢复 - 需要选择告警配置 -->
<div v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER">
<div class="bg-[#dbe5f6] flex items-center justify-center p-10px mb-10px">
<el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
<!-- TODO @puhui999这种类型的提示感觉放在 SELECT 那,后面有个所有的提示,会不会更好呀;因为可以少占用行呢; -->
<span class="text-gray-600">恢复指定的告警配置状态</span>
</div>
<div class="p-10px">
<el-form-item label="选择告警配置" required label-width="110">
<el-select
v-model="actionConfig.alertConfigId"
class="!w-240px"
clearable
placeholder="请选择要恢复的告警配置"
:loading="alertConfigLoading"
>
<el-option
v-for="config in alertConfigList"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item>
</div>
</div>
</div>
</div>
<!-- 产品、设备的选择 -->
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<DeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
import DeviceControlAction from './DeviceControlAction.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
import {
ActionConfig,
ActionDeviceControl,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneActionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 场景联动之执行器组件 */
defineOptions({ name: 'ActionExecutor' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const actionConfig = useVModel(props, 'modelValue', emits) as Ref<ActionConfig>
const message = useMessage()
/** 计算属性:判断是否为设备相关执行类型 */
const isDeviceAction = computed(() => {
return [
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
].includes(actionConfig.value.type as any)
})
/** 计算属性:判断是否为告警相关执行类型 */
const isAlertAction = computed(() => {
return [
IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
IotRuleSceneActionTypeEnum.ALERT_RECOVER
].includes(actionConfig.value.type as any)
})
/** 初始化执行器结构 */
const initActionConfig = () => {
if (!actionConfig.value) {
actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET } as ActionConfig
}
// 设备控制执行器初始化
if (isDeviceAction.value && !actionConfig.value.deviceControl) {
actionConfig.value.deviceControl = {
productKey: '',
deviceNames: [],
type:
actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
? IotDeviceMessageTypeEnum.PROPERTY
: IotDeviceMessageTypeEnum.SERVICE,
identifier:
actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
? IotDeviceMessageIdentifierEnum.PROPERTY_SET
: IotDeviceMessageIdentifierEnum.SERVICE_INVOKE,
data: {}
} as ActionDeviceControl
}
// 告警执行器初始化
if (isAlertAction.value) {
if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER) {
// 告警触发 - 无需额外配置
actionConfig.value.alertConfigId = undefined
} else if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
// 告警恢复 - 需要选择告警配置
if (!actionConfig.value.alertConfigId) {
actionConfig.value.alertConfigId = undefined
}
}
}
}
/** 产品和设备选择 */
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
const product = ref<ProductVO>()
const deviceList = ref<DeviceVO[]>([])
/** 告警配置相关 */
const alertConfigList = ref<AlertConfig[]>([])
const alertConfigLoading = ref(false)
/** 处理选择产品 */
const handleSelectProduct = () => {
productTableSelectRef.value?.open()
}
/** 处理选择设备 */
const handleSelectDevice = () => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
deviceTableSelectRef.value?.open()
}
/** 处理产品选择成功 */
const handleProductSelect = (val: ProductVO) => {
product.value = val
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.productKey = val.productKey
}
// 重置设备选择
deviceList.value = []
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.deviceNames = []
}
}
/** 处理设备选择成功 */
const handleDeviceSelect = (val: DeviceVO[]) => {
deviceList.value = val
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.deviceNames = val.map((item) => item.deviceName)
}
}
/** 获取告警配置列表 */
const getAlertConfigList = async () => {
try {
alertConfigLoading.value = true
alertConfigList.value = await AlertConfigApi.getSimpleAlertConfigList()
} catch (error) {
console.error('获取告警配置列表失败:', error)
} finally {
alertConfigLoading.value = false
}
}
/** 监听执行类型变化,初始化对应配置 */
watch(
() => actionConfig.value.type,
(newType) => {
initActionConfig()
// 如果是告警恢复类型,需要加载告警配置列表
if (newType === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
getAlertConfigList()
}
},
{ immediate: true }
)
/** 初始化产品回显信息 */
const initProductInfo = async () => {
if (!actionConfig.value.deviceControl?.productKey) {
return
}
try {
const productData = await ProductApi.getProductByKey(
actionConfig.value.deviceControl.productKey
)
if (productData) {
product.value = productData
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
/**
* 初始化设备回显信息
*/
const initDeviceInfo = async () => {
if (
!actionConfig.value.deviceControl?.productKey ||
!actionConfig.value.deviceControl?.deviceNames?.length
) {
return
}
try {
const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
actionConfig.value.deviceControl.productKey,
actionConfig.value.deviceControl.deviceNames
)
if (deviceData && deviceData.length > 0) {
deviceList.value = deviceData
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
/** 初始化 */
onMounted(async () => {
initActionConfig()
// 初始化产品和设备回显
if (actionConfig.value.deviceControl) {
await initProductInfo()
await initDeviceInfo()
}
})
</script>

View File

@@ -1,248 +0,0 @@
<template>
<div class="bg-[#dbe5f6] flex p-10px">
<div class="">
<div
class="flex items-center justify-around mb-10px last:mb-0"
v-for="(parameter, index) in parameters"
:key="index"
>
<!-- 选择服务 -->
<el-select
v-if="IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.type"
v-model="parameter.identifier0"
class="!w-240px mr-10px"
clearable
placeholder="请选择服务"
>
<el-option
v-for="thingModel in getThingModelTSLServices"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<el-select
v-model="parameter.identifier"
class="!w-240px mr-10px"
clearable
placeholder="请选择物模型"
>
<el-option
v-for="thingModel in thingModels(parameter?.identifier0)"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<ThingModelParamInput
class="!w-240px mr-10px"
v-model="parameter.value"
:thing-model="
thingModels(parameter?.identifier0)?.find(
(item) => item.identifier === parameter.identifier
)
"
/>
<el-tooltip content="删除参数" placement="top">
<el-button type="danger" circle size="small" @click="removeParameter(index)">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</div>
</div>
<!-- 添加参数 -->
<div class="flex flex-1 flex-col items-center justify-center w-60px h-a">
<el-tooltip content="添加参数" placement="top">
<el-button type="primary" circle size="small" @click="addParameter">
<Icon icon="ep:plus" />
</el-button>
</el-tooltip>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { ThingModelApi } from '@/api/iot/thingmodel'
import {
ActionDeviceControl,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneActionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
import ThingModelParamInput from '../ThingModelParamInput.vue'
/** 设备控制执行器组件 */
defineOptions({ name: 'DeviceControlAction' })
const props = defineProps<{
modelValue: any
actionType: number
productId?: number
productKey?: string
}>()
const emits = defineEmits(['update:modelValue'])
const deviceControlConfig = useVModel(props, 'modelValue', emits) as Ref<ActionDeviceControl>
const message = useMessage()
/** 执行器参数 */
const parameters = ref<{ identifier: string; value: any; identifier0?: string }[]>([])
const addParameter = () => {
if (!props.productId) {
message.warning('请先选择一个产品')
return
}
parameters.value.push({ identifier: '', value: undefined })
}
const removeParameter = (index: number) => {
parameters.value.splice(index, 1)
}
watch(
() => parameters.value,
(newVal) => {
if (isEmpty(newVal)) {
return
}
for (const parameter of newVal) {
if (isEmpty(parameter.identifier)) {
break
}
// 单独处理服务的情况
if (IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.value.type) {
if (!parameter.identifier0) {
continue
}
deviceControlConfig.value.data[parameter.identifier0] = {
identifier: parameter.identifier,
value: parameter.value
}
continue
}
deviceControlConfig.value.data[parameter.identifier] = parameter.value
}
},
{ deep: true }
)
/** 初始化设备控制执行器结构 */
const initDeviceControlConfig = () => {
if (!deviceControlConfig.value) {
deviceControlConfig.value = {
productKey: '',
deviceNames: [],
type:
props.actionType === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
? IotDeviceMessageTypeEnum.PROPERTY
: IotDeviceMessageTypeEnum.SERVICE,
identifier:
props.actionType === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
? IotDeviceMessageIdentifierEnum.PROPERTY_SET
: IotDeviceMessageIdentifierEnum.SERVICE_INVOKE,
data: {}
} as ActionDeviceControl
} else {
// 单独处理服务的情况
if (IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.value.type) {
// 参数回显
parameters.value = Object.entries(deviceControlConfig.value.data).map(([key, value]) => ({
identifier0: key,
identifier: value.identifier,
value: value.value
}))
return
}
// 参数回显
parameters.value = Object.entries(deviceControlConfig.value.data).map(([key, value]) => ({
identifier: key,
value: value
}))
}
// 确保 data 对象存在
if (!deviceControlConfig.value.data) {
deviceControlConfig.value.data = {}
}
}
/** 获取产品物模型 */
const thingModelTSL = ref<any>()
const getThingModelTSL = async () => {
if (!props.productId) {
return
}
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
}
const thingModels = computed(() => (identifier?: string): any[] => {
if (isEmpty(thingModelTSL.value)) {
return []
}
switch (deviceControlConfig.value.type) {
case IotDeviceMessageTypeEnum.PROPERTY:
return thingModelTSL.value?.properties || []
case IotDeviceMessageTypeEnum.SERVICE:
const service = thingModelTSL.value.services?.find(
(item: any) => item.identifier === identifier
)
return service?.inputParams || []
}
return []
})
/** 获取物模型服务 */
const getThingModelTSLServices = computed(() => thingModelTSL.value?.services || [])
/** 监听 productId 变化 */
watch(
() => props.productId,
() => {
getThingModelTSL()
if (deviceControlConfig.value && deviceControlConfig.value.productKey === props.productKey) {
return
}
// 当产品ID变化时清空原有数据
deviceControlConfig.value.data = {}
parameters.value = []
}
)
/** 监听执行类型变化 */
watch(
() => props.actionType,
(val: any) => {
if (!val) {
return
}
// 切换执行类型时清空参数
deviceControlConfig.value.data = {}
parameters.value = []
if (val === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET) {
deviceControlConfig.value.type = IotDeviceMessageTypeEnum.PROPERTY
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.PROPERTY_SET
} else if (val === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
deviceControlConfig.value.type = IotDeviceMessageTypeEnum.SERVICE
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.SERVICE_INVOKE
}
}
)
/** 监听消息类型变化 */
watch(
() => deviceControlConfig.value.type,
() => {
// 切换消息类型时清空参数
deviceControlConfig.value.data = {}
parameters.value = []
if (deviceControlConfig.value.type === IotDeviceMessageTypeEnum.PROPERTY) {
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.PROPERTY_SET
} else if (deviceControlConfig.value.type === IotDeviceMessageTypeEnum.SERVICE) {
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.SERVICE_INVOKE
}
}
)
// 初始化
onMounted(() => {
initDeviceControlConfig()
})
</script>

View File

@@ -1,106 +0,0 @@
<template>
<el-select v-model="selectedOperator" class="w-1/1" clearable :placeholder="placeholder">
<!-- 根据属性类型展示不同的可选条件 -->
<el-option
v-for="(item, key) in filteredOperators"
:key="key"
:label="item.name"
:value="item.value"
/>
</el-select>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { IotRuleSceneTriggerConditionParameterOperatorEnum } from '@/api/iot/rule/scene/scene.types'
/** 条件选择器 */
defineOptions({ name: 'ConditionSelector' })
const props = defineProps({
placeholder: {
type: String,
default: '请选择条件'
},
modelValue: {
type: String,
default: ''
},
dataType: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const selectedOperator = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 根据数据类型过滤可用的操作符
const filteredOperators = computed(() => {
// 如果没有指定数据类型,返回所有操作符
if (!props.dataType) {
return IotRuleSceneTriggerConditionParameterOperatorEnum
}
const operatorMap = new Map()
// 添加通用的操作符(所有类型都有非空操作符)
operatorMap.set('NOT_NULL', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL)
// 根据数据类型添加特定的操作符
switch (props.dataType) {
case 'int':
case 'float':
case 'double':
// 数值类型支持的所有操作符
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('GREATER_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN)
operatorMap.set('GREATER_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS)
operatorMap.set('LESS_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN)
operatorMap.set('LESS_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS)
operatorMap.set('IN', IotRuleSceneTriggerConditionParameterOperatorEnum.IN)
operatorMap.set('NOT_IN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN)
operatorMap.set('BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN)
operatorMap.set('NOT_BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)
break
case 'enum':
// 枚举类型支持的操作符
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('IN', IotRuleSceneTriggerConditionParameterOperatorEnum.IN)
operatorMap.set('NOT_IN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN)
break
case 'bool':
// 布尔类型支持的操作符
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
break
case 'text':
// 文本类型支持的操作符
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('LIKE', IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE)
break
case 'date':
// 日期类型支持的操作符
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('GREATER_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN)
operatorMap.set('GREATER_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS)
operatorMap.set('LESS_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN)
operatorMap.set('LESS_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS)
operatorMap.set('BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN)
operatorMap.set('NOT_BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)
break
// struct 和 array 类型只支持非空操作符,已在通用部分添加
default:
return IotRuleSceneTriggerConditionParameterOperatorEnum
}
return Object.fromEntries(operatorMap)
})
</script>

View File

@@ -1,367 +0,0 @@
<template>
<div>
<div class="m-10px">
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">触发条件</span>
<el-select
v-model="triggerConfig.type"
class="!w-240px"
clearable
placeholder="请选择触发条件"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_TRIGGER_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div v-if="isDeviceTrigger" class="flex items-center mr-60px">
<span class="mr-10px">产品</span>
<el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
{{ product ? product.name : '选择产品' }}
</el-button>
</div>
<!-- TODO @puhui999只允许选择一个或者全部设备 -->
<div v-if="isDeviceTrigger" class="flex items-center mr-60px">
<span class="mr-10px">设备</span>
<el-button type="primary" @click="openDeviceSelect" size="small" plain>
{{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
</el-button>
</div>
<!-- 删除触发器 -->
<div class="absolute top-auto right-16px bottom-auto">
<el-tooltip content="删除触发器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 设备触发器条件 -->
<template v-if="isDeviceTrigger">
<!-- 设备上下线变更 - 无需额外配置 -->
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE"
class="bg-[#dbe5f6] flex items-center justify-center p-10px"
>
<span class="text-gray-600">设备上下线状态变更时触发无需额外配置</span>
</div>
<!-- 物模型属性上报设备事件上报设备服务调用 - 需要配置条件 -->
<div
v-else
class="bg-[#dbe5f6] flex p-10px"
v-for="(condition, index) in triggerConfig.conditions"
:key="index"
>
<div class="w-70%">
<DeviceListenerCondition
v-for="(parameter, index2) in condition.parameters"
:key="index2"
:model-value="parameter"
:condition-type="condition.type"
:thingModels="thingModels(condition)"
@update:model-value="(val) => (condition.parameters[index2] = val)"
class="mb-10px last:mb-0"
>
<el-tooltip content="删除参数" placement="top">
<el-button
type="danger"
circle
size="small"
@click="removeConditionParameter(condition.parameters, index2)"
>
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</DeviceListenerCondition>
</div>
<!-- 添加参数 -->
<div class="flex flex-1 flex-col items-center justify-center w-60px h-a">
<el-tooltip content="添加参数" placement="top">
<el-button
type="primary"
circle
size="small"
@click="addConditionParameter(condition.parameters)"
>
<Icon icon="ep:plus" />
</el-button>
</el-tooltip>
</div>
<!-- 删除条件 -->
<div
class="device-listener-condition flex flex-1 flex-col items-center justify-center w-a h-a"
>
<el-tooltip content="删除条件" placement="top">
<el-button type="danger" size="small" @click="removeCondition(index)">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</div>
</div>
</template>
<!-- 定时触发 -->
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.TIMER"
class="bg-[#dbe5f6] flex items-center justify-between p-10px"
>
<span class="w-120px">CRON 表达式</span>
<crontab v-model="triggerConfig.cronExpression" />
</div>
<!-- 除了设备上下线变更,其他设备触发类型都可以设置多个触发条件 -->
<!-- TODO @puhui999触发有点不太对可以在用下阿里云的呢~ -->
<el-text
v-if="
isDeviceTrigger && triggerConfig.type !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
"
class="ml-10px!"
type="primary"
@click="addCondition"
>
添加触发条件
</el-text>
</div>
<!-- 产品、设备的选择 -->
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<DeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import DeviceListenerCondition from './DeviceListenerCondition.vue'
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { ThingModelApi } from '@/api/iot/thingmodel'
import {
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneTriggerTypeEnum,
TriggerCondition,
TriggerConditionParameter,
TriggerConfig
} from '@/api/iot/rule/scene/scene.types'
import { Crontab } from '@/components/Crontab'
/** 场景联动之监听器组件 */
defineOptions({ name: 'DeviceListener' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<TriggerConfig>
const message = useMessage()
/** 计算属性:判断是否为设备触发类型 */
const isDeviceTrigger = computed(() => {
return [
IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
].includes(triggerConfig.value.type as any)
})
/** 添加触发条件 */
const addCondition = () => {
// 根据触发类型设置默认的条件类型
let defaultConditionType: string = IotDeviceMessageTypeEnum.PROPERTY
switch (triggerConfig.value.type) {
case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
defaultConditionType = IotDeviceMessageTypeEnum.PROPERTY
break
case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
defaultConditionType = IotDeviceMessageTypeEnum.EVENT
break
case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
defaultConditionType = IotDeviceMessageTypeEnum.SERVICE
break
}
// 添加触发条件
triggerConfig.value.conditions?.push({
type: defaultConditionType,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
parameters: []
})
}
/** 移除触发条件 */
const removeCondition = (index: number) => {
triggerConfig.value.conditions?.splice(index, 1)
}
/** 添加参数 */
const addConditionParameter = (conditionParameters: TriggerConditionParameter[]) => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
if (conditionParameters.length >= 1) {
message.warning('只允许添加一个参数')
return
}
conditionParameters.push({} as TriggerConditionParameter)
}
/** 移除参数 */
const removeConditionParameter = (
conditionParameters: TriggerConditionParameter[],
index: number
) => {
conditionParameters.splice(index, 1)
}
/** 产品和设备选择引用 */
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
const product = ref<ProductVO>()
const deviceList = ref<DeviceVO[]>([])
/** 处理产品选择 */
const handleProductSelect = (val: ProductVO) => {
product.value = val
triggerConfig.value.productKey = val.productKey
deviceList.value = []
getThingModelTSL()
}
/** 处理设备选择 */
const handleDeviceSelect = (val: DeviceVO[]) => {
deviceList.value = val
triggerConfig.value.deviceNames = val.map((item) => item.deviceName)
}
/** 打开设备选择器 */
const openDeviceSelect = () => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
deviceTableSelectRef.value?.open()
}
/**
* 初始化产品回显信息
*/
const initProductInfo = async () => {
if (!triggerConfig.value.productKey) {
return
}
try {
// 使用新的API直接通过productKey获取产品信息
const productData = await ProductApi.getProductByKey(triggerConfig.value.productKey)
if (productData) {
product.value = productData
// 加载物模型数据
await getThingModelTSL()
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
/**
* 初始化设备回显信息
*/
const initDeviceInfo = async () => {
if (!triggerConfig.value.productKey || !triggerConfig.value.deviceNames?.length) {
return
}
try {
// 使用新的API直接通过productKey和deviceNames获取设备列表
const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
triggerConfig.value.productKey,
triggerConfig.value.deviceNames
)
if (deviceData && deviceData.length > 0) {
deviceList.value = deviceData
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
/** 获取产品物模型 */
const thingModelTSL = ref<any>()
const thingModels = computed(() => (condition: TriggerCondition) => {
if (isEmpty(thingModelTSL.value)) {
return []
}
switch (condition.type) {
case IotDeviceMessageTypeEnum.PROPERTY:
return thingModelTSL.value?.properties || []
case IotDeviceMessageTypeEnum.SERVICE:
return thingModelTSL.value?.services || []
case IotDeviceMessageTypeEnum.EVENT:
return thingModelTSL.value?.events || []
}
return []
})
const getThingModelTSL = async () => {
if (!product.value) {
return
}
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
}
/** 监听触发类型变化,自动设置条件类型 */
watch(
() => triggerConfig.value.type,
(newType) => {
if (!newType || newType === IotRuleSceneTriggerTypeEnum.TIMER) {
return
}
// 设备上下线变更不需要条件配置
if (newType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
triggerConfig.value.conditions = []
return
}
// 为其他设备触发类型设置默认条件
if (triggerConfig.value.conditions && triggerConfig.value.conditions.length > 0) {
triggerConfig.value.conditions.forEach((condition) => {
switch (newType) {
case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
condition.type = IotDeviceMessageTypeEnum.PROPERTY
break
case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
condition.type = IotDeviceMessageTypeEnum.EVENT
break
case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
condition.type = IotDeviceMessageTypeEnum.SERVICE
break
}
})
}
}
)
/** 初始化 */
onMounted(async () => {
// 初始化产品和设备回显
if (triggerConfig.value) {
// 初始化conditions数组如果不存在且不是设备上下线变更类型
if (
!triggerConfig.value.conditions &&
triggerConfig.value.type !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
) {
triggerConfig.value.conditions = []
}
await initProductInfo()
await initDeviceInfo()
}
})
</script>

View File

@@ -1,87 +0,0 @@
<template>
<div class="flex items-center w-1/1">
<!-- 选择服务 -->
<el-select
v-if="
[IotDeviceMessageTypeEnum.SERVICE, IotDeviceMessageTypeEnum.EVENT].includes(conditionType)
"
v-model="conditionParameter.identifier0"
class="!w-150px mr-10px"
clearable
placeholder="请选择服务"
>
<el-option
v-for="thingModel in thingModels"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<el-select
v-model="conditionParameter.identifier"
class="!w-150px mr-10px"
clearable
placeholder="请选择物模型"
>
<el-option
v-for="thingModel in getThingModels"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<ConditionSelector
v-model="conditionParameter.operator"
:data-type="model?.dataType"
class="!w-150px mr-10px"
/>
<ThingModelParamInput
v-if="
conditionParameter.operator !==
IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value
"
class="!w-200px mr-10px"
v-model="conditionParameter.value"
:thing-model="model"
/>
<!-- 按钮插槽 -->
<slot></slot>
</div>
</template>
<script setup lang="ts">
import ConditionSelector from './ConditionSelector.vue'
import {
IotDeviceMessageTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
TriggerConditionParameter
} from '@/api/iot/rule/scene/scene.types'
import { useVModel } from '@vueuse/core'
import ThingModelParamInput from '@/views/iot/rule/scene/components/ThingModelParamInput.vue'
/** 设备触发条件 */
defineOptions({ name: 'DeviceListenerCondition' })
const props = defineProps<{ modelValue: any; conditionType: any; thingModels: any }>()
const emits = defineEmits(['update:modelValue'])
const conditionParameter = useVModel(props, 'modelValue', emits) as Ref<TriggerConditionParameter>
/** 属性就是 thingModels服务和事件取对应的 outputParams */
const getThingModels = computed(() => {
switch (props.conditionType) {
case IotDeviceMessageTypeEnum.PROPERTY:
return props.thingModels || []
case IotDeviceMessageTypeEnum.SERVICE:
case IotDeviceMessageTypeEnum.EVENT:
return (
props.thingModels.find(
(item: any) => item.identifier === conditionParameter.value.identifier0
)?.outputParams || []
)
}
})
/** 获得物模型属性、类型 */
const model = computed(() =>
getThingModels.value.find((item: any) => item.identifier === conditionParameter.value.identifier)
)
</script>

View File

@@ -1,166 +0,0 @@
<template>
<div>
<div class="m-10px">
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">触发条件</span>
<el-select
v-model="triggerConfig.type"
class="!w-240px"
clearable
placeholder="请选择触发条件"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_TRIGGER_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div class="flex items-center mr-60px">
<span class="mr-10px">产品</span>
<el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
{{ product ? product.name : '选择产品' }}
</el-button>
</div>
<div class="flex items-center mr-60px">
<span class="mr-10px">设备</span>
<el-button type="primary" @click="openDeviceSelect" size="small" plain>
{{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
</el-button>
</div>
<!-- 删除触发器 -->
<div class="absolute top-auto right-16px bottom-auto">
<el-tooltip content="删除触发器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 设备状态变更说明 -->
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE"
class="bg-[#dbe5f6] flex items-center justify-center p-10px"
>
<el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
<span class="text-gray-600">当选中的设备上线或下线时触发场景联动</span>
</div>
<!-- 定时触发 -->
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.TIMER"
class="bg-[#dbe5f6] flex items-center justify-between p-10px"
>
<span class="w-120px">CRON 表达式</span>
<crontab v-model="triggerConfig.cronExpression" />
</div>
</div>
<!-- 产品设备的选择 -->
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<DeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { IotRuleSceneTriggerTypeEnum, TriggerConfig } from '@/api/iot/rule/scene/scene.types'
import { Crontab } from '@/components/Crontab'
/** 设备状态监听器组件 */
defineOptions({ name: 'DeviceStateListener' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<TriggerConfig>
const message = useMessage()
/** 产品和设备选择引用 */
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
const product = ref<ProductVO>()
const deviceList = ref<DeviceVO[]>([])
/** 处理产品选择 */
const handleProductSelect = (val: ProductVO) => {
product.value = val
triggerConfig.value.productKey = val.productKey
deviceList.value = []
}
/** 处理设备选择 */
const handleDeviceSelect = (val: DeviceVO[]) => {
deviceList.value = val
triggerConfig.value.deviceNames = val.map((item) => item.deviceName)
}
/** 打开设备选择器 */
const openDeviceSelect = () => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
deviceTableSelectRef.value?.open()
}
/**
* 初始化产品回显信息
*/
const initProductInfo = async () => {
if (!triggerConfig.value.productKey) {
return
}
try {
const productData = await ProductApi.getProductByKey(triggerConfig.value.productKey)
if (productData) {
product.value = productData
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
/**
* 初始化设备回显信息
*/
const initDeviceInfo = async () => {
if (!triggerConfig.value.productKey || !triggerConfig.value.deviceNames?.length) {
return
}
try {
const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
triggerConfig.value.productKey,
triggerConfig.value.deviceNames
)
if (deviceData && deviceData.length > 0) {
deviceList.value = deviceData
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
/** 初始化 */
onMounted(async () => {
if (triggerConfig.value) {
await initProductInfo()
await initDeviceInfo()
}
})
</script>