!746 【功能新增】IoT: 规则场景联动

Merge pull request !746 from puhui999/feature/iot
This commit is contained in:
芋道源码
2025-03-23 00:11:17 +00:00
committed by Gitee
13 changed files with 2867 additions and 1315 deletions

2692
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
import request from '@/config/axios'
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
// IoT 规则场景(场景联动) VO
export interface RuleSceneVO {
id?: number // 场景编号
name: string // 场景名称
description?: string // 场景描述
status: number // 场景状态
triggers: IotRuleSceneTriggerConfig[] // 触发器数组
actions?: any[] // 执行器数组
}
// IoT 规则场景(场景联动) API
export const RuleSceneApi = {
// 查询规则场景(场景联动)分页
getRuleScenePage: async (params: any) => {
return await request.get({ url: `/iot/rule-scene/page`, params })
},
// 查询规则场景(场景联动)详情
getRuleScene: async (id: number) => {
return await request.get({ url: `/iot/rule-scene/get?id=` + id })
},
// 新增规则场景(场景联动)
createRuleScene: async (data: RuleSceneVO) => {
return await request.post({ url: `/iot/rule-scene/create`, data })
},
// 修改规则场景(场景联动)
updateRuleScene: async (data: RuleSceneVO) => {
return await request.put({ url: `/iot/rule-scene/update`, data })
},
// 删除规则场景(场景联动)
deleteRuleScene: async (id: number) => {
return await request.delete({ url: `/iot/rule-scene/delete?id=` + id })
}
}

View File

@@ -0,0 +1,222 @@
/**
* 场景规则触发器配置
*/
export interface IotRuleSceneTriggerConfig {
/**
* 触发类型
* - 1: 设备触发
* - 2: 定时触发
*/
type: number
/** 产品标识 */
productKey: string
/** 设备名称数组 */
deviceNames: string[]
/** 触发条件数组。条件之间是"或"的关系 */
conditions: IotRuleSceneTriggerCondition[]
/** CRON 表达式。当 type = 2 时必填 */
cronExpression?: string
}
/**
* 触发条件
*/
export interface IotRuleSceneTriggerCondition {
/**
* 消息类型
* - property: 属性上报
* - event: 事件上报
*/
type: string
/** 消息标识符 */
identifier?: string
/** 参数数组。参数之间是"或"的关系 */
parameters: IotRuleSceneTriggerConditionParameter[]
}
/**
* 触发条件参数
*/
export interface IotRuleSceneTriggerConditionParameter {
/** 标识符(属性、事件、服务) */
identifier: string
/**
* 操作符
*/
operator: string
/**
* 比较值
* 如果有多个值,则使用 "," 分隔,类似 "1,2,3"
*/
value: string
}
/**
* 执行器配置
*/
export interface IotRuleSceneActionConfig {
/**
* 执行类型
* - 1: 设备控制
* - 2: 数据桥接
*/
type: number
/** 设备控制配置。当 type = 1 时必填 */
deviceControl?: IotRuleSceneActionDeviceControl
/** 数据桥接编号。当 type = 2 时必填 */
dataBridgeId?: number
}
/**
* 执行设备控制
*/
export interface IotRuleSceneActionDeviceControl {
/** 产品标识 */
productKey: string
/** 设备名称数组 */
deviceNames: string[]
/**
* 消息类型
* - property: 属性
* - service: 服务
*/
type: string
/**
* 消息标识符
* - property_set: 属性设置
* - service_invoke: 服务调用
*/
identifier: string
/** 具体数据 */
data: Record<string, any>
}
/**
* 场景规则创建/更新请求
*/
export interface IotRuleSceneSaveReqVO {
/** 场景规则编号 */
id?: number
/** 场景规则名称 */
name: string
/** 场景规则状态0=禁用 1=启用) */
status: number
/** 触发器配置 */
triggerConfig: IotRuleSceneTriggerConfig
/** 执行动作配置数组 */
actionConfigs: IotRuleSceneActionConfig[]
/** 备注 */
remark?: string
}
/**
* 场景规则响应
*/
export interface IotRuleSceneRespVO {
/** 场景规则编号 */
id: number
/** 场景规则名称 */
name: string
/** 场景规则状态0=禁用 1=启用) */
status: number
/** 触发器配置 */
triggerConfig: IotRuleSceneTriggerConfig
/** 执行动作配置数组 */
actionConfigs: IotRuleSceneActionConfig[]
/** 备注 */
remark?: string
/** 创建时间 */
createTime: Date
}
/**
* 场景规则分页项
*/
export interface IotRuleScenePageItemRespVO extends IotRuleSceneRespVO {
/** 触发次数 */
triggerCount: number
/** 最后触发时间 */
lastTriggerTime?: Date
}
/**
* 场景规则分页请求
*/
export interface IotRuleScenePageReqVO {
/** 场景规则名称 */
name?: string
/** 场景规则状态0=禁用 1=启用) */
status?: number
/** 创建时间 */
createTime?: [Date, Date]
/** 页码 */
pageNo?: number
/** 每页条数 */
pageSize?: number
}
/**
* 场景规则类型枚举
*/
export enum IotRuleSceneTriggerTypeEnum {
/** 设备触发 */
DEVICE = 1,
/** 定时触发 */
TIMER = 2
}
/**
* 场景规则动作类型枚举
*/
export enum IotRuleSceneActionTypeEnum {
/** 设备控制 */
DEVICE_CONTROL = 1,
/** 数据桥接 */
DATA_BRIDGE = 2
}
/**
* 设备消息类型枚举
*/
export enum IotDeviceMessageTypeEnum {
/** 属性 */
PROPERTY = 'property',
/** 事件 */
EVENT = 'event',
/** 服务 */
SERVICE = 'service'
}
/**
* 设备消息标识符枚举
*/
export enum IotDeviceMessageIdentifierEnum {
/** 属性上报 */
PROPERTY_REPORT = 'property_report',
/** 属性设置 */
PROPERTY_SET = 'property_set',
/** 事件上报 */
EVENT_REPORT = 'event_report',
/** 服务调用 */
SERVICE_INVOKE = 'service_invoke'
}
/**
* 触发条件参数操作符枚举
*/
export enum IotRuleSceneTriggerConditionParameterOperatorEnum {
/** 等于 */
EQ = 'eq',
/** 大于 */
GT = 'gt',
/** 大于等于 */
GTE = 'gte',
/** 小于 */
LT = 'lt',
/** 小于等于 */
LTE = 'lte',
/** 范围 */
BETWEEN = 'between',
/** 在列表中 */
IN = 'in'
}

View File

@@ -58,11 +58,10 @@ export const ThingModelApi = {
return await request.get({ url: `/iot/thing-model/list`, params })
},
// 获得产品物模型
getThingModelListByProductId: async (params: any) => {
// 获得产品物模型 TSL
getThingModelTSLByProductId: async (productId: number) => {
return await request.get({
url: `/iot/thing-model/list-by-product-id`,
params
url: `/iot/thing-model/tsl-by-product-id?productId=${productId}`
})
},

View File

@@ -61,7 +61,7 @@ export const useAppStore = defineStore('app', {
tagsView: true, // 标签页
tagsViewImmerse: false, // 标签页沉浸
tagsViewIcon: true, // 是否显示标签图标
logo: true, // logo
logo: false, // logo
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日

View File

@@ -245,5 +245,7 @@ export enum DICT_TYPE {
IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态
IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型
IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum' // 桥梁类型
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
IOT_DEVICE_MESSAGE_TYPE_ENUM = 'iot_device_message_type_enum', // IoT 设备消息类型枚举
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum' // IoT 场景流转的触发类型枚举
}

View File

@@ -0,0 +1,300 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item v-if="!props.productId" label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
placeholder="请选择产品"
clearable
class="!w-240px"
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入 DeviceName"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入备注名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select
v-model="queryParams.deviceType"
placeholder="请选择设备类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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.IOT_DEVICE_STATE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="设备分组" prop="groupId">
<el-select
v-model="queryParams.groupId"
placeholder="请选择设备分组"
clearable
class="!w-240px"
>
<el-option
v-for="group in deviceGroups"
:key="group.id"
:label="group.name"
:value="group.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="multiple" type="selection" width="55" />
<el-table-column v-else width="55">
<template #default="scope">
<el-radio
v-model="selectedId"
:label="scope.row.id"
@change="() => handleRadioChange(scope.row)"
>
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="DeviceName" align="center" prop="deviceName" />
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="所属产品" align="center" prop="productId">
<template #default="scope">
{{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="设备类型" align="center" prop="deviceType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="所属分组" align="center" prop="groupId">
<template #default="scope">
<template v-if="scope.row.groupIds?.length">
<el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
{{ deviceGroups.find((g) => g.id === id)?.name }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="onlineTime"
:formatter="dateFormatter"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<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 { dateFormatter } from '@/utils/formatTime'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
defineOptions({ name: 'IoTDeviceTableSelect' })
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
productId: {
type: Number,
default: null
}
})
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('设备选择器')
const formLoading = ref(false)
const loading = ref(true) // 列表的加载中
const list = ref<DeviceVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const selectedDevices = ref<DeviceVO[]>([]) // 选中的设备列表
const selectedId = ref<number>() // 单选模式下选中的ID
const products = ref<ProductVO[]>([]) // 产品列表
const deviceGroups = ref<DeviceGroupVO[]>([]) // 设备分组列表
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceName: undefined,
productId: undefined,
deviceType: undefined,
nickname: undefined,
status: undefined,
groupId: undefined
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
if (props.productId) {
queryParams.productId = props.productId as unknown as any
}
const data = await DeviceApi.getDevicePage(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 open = async () => {
dialogVisible.value = true
// 重置选择状态
selectedDevices.value = []
selectedId.value = undefined
if (!props.productId) {
// 获取产品列表
products.value = await ProductApi.getSimpleProductList()
}
// 获取设备列表
await getList()
}
defineExpose({ open })
/** 处理行点击事件 */
const tableRef = ref()
const handleRowClick = (row: DeviceVO) => {
if (props.multiple) {
tableRef.value?.toggleRowSelection(row)
} else {
selectedId.value = row.id
selectedDevices.value = [row]
}
}
/** 处理单选变更事件 */
const handleRadioChange = (row: DeviceVO) => {
selectedDevices.value = [row]
}
/** 处理选择变更事件 */
const handleSelectionChange = (selection: DeviceVO[]) => {
if (props.multiple) {
selectedDevices.value = selection
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
if (selectedDevices.value.length === 0) {
message.warning(props.multiple ? '请至少选择一个设备' : '请选择一个设备')
return
}
emit('success', props.multiple ? selectedDevices.value : selectedDevices.value[0])
dialogVisible.value = false
}
/** 初始化 **/
onMounted(async () => {
// 获取分组列表
deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
})
</script>

View File

@@ -0,0 +1,217 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="产品名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入产品名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="ProductKey" prop="productKey">
<el-input
v-model="queryParams.productKey"
class="!w-240px"
clearable
placeholder="请输入产品标识"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="multiple" type="selection" width="55" />
<el-table-column v-else width="55">
<template #default="scope">
<el-radio
v-model="selectedId"
:label="scope.row.id"
@change="() => handleRadioChange(scope.row)"
>
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column align="center" label="名称" prop="name" />
<el-table-column align="center" label="ProductKey" prop="productKey" />
<el-table-column align="center" label="品类" prop="categoryName" />
<el-table-column align="center" label="设备类型" prop="deviceType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column align="center" label="产品图标" prop="icon">
<template #default="scope">
<el-image
v-if="scope.row.icon"
:preview-src-list="[scope.row.icon]"
:src="scope.row.icon"
class="w-40px h-40px"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column align="center" label="产品图片" prop="picture">
<template #default="scope">
<el-image
v-if="scope.row.picUrl"
:preview-src-list="[scope.row.picture]"
:src="scope.row.picUrl"
class="w-40px h-40px"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<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 } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
defineOptions({ name: 'IoTProductTableSelect' })
const props = defineProps({
multiple: {
type: Boolean,
default: false
}
})
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('产品选择器')
const formLoading = ref(false)
const loading = ref(true) // 列表的加载中
const list = ref<ProductVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const selectedProducts = ref<ProductVO[]>([]) // 选中的产品列表
const selectedId = ref<number>() // 单选模式下选中的ID
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
productKey: undefined
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ProductApi.getProductPage(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 open = async () => {
dialogVisible.value = true
// 重置选择状态
selectedProducts.value = []
selectedId.value = undefined
await getList()
}
defineExpose({ open })
/** 处理行点击事件 */
const tableRef = ref()
const handleRowClick = (row: ProductVO) => {
if (props.multiple) {
tableRef.value?.toggleRowSelection(row)
} else {
selectedId.value = row.id
selectedProducts.value = [row]
}
}
/** 处理单选变更事件 */
const handleRadioChange = (row: ProductVO) => {
selectedProducts.value = [row]
}
/** 处理选择变更事件 */
const handleSelectionChange = (selection: ProductVO[]) => {
if (props.multiple) {
selectedProducts.value = selection
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
if (selectedProducts.value.length === 0) {
message.warning(props.multiple ? '请至少选择一个产品' : '请选择一个产品')
return
}
emit('success', props.multiple ? selectedProducts.value : selectedProducts.value[0])
dialogVisible.value = false
}
</script>

View File

@@ -0,0 +1,166 @@
<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"
:label="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>
<device-listener
v-for="(trigger, index) in formData.triggers"
:key="index"
:model-value="trigger"
@update:model-value="(val) => (formData.triggers[index] = val)"
class="mb-10px"
>
<el-button
type="danger"
round
:icon="Delete"
size="small"
@click="removeTrigger(index)"
/>
</device-listener>
<el-text class="ml-10px!" type="primary" @click="addTrigger">添加触发器</el-text>
</el-col>
<el-col :span="24">
<el-divider content-position="left">执行动作配置</el-divider>
<el-form-item label="执行器数组" prop="actionConfigs">
<!-- <el-input v-model="formData.actions" placeholder="请输入执行器数组" />-->
</el-form-item>
</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, RuleSceneVO } from '@/api/iot/rule/scene'
import DeviceListener from './components/DeviceListener.vue'
import { Delete } from '@element-plus/icons-vue'
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
/** IoT 规则场景(场景联动) 表单 */
defineOptions({ name: 'RuleSceneForm' })
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<RuleSceneVO>({
status: 0,
triggers: [] as IotRuleSceneTriggerConfig[]
} as RuleSceneVO)
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({
type: 1,
productKey: '',
deviceNames: [],
conditions: [
{
type: 'property',
parameters: []
}
]
})
}
/** 移除触发器 */
const removeTrigger = (index: number) => {
const newTriggers = [...formData.value.triggers]
newTriggers.splice(index, 1)
formData.value.triggers = newTriggers
}
/** 打开弹窗 */
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 {
formData.value = await RuleSceneApi.getRuleScene(id)
} 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 RuleSceneVO
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: 0,
triggers: [] as IotRuleSceneTriggerConfig[]
} as RuleSceneVO
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<el-select
v-model="selectedOperator"
class="condition-selector"
clearable
:placeholder="placeholder"
>
<!-- TODO puhui999: 考虑根据属性类型不同展示不同的可选条件 -->
<el-option label="等于" value="=" />
<el-option label="不等于" value="!=" />
<el-option label="大于" value=">" />
<el-option label="大于等于" value=">=" />
<el-option label="小于" value="<" />
<el-option label="小于等于" value="<=" />
<el-option label="在列表中" value="in" />
<el-option label="不在列表中" value="not in" />
<el-option label="在范围内" value="between" />
<el-option label="不在范围内" value="not between" />
<el-option label="包含" value="like" />
<el-option label="非空" value="not null" />
</el-select>
</template>
<script setup lang="ts">
import { computed } from 'vue'
/** 条件选择器 */
defineOptions({ name: 'ConditionSelector' })
const props = defineProps({
placeholder: {
type: String,
default: '请选择条件'
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const selectedOperator = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
<style scoped>
.condition-selector {
width: 100%;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div class="device-listener m-10px">
<div class="device-listener-header 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()">
{{ !product ? '选择产品' : product.name }}
</el-button>
</div>
<div class="flex items-center mr-60px">
<span class="mr-10px">设备</span>
<el-button type="primary" @click="openDeviceSelect">
{{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
</el-button>
</div>
<!-- 删除触发器 -->
<div class="device-listener-delete">
<el-tooltip content="删除触发器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 触发器条件 -->
<div
class="device-listener-condition flex p-10px"
v-for="(condition, index) in triggerConfig.conditions"
:key="index"
>
<div class="flex flex-col items-center justify-center mr-10px h-a">
<el-select
v-model="condition.type"
@change="condition.parameters = []"
class="!w-160px"
clearable
placeholder=""
>
<!-- <el-option-->
<!-- v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_MESSAGE_TYPE_ENUM)"-->
<!-- :key="dict.value"-->
<!-- :label="dict.label"-->
<!-- :value="dict.value"-->
<!-- />-->
<el-option label="属性" value="property" />
<el-option label="服务" value="service" />
<el-option label="事件" value="event" />
</el-select>
</div>
<div class="">
<DeviceListenerCondition
v-for="(parameter, index2) in condition.parameters"
:key="index2"
:model-value="parameter"
:thingModels="thingModels(condition)"
@update:model-value="(val) => (condition.parameters[index2] = val)"
class="mb-10px last:mb-0"
>
<el-tooltip content="删除参数" placement="top">
<el-button
class="device-listener-delete"
type="danger"
circle
:icon="Delete"
size="small"
@click="removeConditionParameter(condition.parameters, index2)"
/>
</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
:icon="Plus"
size="small"
@click="addConditionParameter(condition.parameters)"
/>
</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" :icon="Delete" size="small" @click="removeCondition(index)" />
</el-tooltip>
</div>
</div>
<el-text class="ml-10px!" type="primary" @click="addCondition">添加触发条件</el-text>
</div>
<IoTProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<IoTDeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</template>
<script setup lang="ts">
import { Delete, Plus } from '@element-plus/icons-vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import DeviceListenerCondition from './DeviceListenerCondition.vue'
import IoTProductTableSelect from '@/views/iot/product/product/components/IoTProductTableSelect.vue'
import IoTDeviceTableSelect from '@/views/iot/device/device/components/IoTDeviceTableSelect.vue'
import {
IotRuleSceneTriggerCondition,
IotRuleSceneTriggerConditionParameter,
IotRuleSceneTriggerConfig
} from '@/api/iot/rule/scene/scene.types'
import { ProductVO } from '@/api/iot/product/product'
import { DeviceVO } from '@/api/iot/device/device'
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { ThingModelApi } from '@/api/iot/thingmodel'
/** 场景联动之监听器组件 */
defineOptions({ name: 'DeviceListener' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<IotRuleSceneTriggerConfig>
const message = useMessage()
/** 添加触发条件 */
const addCondition = () => {
triggerConfig.value.conditions.push({
type: 'property',
parameters: []
})
}
/** 移除触发条件 */
const removeCondition = (index: number) => {
triggerConfig.value.conditions.splice(index, 1)
}
/** 添加参数 */
const addConditionParameter = (conditionParameters: IotRuleSceneTriggerConditionParameter[]) => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
conditionParameters.push({} as IotRuleSceneTriggerConditionParameter)
}
/** 移除参数 */
const removeConditionParameter = (
conditionParameters: IotRuleSceneTriggerConditionParameter[],
index: number
) => {
conditionParameters.splice(index, 1)
}
const productTableSelectRef = ref<InstanceType<typeof IoTProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof IoTDeviceTableSelect>>()
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 thingModelTSL = ref<any>()
const thingModels = computed(() => (condition: IotRuleSceneTriggerCondition) => {
switch (condition.type) {
case 'property':
return thingModelTSL.value.properties
// TODO puhui999: 服务和事件后续考虑
case 'service':
return thingModelTSL.value.services
case 'event':
return thingModelTSL.value.events
}
return []
})
const getThingModelTSL = async () => {
if (!product.value) {
return
}
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
}
</script>
<style lang="scss" scoped>
.device-listener {
.device-listener-header {
position: relative;
background-color: #eff3f7;
.device-listener-delete {
position: absolute;
top: auto;
right: 16px;
bottom: auto;
}
}
.device-listener-condition {
background-color: #dbe5f6;
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div class="device-listener-condition">
<el-select
v-model="conditionParameter.identifier"
class="!w-240px mr-10px"
clearable
placeholder="请选择物模型"
>
<el-option
v-for="thingModel in thingModels"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<ConditionSelector v-model="conditionParameter.operator" class="!w-180px mr-10px" />
<!-- TODO puhui999: 输入值范围校验 -->
<el-input v-model="conditionParameter.value" class="!w-240px mr-10px" placeholder="请输入值">
<template #append> {{ getUnitName }} </template>
</el-input>
<!-- 按钮插槽 -->
<slot></slot>
</div>
</template>
<script setup lang="ts">
import ConditionSelector from './ConditionSelector.vue'
import { IotRuleSceneTriggerConditionParameter } from '@/api/iot/rule/scene/scene.types'
import { useVModel } from '@vueuse/core'
defineOptions({ name: 'DeviceListenerCondition' })
const props = defineProps<{ modelValue: any; thingModels: any }>()
const emits = defineEmits(['update:modelValue'])
const conditionParameter = useVModel(
props,
'modelValue',
emits
) as Ref<IotRuleSceneTriggerConditionParameter>
/** 获得属性单位 */
const getUnitName = computed(() => {
const model = props.thingModels?.find(
(item: any) => item.identifier === conditionParameter.value.identifier
)
// 属性
if (model?.dataSpecs) {
return model.dataSpecs.unitName
}
// 服务和事件
// if (model?.outputParams) {
// return model.dataSpecs.unitName
// }
return '单位'
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,187 @@
<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" />
<el-table-column label="执行器数组" align="center" prop="actions" />
<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>
<!-- 表单弹窗添加/修改 -->
<IoTRuleSceneForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { RuleSceneApi, RuleSceneVO } from '@/api/iot/rule/scene'
import IoTRuleSceneForm from './IoTRuleSceneForm.vue'
/** IoT 规则场景(场景联动) 列表 */
defineOptions({ name: 'IotRuleScene' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<RuleSceneVO[]>([]) // 列表的数据
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>