perf:【IoT 物联网】场景联动触发器优化
parent
d3d6f8f8ab
commit
274ecb5dca
|
@ -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",
|
||||
|
|
|
@ -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']
|
||||
|
||||
// 表单验证规则类型
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,469 +0,0 @@
|
|||
// TODO @puhui999:这些后续需要删除哈
|
||||
# IotThingModelTSLRespVO 数据结构文档
|
||||
|
||||
## 概述
|
||||
|
||||
`IotThingModelTSLRespVO` 是IoT产品物模型TSL(Thing 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设备的建模需求。
|
|
@ -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 @puhui999:unocss - 已完成转换
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
|
@ -16,23 +16,42 @@
|
|||
</template>
|
||||
|
||||
<div class="p-0">
|
||||
<el-row :gutter="24">
|
||||
<!-- TODO @puhui999:NameInput、StatusRadio、DescriptionInput 是不是直接写在当前界面哈。有点散; -->
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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 @puhui999:unocss 哈 - 已完成转换 */
|
||||
:deep(.el-select-dropdown__item) {
|
||||
height: auto;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue