perf:【IoT 物联网】场景联动触发器优化

pull/800/head
puhui999 2025-07-28 16:45:43 +08:00
parent d3d6f8f8ab
commit 274ecb5dca
14 changed files with 96 additions and 2740 deletions

View File

@ -88,7 +88,7 @@
},
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",

View File

@ -173,12 +173,13 @@ interface IotRuleScene extends TenantBaseDO {
actions: ActionConfig[] // 执行器数组(必填,至少一个)
}
// 工具类型
// TODO @puhui999这些在瞅瞅~
type TriggerType = (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
type ActionType = (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
type OperatorType =
// 工具类型 - 从枚举中提取类型
export type TriggerType =
(typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
export type ActionType =
(typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
export type OperatorType =
(typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value']
// 表单验证规则类型

View File

@ -1,469 +0,0 @@
// TODO @puhui999:这些后续需要删除哈
# IotThingModelTSLRespVO 数据结构文档
## 概述
`IotThingModelTSLRespVO` 是IoT产品物模型TSLThing Specification Language的响应数据结构用于返回完整的产品物模型定义包括属性、事件和服务的详细信息。TSL是阿里云IoT平台定义的一套物模型描述规范。
## 主体数据结构
### IotThingModelTSLRespVO
```typescript
interface IotThingModelTSLRespVO {
productId: number; // 产品编号(必填)
productKey: string; // 产品标识(必填)
properties: ThingModelProperty[]; // 属性列表(必填)
events: ThingModelEvent[]; // 事件列表(必填)
services: ThingModelService[]; // 服务列表(必填)
}
```
**字段说明:**
- `productId`: 产品编号唯一标识一个IoT产品
- `productKey`: 产品标识符,用于设备连接和识别
- `properties`: 设备属性列表,描述设备的状态信息
- `events`: 设备事件列表,描述设备主动上报的事件
- `services`: 设备服务列表,描述可以调用的设备功能
## 属性数据结构 (ThingModelProperty)
### 基本结构
```typescript
interface ThingModelProperty {
identifier: string; // 属性标识符(必填)
name: string; // 属性名称(必填)
accessMode: string; // 访问模式(必填)
required?: boolean; // 是否必选
dataType: string; // 数据类型(必填)
dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型)
dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型)
}
```
### 字段详细说明
#### identifier属性标识符
- **类型**: `string`
- **必填**: 是
- **格式**: 正则表达式 `^[a-zA-Z][a-zA-Z0-9_]{0,31}$`
- **说明**: 只能由字母、数字和下划线组成必须以字母开头长度不超过32个字符
- **示例**: `"temperature"`, `"humidity"`, `"power_status"`
#### name属性名称
- **类型**: `string`
- **必填**: 是
- **说明**: 属性的显示名称,用于界面展示
- **示例**: `"温度"`, `"湿度"`, `"电源状态"`
#### accessMode访问模式
- **类型**: `string`
- **必填**: 是
- **枚举值**:
- `"r"`: 只读,设备只能上报,平台不能下发
- `"rw"`: 读写,设备可以上报,平台也可以下发
- **示例**: `"r"`, `"rw"`
#### dataType数据类型
- **类型**: `string`
- **必填**: 是
- **枚举值**:
- `"int"`: 整数型
- `"float"`: 单精度浮点型
- `"double"`: 双精度浮点型
- `"enum"`: 枚举型
- `"bool"`: 布尔型
- `"text"`: 文本型
- `"date"`: 时间型
- `"struct"`: 结构体型
- `"array"`: 数组型
## 事件数据结构 (ThingModelEvent)
### 基本结构
```typescript
interface ThingModelEvent {
identifier: string; // 事件标识符(必填)
name: string; // 事件名称(必填)
required?: boolean; // 是否必选
type: string; // 事件类型(必填)
outputParams?: ThingModelParam[]; // 输出参数
method?: string; // 执行方法
}
```
### 字段详细说明
#### type事件类型
- **类型**: `string`
- **必填**: 是
- **枚举值**:
- `"info"`: 信息事件
- `"alert"`: 告警事件
- `"error"`: 故障事件
#### outputParams输出参数
- **类型**: `ThingModelParam[]`
- **必填**: 否
- **说明**: 事件触发时返回的参数信息
## 服务数据结构 (ThingModelService)
### 基本结构
```typescript
interface ThingModelService {
identifier: string; // 服务标识符(必填)
name: string; // 服务名称(必填)
required?: boolean; // 是否必选
callType: string; // 调用类型(必填)
inputParams?: ThingModelParam[]; // 输入参数
outputParams?: ThingModelParam[]; // 输出参数
method?: string; // 执行方法
}
```
### 字段详细说明
#### callType调用类型
- **类型**: `string`
- **必填**: 是
- **枚举值**:
- `"async"`: 异步调用
- `"sync"`: 同步调用
## 参数数据结构 (ThingModelParam)
### 基本结构
```typescript
interface ThingModelParam {
identifier: string; // 参数标识符(必填)
name: string; // 参数名称(必填)
direction: string; // 参数方向(必填)
paraOrder?: number; // 参数序号
dataType: string; // 数据类型(必填)
dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型)
dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型)
}
```
### 字段详细说明
#### direction参数方向
- **类型**: `string`
- **必填**: 是
- **枚举值**:
- `"input"`: 输入参数
- `"output"`: 输出参数
## 数据规范结构 (ThingModelDataSpecs)
数据规范是一个抽象基类,根据不同的数据类型有不同的具体实现:
### 1. 数值型数据规范 (ThingModelNumericDataSpec)
适用于 `int`、`float`、`double` 类型:
```typescript
interface ThingModelNumericDataSpec {
dataType: "int" | "float" | "double";
max: string; // 最大值(必填)
min: string; // 最小值(必填)
step: string; // 步长(必填)
precise?: string; // 精度float/double可选
defaultValue?: string; // 默认值
unit?: string; // 单位符号
unitName?: string; // 单位名称
}
```
### 2. 布尔/枚举型数据规范 (ThingModelBoolOrEnumDataSpecs)
适用于 `bool`、`enum` 类型:
```typescript
interface ThingModelBoolOrEnumDataSpecs {
dataType: "bool" | "enum";
name: string; // 枚举项名称(必填)
value: number; // 枚举值(必填)
}
```
### 3. 文本/时间型数据规范 (ThingModelDateOrTextDataSpecs)
适用于 `text`、`date` 类型:
```typescript
interface ThingModelDateOrTextDataSpecs {
dataType: "text" | "date";
length?: number; // 数据长度text类型需要最大2048
defaultValue?: string; // 默认值
}
```
### 4. 数组型数据规范 (ThingModelArrayDataSpecs)
适用于 `array` 类型:
```typescript
interface ThingModelArrayDataSpecs {
dataType: "array";
size: number; // 数组元素个数(必填)
childDataType: string; // 数组元素数据类型(必填)
dataSpecsList?: ThingModelDataSpecs[]; // 子元素数据规范struct类型时
}
```
**childDataType 枚举值**:
- `"struct"`: 结构体
- `"int"`: 整数
- `"float"`: 单精度浮点
- `"double"`: 双精度浮点
- `"text"`: 文本
### 5. 结构体型数据规范 (ThingModelStructDataSpecs)
适用于 `struct` 类型:
```typescript
interface ThingModelStructDataSpecs {
dataType: "struct";
identifier: string; // 属性标识符(必填)
name: string; // 属性名称(必填)
accessMode: string; // 操作类型(必填)
required?: boolean; // 是否必选
childDataType: string; // 子数据类型(必填)
dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型)
dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型)
}
```
**childDataType 枚举值**:
- `"int"`: 整数
- `"float"`: 单精度浮点
- `"double"`: 双精度浮点
- `"text"`: 文本
- `"date"`: 时间
- `"enum"`: 枚举
- `"bool"`: 布尔
## 数据类型映射关系
### dataSpecs vs dataSpecsList
- **dataSpecs**: 用于非列表型数据类型(`int`、`float`、`double`、`text`、`date`、`array`
- **dataSpecsList**: 用于列表型数据类型(`enum`、`bool`、`struct`
### JSON多态序列化
数据规范使用Jackson的`@JsonTypeInfo`和`@JsonSubTypes`注解实现多态序列化:
```json
{
"dataType": "int",
"max": "100",
"min": "0",
"step": "1",
"unit": "°C",
"unitName": "摄氏度"
}
```
## 完整示例
### 温度传感器物模型示例
```json
{
"productId": 1024,
"productKey": "temperature_sensor",
"properties": [
{
"identifier": "temperature",
"name": "温度",
"accessMode": "r",
"required": true,
"dataType": "float",
"dataSpecs": {
"dataType": "float",
"max": "100.0",
"min": "-40.0",
"step": "0.1",
"precise": "1",
"unit": "°C",
"unitName": "摄氏度"
}
},
{
"identifier": "power_switch",
"name": "电源开关",
"accessMode": "rw",
"required": false,
"dataType": "bool",
"dataSpecsList": [
{
"dataType": "bool",
"name": "关闭",
"value": 0
},
{
"dataType": "bool",
"name": "开启",
"value": 1
}
]
}
],
"events": [
{
"identifier": "high_temperature_alert",
"name": "高温告警",
"required": false,
"type": "alert",
"outputParams": [
{
"identifier": "current_temp",
"name": "当前温度",
"direction": "output",
"dataType": "float",
"dataSpecs": {
"dataType": "float",
"max": "100.0",
"min": "-40.0",
"step": "0.1"
}
}
]
}
],
"services": [
{
"identifier": "reset_device",
"name": "重置设备",
"required": false,
"callType": "async",
"inputParams": [
{
"identifier": "reset_type",
"name": "重置类型",
"direction": "input",
"dataType": "enum",
"dataSpecsList": [
{
"dataType": "enum",
"name": "软重置",
"value": 1
},
{
"dataType": "enum",
"name": "硬重置",
"value": 2
}
]
}
],
"outputParams": [
{
"identifier": "result",
"name": "执行结果",
"direction": "output",
"dataType": "bool",
"dataSpecsList": [
{
"dataType": "bool",
"name": "失败",
"value": 0
},
{
"dataType": "bool",
"name": "成功",
"value": 1
}
]
}
]
}
]
}
```
## 前端使用建议
### 1. TypeScript类型定义
建议在前端项目中定义完整的TypeScript接口确保类型安全
```typescript
// 定义完整的类型接口
export interface IotThingModelTSLRespVO {
productId: number;
productKey: string;
properties: ThingModelProperty[];
events: ThingModelEvent[];
services: ThingModelService[];
}
// 使用联合类型处理数据规范的多态性
export type ThingModelDataSpecs =
| ThingModelNumericDataSpec
| ThingModelBoolOrEnumDataSpecs
| ThingModelDateOrTextDataSpecs
| ThingModelArrayDataSpecs
| ThingModelStructDataSpecs;
```
### 2. 数据验证
```typescript
// 验证数据类型和数据规范的一致性
function validateDataSpecs(dataType: string, dataSpecs: any): boolean {
switch (dataType) {
case 'int':
case 'float':
case 'double':
return dataSpecs.dataType === dataType &&
dataSpecs.max !== undefined &&
dataSpecs.min !== undefined;
case 'bool':
case 'enum':
return Array.isArray(dataSpecs) &&
dataSpecs.every(spec => spec.name && spec.value !== undefined);
// ... 其他类型验证
default:
return false;
}
}
```
### 3. 数据转换工具
```typescript
// 将后端数据转换为前端展示格式
function formatPropertyValue(property: ThingModelProperty, value: any): string {
if (property.dataType === 'enum' || property.dataType === 'bool') {
const spec = property.dataSpecsList?.find(s => s.value === value);
return spec?.name || String(value);
}
if (property.dataType === 'float' || property.dataType === 'double') {
const unit = property.dataSpecs?.unit || '';
return `${value}${unit}`;
}
return String(value);
}
```
## 注意事项
1. **数据规范选择**: 根据`dataType`选择使用`dataSpecs`还是`dataSpecsList`
2. **标识符唯一性**: 在同一产品下,所有功能的`identifier`必须唯一
3. **数据类型一致性**: 参数的`dataType`必须与其`dataSpecs`的`dataType`保持一致
4. **枚举值处理**: 布尔型和枚举型数据使用`dataSpecsList`数组存储可选值
5. **嵌套结构**: 结构体和数组类型可能包含嵌套的数据规范定义
6. **版本兼容**: 物模型结构可能随版本演进,前端需要做好兼容性处理
这个数据结构为IoT设备的完整功能描述提供了标准化的格式支持复杂的数据类型和嵌套结构能够满足各种IoT设备的建模需求。

View File

@ -8,25 +8,13 @@
@change="handleDeviceChange"
/>
<!-- TODO @puhui999这里有点冗余建议去掉 -->
<!-- 设备状态变更提示 -->
<div v-if="trigger.type === TriggerTypeEnum.DEVICE_STATE_UPDATE" class="mt-8px">
<el-alert title="设备状态变更触发" type="info" :closable="false" show-icon>
<template #default>
<p class="m-0">当选中的设备上线或离线时将自动触发场景规则</p>
<p class="m-0 mt-4px text-12px text-[var(--el-text-color-secondary)]">无需配置额外的触发条件</p>
</template>
</el-alert>
</div>
<!-- 条件组配置 -->
<div v-else-if="needsConditions" class="space-y-12px">
<div v-if="needsConditions" class="space-y-12px">
<div class="flex items-center justify-between mb-12px">
<div class="flex items-center gap-8px">
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">触发条件</span>
<!-- TODO @puhui999去掉数量限制 -->
<el-tag size="small" type="info">
{{ trigger.conditionGroups?.length || 0 }}/{{ maxConditionGroups }}
{{ trigger.conditionGroups?.length || 0 }}个条件组
</el-tag>
</div>
<div class="flex items-center gap-8px">
@ -52,18 +40,13 @@
:key="`group-${groupIndex}`"
class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
>
<div class="flex items-center justify-between p-12px px-16px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)]">
<div
class="flex items-center justify-between p-12px px-16px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)]"
>
<div class="flex items-center text-14px font-500 text-[var(--el-text-color-primary)]">
<span>条件组 {{ groupIndex + 1 }}</span>
<!-- TODO @puhui999不用条件组之间就是或条件之间就是且 -->
<el-select
v-model="group.logicOperator"
size="small"
class="w-80px ml-12px"
>
<el-option label="且" value="AND" />
<el-option label="或" value="OR" />
</el-select>
<el-tag size="small" type="info" class="ml-8px">条件间为"且"关系</el-tag>
</div>
<el-button
type="danger"
@ -262,5 +245,4 @@ watch(
updateValidationResult()
}
)
// TODO @puhui999unocss -
</script>

View File

@ -1,52 +1,23 @@
<!-- 定时触发配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<div class="flex items-center justify-between p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px">
<Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">定时触发配置</span>
</div>
<div class="flex items-center gap-8px">
<el-button type="text" size="small" @click="showBuilder = !showBuilder">
<Icon :icon="showBuilder ? 'ep:edit' : 'ep:setting'" />
{{ showBuilder ? '手动编辑' : '可视化编辑' }}
</el-button>
</div>
<div class="flex items-center gap-8px p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">定时触发配置</span>
</div>
<!-- 可视化编辑器 -->
<!-- TODO @puhui999是不是复用现有的 cron 组件不然有点重复哈维护比较复杂 -->
<div v-if="showBuilder" class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]">
<CronBuilder v-model="localValue" @validate="handleValidate" />
</div>
<!-- 手动编辑 -->
<div v-else class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]">
<!-- CRON表达式配置 -->
<div class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]">
<el-form-item label="CRON表达式" required>
<CronInput v-model="localValue" @validate="handleValidate" />
<Crontab v-model="localValue" />
</el-form-item>
</div>
<!-- 下次执行时间预览 -->
<NextExecutionPreview :cron-expression="localValue" />
<!-- 验证结果 -->
<div v-if="validationMessage" class="mt-8px">
<el-alert
:title="validationMessage"
:type="isValid ? 'success' : 'error'"
:closable="false"
show-icon
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import CronBuilder from '../inputs/CronBuilder.vue'
import CronInput from '../inputs/CronInput.vue'
import NextExecutionPreview from '../previews/NextExecutionPreview.vue'
import { Crontab } from '@/components/Crontab'
/** 定时触发配置组件 */
defineOptions({ name: 'TimerTriggerConfig' })
@ -66,23 +37,4 @@ const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit, {
defaultValue: '0 0 12 * * ?'
})
//
const showBuilder = ref(true)
const validationMessage = ref('')
const isValid = ref(true)
//
const handleValidate = (result: { valid: boolean; message: string }) => {
isValid.value = result.valid
validationMessage.value = result.message
emit('validate', result)
}
//
onMounted(() => {
handleValidate({ valid: true, message: '定时触发配置验证通过' })
})
</script>
</script>

View File

@ -1,242 +0,0 @@
<!-- CRON 可视化构建器组件 -->
<!-- TODO @puhui999看看能不能复用全局的 cron 组件 -->
<template>
<div class="cron-builder">
<div class="builder-header">
<span class="header-title">可视化 CRON 编辑器</span>
</div>
<div class="builder-content">
<!-- 快捷选项 -->
<div class="quick-options">
<span class="options-label">常用配置</span>
<el-button
v-for="option in quickOptions"
:key="option.label"
size="small"
@click="applyQuickOption(option)"
>
{{ option.label }}
</el-button>
</div>
<!-- 详细配置 -->
<div class="detailed-config">
<el-row :gutter="16">
<el-col :span="4">
<el-form-item label="秒">
<el-select v-model="cronParts.second" @change="updateCronExpression">
<el-option label="每秒" value="*" />
<el-option label="0秒" value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="分钟">
<el-select v-model="cronParts.minute" @change="updateCronExpression">
<el-option label="每分钟" value="*" />
<el-option
v-for="i in 60"
:key="i - 1"
:label="`${i - 1}分`"
:value="String(i - 1)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="小时">
<el-select v-model="cronParts.hour" @change="updateCronExpression">
<el-option label="每小时" value="*" />
<el-option
v-for="i in 24"
:key="i - 1"
:label="`${i - 1}时`"
:value="String(i - 1)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="日">
<el-select v-model="cronParts.day" @change="updateCronExpression">
<el-option label="每日" value="*" />
<el-option v-for="i in 31" :key="i" :label="`${i}日`" :value="String(i)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="月">
<el-select v-model="cronParts.month" @change="updateCronExpression">
<el-option label="每月" value="*" />
<el-option
v-for="(month, index) in months"
:key="index"
:label="month"
:value="String(index + 1)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="周">
<el-select v-model="cronParts.week" @change="updateCronExpression">
<el-option label="每周" value="*" />
<el-option
v-for="(week, index) in weeks"
:key="index"
:label="week"
:value="String(index)"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
/** CRON 可视化构建器组件 */
defineOptions({ name: 'CronBuilder' })
interface Props {
modelValue: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
// CRON
const cronParts = reactive({
second: '0',
minute: '0',
hour: '12',
day: '*',
month: '*',
week: '?'
})
//
const months = [
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月'
]
const weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
//
const quickOptions = [
{ label: '每分钟', cron: '0 * * * * ?' },
{ label: '每小时', cron: '0 0 * * * ?' },
{ label: '每天中午', cron: '0 0 12 * * ?' },
{ label: '每天凌晨', cron: '0 0 0 * * ?' },
{ label: '工作日9点', cron: '0 0 9 * * MON-FRI' },
{ label: '每周一', cron: '0 0 9 * * MON' }
]
//
const updateCronExpression = () => {
localValue.value = `${cronParts.second} ${cronParts.minute} ${cronParts.hour} ${cronParts.day} ${cronParts.month} ${cronParts.week}`
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
}
const applyQuickOption = (option: any) => {
localValue.value = option.cron
parseCronExpression()
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
}
const parseCronExpression = () => {
if (!localValue.value) return
const parts = localValue.value.split(' ')
if (parts.length >= 6) {
cronParts.second = parts[0] || '0'
cronParts.minute = parts[1] || '0'
cronParts.hour = parts[2] || '12'
cronParts.day = parts[3] || '*'
cronParts.month = parts[4] || '*'
cronParts.week = parts[5] || '?'
}
}
//
onMounted(() => {
if (localValue.value) {
parseCronExpression()
} else {
updateCronExpression()
}
})
</script>
<style scoped>
.cron-builder {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: var(--el-fill-color-blank);
}
.builder-header {
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.header-title {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
}
.builder-content {
padding: 16px;
}
.quick-options {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.options-label {
font-weight: 500;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.detailed-config {
margin-top: 16px;
}
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__label) {
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>

View File

@ -1,141 +0,0 @@
<!-- CRON 表达式输入组件 -->
<!-- TODO @puhui999看看能不能复用全局的 cron 组件 -->
<template>
<div class="cron-input">
<el-input
v-model="localValue"
placeholder="请输入 CRON 表达式0 0 12 * * ?"
@blur="handleBlur"
@input="handleInput"
>
<template #suffix>
<el-tooltip content="CRON 表达式帮助" placement="top">
<Icon icon="ep:question-filled" class="input-help" @click="showHelp = !showHelp" />
</el-tooltip>
</template>
</el-input>
<!-- 帮助信息 -->
<div v-if="showHelp" class="cron-help">
<el-alert title="CRON 表达式格式:秒 分 时 日 月 周" type="info" :closable="false" show-icon>
<template #default>
<div class="help-content">
<p><strong>示例</strong></p>
<ul>
<li><code>0 0 12 * * ?</code> - 每天中午12点执行</li>
<li><code>0 */5 * * * ?</code> - 5</li>
<li><code>0 0 9-17 * * MON-FRI</code> - 工作日9-17点每小时执行</li>
</ul>
<p><strong>特殊字符</strong></p>
<ul>
<li><code>*</code> - 匹配任意值</li>
<li><code>?</code> - 不指定值用于日和周</li>
<li><code>/</code> - 间隔触发 */5 5</li>
<li><code>-</code> - 范围 9-17 表示9到17</li>
<li><code>,</code> - 列举 MON,WED,FRI</li>
</ul>
</div>
</template>
</el-alert>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { validateCronExpression } from '../../utils/validation'
/** CRON 表达式输入组件 */
defineOptions({ name: 'CronInput' })
interface Props {
modelValue: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
//
const showHelp = ref(false)
//
const handleInput = () => {
validateExpression()
}
const handleBlur = () => {
validateExpression()
}
const validateExpression = () => {
if (!localValue.value) {
emit('validate', { valid: false, message: '请输入CRON表达式' })
return
}
const isValid = validateCronExpression(localValue.value)
if (isValid) {
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
} else {
emit('validate', { valid: false, message: 'CRON表达式格式不正确' })
}
}
//
watch(
() => localValue.value,
() => {
validateExpression()
}
)
//
onMounted(() => {
if (localValue.value) {
validateExpression()
}
})
</script>
<style scoped>
.cron-input {
width: 100%;
}
.input-help {
color: var(--el-text-color-placeholder);
cursor: pointer;
transition: color 0.2s;
}
.input-help:hover {
color: var(--el-color-primary);
}
.cron-help {
margin-top: 8px;
}
.help-content ul {
margin: 8px 0 0 0;
padding-left: 20px;
}
.help-content li {
margin-bottom: 4px;
}
.help-content code {
background: var(--el-fill-color-light);
padding: 2px 4px;
border-radius: 2px;
font-family: 'Courier New', monospace;
}
</style>

View File

@ -1,178 +0,0 @@
<!-- 场景描述输入组件 -->
<template>
<div class="relative w-full">
<el-input
ref="inputRef"
v-model="localValue"
type="textarea"
placeholder="请输入场景描述(可选)"
:rows="3"
maxlength="200"
show-word-limit
resize="none"
@input="handleInput"
/>
<!-- 描述模板 -->
<teleport to="body">
<div v-if="showTemplates" ref="templateDropdownRef" class="fixed z-1000 bg-white border border-[var(--el-border-color-light)] rounded-6px shadow-[var(--el-box-shadow)] min-w-300px max-w-400px" :style="dropdownStyle">
<div class="flex items-center justify-between p-12px border-b border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]">
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">描述模板</span>
<el-button type="text" size="small" @click="showTemplates = false">
<Icon icon="ep:close" />
</el-button>
</div>
<div class="max-h-300px overflow-y-auto">
<div
v-for="template in descriptionTemplates"
:key="template.title"
class="p-12px border-b border-[var(--el-border-color-lighter)] cursor-pointer transition-colors duration-200 hover:bg-[var(--el-fill-color-light)] last:border-b-0"
@click="applyTemplate(template)"
>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-4px">{{ template.title }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ template.content }}</div>
</div>
</div>
</div>
</teleport>
<!-- TODO @puhui999不用模版哈简单点 -->
<!-- 模板按钮 -->
<div v-if="!localValue && !showTemplates" class="absolute top-2px right-2px">
<el-button type="text" size="small" @click="toggleTemplates">
<Icon icon="ep:document" class="mr-1" />
使用模板
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
/** 场景描述输入组件 */
defineOptions({ name: 'DescriptionInput' })
interface Props {
modelValue?: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit, {
defaultValue: ''
})
const showTemplates = ref(false)
const templateDropdownRef = ref()
const inputRef = ref()
const dropdownStyle = ref({})
//
const descriptionTemplates = [
{
title: '温度控制场景',
content: '当环境温度超过设定阈值时,自动启动空调降温设备,确保环境温度保持在舒适范围内。'
},
{
title: '设备监控场景',
content: '实时监控关键设备的运行状态,当设备出现异常或离线时,立即发送告警通知相关人员。'
},
{
title: '节能控制场景',
content: '根据时间段和环境条件,自动调节设备功率或关闭非必要设备,实现智能节能管理。'
},
{
title: '安防联动场景',
content: '当检测到异常情况时,自动触发安防设备联动,包括报警器、摄像头录制等安全措施。'
},
{
title: '定时任务场景',
content: '按照预设的时间计划,定期执行设备检查、数据备份或系统维护等自动化任务。'
}
]
//
const calculateDropdownPosition = () => {
if (!inputRef.value) return
const inputElement = inputRef.value.$el || inputRef.value
const rect = inputElement.getBoundingClientRect()
const viewportHeight = window.innerHeight
const dropdownHeight = 300 //
let top = rect.bottom + 4
let left = rect.left
//
if (top + dropdownHeight > viewportHeight) {
top = rect.top - dropdownHeight - 4
}
//
const maxLeft = window.innerWidth - 400 //
if (left > maxLeft) {
left = maxLeft
}
if (left < 10) {
left = 10
}
dropdownStyle.value = {
top: `${top}px`,
left: `${left}px`
}
}
const handleInput = (value: string) => {
if (value.length > 0) {
showTemplates.value = false
}
}
const applyTemplate = (template: any) => {
localValue.value = template.content
showTemplates.value = false
}
const toggleTemplates = () => {
showTemplates.value = !showTemplates.value
if (showTemplates.value) {
nextTick(() => {
calculateDropdownPosition()
})
}
}
//
const handleClickOutside = (event: Event) => {
if (
templateDropdownRef.value &&
!templateDropdownRef.value.contains(event.target as Node) &&
inputRef.value &&
!inputRef.value.$el.contains(event.target as Node)
) {
showTemplates.value = false
}
}
//
onMounted(() => {
window.addEventListener('resize', calculateDropdownPosition)
window.addEventListener('scroll', calculateDropdownPosition)
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateDropdownPosition)
window.removeEventListener('scroll', calculateDropdownPosition)
document.removeEventListener('click', handleClickOutside)
})
</script>

View File

@ -1,111 +0,0 @@
<!-- 场景名称输入组件 -->
<template>
<div class="relative w-full">
<el-input
v-model="localValue"
placeholder="请输入场景名称"
maxlength="50"
show-word-limit
clearable
@blur="handleBlur"
@input="handleInput"
>
<template #prefix>
<Icon icon="ep:edit" class="text-[var(--el-text-color-placeholder)]" />
</template>
</el-input>
<!-- 智能提示 -->
<!-- TODO @puhui999暂时不用考虑智能推荐哈用途不大 -->
<div v-if="showSuggestions && suggestions.length > 0" class="absolute top-full left-0 right-0 z-1000 bg-white border border-[var(--el-border-color-light)] rounded-4px shadow-[var(--el-box-shadow-light)] mt-4px">
<div class="p-8px px-12px border-b border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]">
<span class="text-12px text-[var(--el-text-color-secondary)] font-500">推荐名称</span>
</div>
<div class="max-h-200px overflow-y-auto">
<div
v-for="suggestion in suggestions"
:key="suggestion"
class="p-8px px-12px cursor-pointer transition-colors duration-200 text-14px text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color-light)] last:border-b-0"
@click="applySuggestion(suggestion)"
>
{{ suggestion }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
/** 场景名称输入组件 */
defineOptions({ name: 'NameInput' })
interface Props {
modelValue: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
//
const showSuggestions = ref(false)
const suggestions = ref<string[]>([])
//
const nameTemplates = [
'温度过高自动降温',
'设备离线告警通知',
'湿度异常自动调节',
'夜间安防模式启动',
'能耗超标自动关闭',
'故障设备自动重启',
'定时设备状态检查',
'环境数据异常告警'
]
const handleInput = (value: string) => {
if (value.length > 0 && value.length < 10) {
//
suggestions.value = nameTemplates
.filter(
(template) =>
template.includes(value) || (value.includes('温度') && template.includes('温度'))
)
.slice(0, 5)
showSuggestions.value = suggestions.value.length > 0
} else {
showSuggestions.value = false
}
}
const handleBlur = () => {
//
setTimeout(() => {
showSuggestions.value = false
}, 200)
}
const applySuggestion = (suggestion: string) => {
localValue.value = suggestion
showSuggestions.value = false
}
//
onMounted(() => {
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement
if (!target.closest('.name-input')) {
showSuggestions.value = false
}
})
})
</script>

View File

@ -1,158 +0,0 @@
<!-- 场景状态选择组件 -->
<template>
<div class="status-radio">
<el-radio-group
v-model="localValue"
@change="handleChange"
>
<el-radio :value="0" class="status-option">
<div class="status-content">
<div class="status-indicator enabled"></div>
<div class="status-info">
<div class="status-label">启用</div>
<div class="status-desc">场景规则生效满足条件时自动执行</div>
</div>
</div>
</el-radio>
<el-radio :value="1" class="status-option">
<div class="status-content">
<div class="status-indicator disabled"></div>
<div class="status-info">
<div class="status-label">禁用</div>
<div class="status-desc">场景规则暂停不会触发任何执行动作</div>
</div>
</div>
</el-radio>
</el-radio-group>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
/** 场景状态选择组件 */
defineOptions({ name: 'StatusRadio' })
interface Props {
modelValue: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
const handleChange = (value: number) => {
emit('change', value)
}
</script>
<style scoped>
.status-radio {
width: 100%;
}
.status-radio :deep(.el-radio) {
margin-bottom: 8px;
}
.status-radio :deep(.el-radio:last-child) {
margin-bottom: 0;
}
:deep(.el-radio-group) {
display: flex;
flex-direction: row;
gap: 16px;
width: 100%;
align-items: flex-start;
}
:deep(.el-radio) {
margin-right: 0;
width: auto;
flex: 1;
height: auto;
align-items: flex-start;
}
.status-option {
width: auto;
flex: 1;
}
:deep(.el-radio__input) {
margin-top: 12px;
flex-shrink: 0;
}
:deep(.el-radio__label) {
width: 100%;
padding-left: 8px;
}
.status-content {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
transition: all 0.2s;
width: 100%;
margin-left: 0;
}
:deep(.el-radio.is-checked) .status-content {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.status-content:hover {
border-color: var(--el-color-primary-light-3);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
margin-top: 4px;
flex-shrink: 0;
}
.status-indicator.enabled {
background: var(--el-color-success);
box-shadow: 0 0 0 2px var(--el-color-success-light-8);
}
.status-indicator.disabled {
background: var(--el-color-danger);
box-shadow: 0 0 0 2px var(--el-color-danger-light-8);
}
.status-info {
flex: 1;
}
.status-label {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
margin-bottom: 4px;
}
.status-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
</style>

View File

@ -16,23 +16,42 @@
</template>
<div class="p-0">
<el-row :gutter="24">
<!-- TODO @puhui999NameInputStatusRadioDescriptionInput 是不是直接写在当前界面哈有点散 -->
<el-row :gutter="24" class="mb-24px">
<el-col :span="12">
<el-form-item label="场景名称" prop="name" required>
<NameInput v-model="formData.name" />
<el-input
v-model="formData.name"
placeholder="请输入场景名称"
maxlength="50"
show-word-limit
clearable
/>
</el-form-item>
</el-col>
<!-- TODO @puhui999每个一行会好点 -->
<el-col :span="12">
<el-form-item label="场景状态" prop="status" required>
<StatusRadio v-model="formData.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-row>
<el-form-item label="场景描述" prop="description">
<DescriptionInput v-model="formData.description" />
<el-input
v-model="formData.description"
type="textarea"
placeholder="请输入场景描述(可选)"
:rows="3"
maxlength="200"
show-word-limit
resize="none"
/>
</el-form-item>
</div>
</el-card>
@ -40,9 +59,7 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import NameInput from '../inputs/NameInput.vue'
import DescriptionInput from '../inputs/DescriptionInput.vue'
import StatusRadio from '../inputs/StatusRadio.vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
/** 基础信息配置组件 */
@ -61,7 +78,6 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formData = useVModel(props, 'modelValue', emit)
// TODO @puhui999 unocss
</script>
<style scoped>

View File

@ -6,15 +6,13 @@
<div class="flex items-center gap-8px">
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
<!-- TODO @puhui999是不是去掉 maxTriggers计数 -->
<el-tag size="small" type="info">{{ triggers.length }}/{{ maxTriggers }}</el-tag>
<el-tag size="small" type="info">{{ triggers.length }}个触发器</el-tag>
</div>
<div class="flex items-center gap-8px">
<el-button
type="primary"
size="small"
@click="addTrigger"
:disabled="triggers.length >= maxTriggers"
>
<Icon icon="ep:plus" />
添加触发器
@ -26,13 +24,7 @@
<div class="p-0">
<!-- 空状态 -->
<div v-if="triggers.length === 0">
<el-empty description="暂无触发器配置">
<!-- TODO @puhui999这个要不要去掉哈入口统一点 -->
<el-button type="primary" @click="addTrigger">
<Icon icon="ep:plus" />
添加第一个触发器
</el-button>
</el-empty>
<el-empty description="暂无触发器配置,请点击右上角添加触发器按钮开始配置" />
</div>
<!-- 触发器列表 -->
@ -62,18 +54,28 @@
<div class="space-y-16px">
<!-- 触发类型选择 -->
<TriggerTypeSelector
:model-value="trigger.type"
@update:model-value="(value) => updateTriggerType(index, value)"
@change="onTriggerTypeChange(trigger, $event)"
/>
<el-form-item label="触发类型" required>
<el-select
:model-value="trigger.type"
@update:model-value="(value) => updateTriggerType(index, value)"
@change="onTriggerTypeChange(trigger, $event)"
placeholder="请选择触发类型"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(trigger.type)"
:model-value="trigger"
@update:model-value="(value) => updateTrigger(index, value)"
@validate="(result) => handleTriggerValidate(index, result)"
/>
<!-- 定时触发配置 -->
@ -81,38 +83,16 @@
v-if="trigger.type === TriggerTypeEnum.TIMER"
:model-value="trigger.cronExpression"
@update:model-value="(value) => updateTriggerCronExpression(index, value)"
@validate="(result) => handleTriggerValidate(index, result)"
/>
</div>
</div>
</div>
<!-- 添加提示 -->
<!-- TODO @puhui999这个要不要去掉哈入口统一点 -->
<div v-if="triggers.length > 0 && triggers.length < maxTriggers" class="text-center py-16px">
<el-button type="primary" plain @click="addTrigger">
<Icon icon="ep:plus" />
继续添加触发器
</el-button>
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxTriggers }} 个触发器 </span>
</div>
<!-- 验证结果 -->
<div v-if="validationMessage" class="validation-result">
<el-alert
:title="validationMessage"
:type="isValid ? 'success' : 'error'"
:closable="false"
show-icon
/>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import TriggerTypeSelector from '../selectors/TriggerTypeSelector.vue'
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
import {
@ -129,7 +109,6 @@ interface Props {
interface Emits {
(e: 'update:triggers', value: TriggerFormData[]): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
@ -153,13 +132,33 @@ const createDefaultTriggerData = (): TriggerFormData => {
}
}
//
const maxTriggers = 5
//
const triggerValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
const validationMessage = ref('')
const isValid = ref(true)
//
const triggerTypeOptions = [
{
value: TriggerTypeEnum.DEVICE_STATE_UPDATE,
label: '设备状态变更'
},
{
value: TriggerTypeEnum.DEVICE_PROPERTY_POST,
label: '设备属性上报'
},
{
value: TriggerTypeEnum.DEVICE_EVENT_POST,
label: '设备事件上报'
},
{
value: TriggerTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用'
},
{
value: TriggerTypeEnum.TIMER,
label: '定时触发'
}
]
//
const triggerTypeNames = {
@ -180,12 +179,13 @@ const triggerTypeTags = {
//
const isDeviceTrigger = (type: number) => {
return [
const deviceTriggerTypes = [
TriggerTypeEnum.DEVICE_STATE_UPDATE,
TriggerTypeEnum.DEVICE_PROPERTY_POST,
TriggerTypeEnum.DEVICE_EVENT_POST,
TriggerTypeEnum.DEVICE_SERVICE_INVOKE
].includes(type)
] as number[]
return deviceTriggerTypes.includes(type)
}
const getTriggerTypeName = (type: number) => {
@ -198,31 +198,12 @@ const getTriggerTypeTag = (type: number) => {
//
const addTrigger = () => {
if (triggers.value.length >= maxTriggers) {
return
}
const newTrigger = createDefaultTriggerData()
triggers.value.push(newTrigger)
}
const removeTrigger = (index: number) => {
triggers.value.splice(index, 1)
delete triggerValidations.value[index]
//
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
Object.keys(triggerValidations.value).forEach((key) => {
const numKey = parseInt(key)
if (numKey > index) {
newValidations[numKey - 1] = triggerValidations.value[numKey]
} else if (numKey < index) {
newValidations[numKey] = triggerValidations.value[numKey]
}
})
triggerValidations.value = newValidations
updateValidationResult()
}
const updateTriggerType = (index: number, type: number) => {
@ -259,39 +240,6 @@ const onTriggerTypeChange = (trigger: TriggerFormData, type: number) => {
}
}
}
const handleTriggerValidate = (index: number, result: { valid: boolean; message: string }) => {
triggerValidations.value[index] = result
updateValidationResult()
}
const updateValidationResult = () => {
const validations = Object.values(triggerValidations.value)
const allValid = validations.every((v) => v.valid)
const hasValidations = validations.length > 0
if (!hasValidations) {
isValid.value = true
validationMessage.value = ''
} else if (allValid) {
isValid.value = true
validationMessage.value = '所有触发器配置验证通过'
} else {
isValid.value = false
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
validationMessage.value = `触发器配置错误: ${errorMessages.join('; ')}`
}
emit('validate', { valid: isValid.value, message: validationMessage.value })
}
//
watch(
() => triggers.value.length,
() => {
updateValidationResult()
}
)
</script>

View File

@ -1,142 +0,0 @@
<!-- 触发器类型选择组件 -->
<template>
<div class="w-full">
<el-form-item label="触发类型" required>
<el-select
v-model="localValue"
placeholder="请选择触发类型"
@change="handleChange"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex items-center gap-12px flex-1">
<!-- TODO @puhui999貌似没对齐 -->
<Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" />
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div>
</div>
</div>
<!-- TODO @puhui999这个要不去掉 -->
<el-tag :type="option.tag" size="small">
{{ option.category }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
<!-- 类型说明 -->
<!-- TODO @puhui999这个去掉感觉没啥内容哈 -->
<div v-if="selectedOption" class="mt-16px p-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px mb-12px">
<Icon :icon="selectedOption.icon" class="text-20px text-[var(--el-color-primary)]" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">{{ selectedOption.label }}</span>
</div>
<div class="ml-28px">
<p class="text-14px text-[var(--el-text-color-regular)] m-0 mb-12px leading-relaxed">{{ selectedOption.description }}</p>
<div class="flex flex-col gap-6px">
<div v-for="feature in selectedOption.features" :key="feature" class="flex items-center gap-6px">
<Icon icon="ep:check" class="text-12px text-[var(--el-color-success)] flex-shrink-0" />
<span class="text-12px text-[var(--el-text-color-secondary)]">{{ feature }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
/** 触发器类型选择组件 */
defineOptions({ name: 'TriggerTypeSelector' })
interface Props {
modelValue: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
//
const triggerTypeOptions = [
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
label: '设备状态变更',
description: '当设备上线、离线状态发生变化时触发',
icon: 'ep:connection',
tag: 'warning',
category: '设备状态',
features: ['监控设备连接状态', '实时响应设备变化', '无需配置额外条件']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
label: '设备属性上报',
description: '当设备属性值满足指定条件时触发',
icon: 'ep:data-line',
tag: 'primary',
category: '数据监控',
features: ['监控设备属性变化', '支持多种比较条件', '可配置阈值范围']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
label: '设备事件上报',
description: '当设备上报特定事件时触发',
icon: 'ep:bell',
tag: 'success',
category: '事件监控',
features: ['监控设备事件', '支持事件参数过滤', '实时事件响应']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用',
description: '当设备服务被调用时触发',
icon: 'ep:service',
tag: 'info',
category: '服务监控',
features: ['监控服务调用', '支持参数条件', '服务执行跟踪']
},
{
value: IotRuleSceneTriggerTypeEnum.TIMER,
label: '定时触发',
description: '按照设定的时间计划定时触发',
icon: 'ep:timer',
tag: 'danger',
category: '定时任务',
features: ['支持CRON表达式', '灵活的时间配置', '可视化时间设置']
}
]
//
const selectedOption = computed(() => {
return triggerTypeOptions.find((option) => option.value === localValue.value)
})
//
const handleChange = (value: number) => {
emit('change', value)
}
</script>
<style scoped>
/** TODO @puhui999unocss 哈 - 已完成转换 */
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>