diff --git a/apps/web-antd/src/locales/langs/en-US/page.json b/apps/web-antd/src/locales/langs/en-US/page.json index 408b57d16..e15a02da0 100644 --- a/apps/web-antd/src/locales/langs/en-US/page.json +++ b/apps/web-antd/src/locales/langs/en-US/page.json @@ -168,7 +168,51 @@ "cronRequired": "Please enter a Cron expression", "expressionRequired": "Please enter an expression", "valueRequired": "Please enter a value", - "alarmNameRequired": "Please enter an alarm name" + "alarmNameRequired": "Please enter an alarm name", + "alarmConfigRequired": "Please select an alarm config", + "endTimeRequired": "Please enter end time", + "eventRequired": "Please select an event", + "productRequired": "Please select a product", + "serviceRequired": "Please select a service", + "startTimeRequired": "Please enter start time" + }, + "trigger": { + "product": "Product", + "productPlaceholder": "Select product", + "selectProductFirst": "Select a product first", + "eventIdentifier": "Event", + "eventPlaceholder": "Select event", + "serviceIdentifier": "Service", + "servicePlaceholder": "Select service", + "device_optional": "Device (Optional)", + "deviceOptionalPlaceholder": "Select a device or leave empty to match all", + "identifiersPlaceholder": "Select property identifiers", + "cronPlaceholder": "e.g. 0 0 9 * * ?", + "cronPresets": "Presets", + "cronPresetBtn": "Use preset", + "cronFormatOk": "Format OK", + "cronFormatError": "Invalid Cron expression" + }, + "condition": { + "timeRangeMode": "Time Range Mode", + "modeDaily": "Daily", + "modeWeekly": "Weekly", + "modeHoliday": "Holiday", + "startTime": "Start Time", + "endTime": "End Time", + "expectedState": "Expected State", + "expressionPlaceholder": "Enter Aviator expression, supports ${data.x} / ${meta.x}", + "variableHintLabel": "Available Variables" + }, + "action": { + "alarmConfig": "Alarm Config", + "alarmConfigPlaceholder": "Select alarm config", + "inputParams": "Input Params (JSON)", + "templateTitle": "Title", + "templateTitlePlaceholder": "Supports ${alarm.name} / ${meta.deviceName}", + "templateBody": "Body", + "templateBodyPlaceholder": "Supports ${data.x} / ${meta.x} / ${alarm.x} / ${trigger.x}", + "userSelectorPlaceholder": "Select recipients" }, "node": { "device_state": { diff --git a/apps/web-antd/src/locales/langs/zh-CN/page.json b/apps/web-antd/src/locales/langs/zh-CN/page.json index 1121ee4fe..9a0593158 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/page.json +++ b/apps/web-antd/src/locales/langs/zh-CN/page.json @@ -168,7 +168,51 @@ "cronRequired": "请输入 Cron 表达式", "expressionRequired": "请输入表达式", "valueRequired": "请输入值", - "alarmNameRequired": "请输入告警名称" + "alarmNameRequired": "请输入告警名称", + "alarmConfigRequired": "请选择告警配置", + "endTimeRequired": "请输入结束时间", + "eventRequired": "请选择事件", + "productRequired": "请选择产品", + "serviceRequired": "请选择服务", + "startTimeRequired": "请输入开始时间" + }, + "trigger": { + "product": "产品", + "productPlaceholder": "请选择产品", + "selectProductFirst": "请先选择产品", + "eventIdentifier": "事件标识", + "eventPlaceholder": "请选择事件", + "serviceIdentifier": "服务标识", + "servicePlaceholder": "请选择服务", + "device_optional": "指定设备(可选)", + "deviceOptionalPlaceholder": "选择具体设备或留空匹配所有", + "identifiersPlaceholder": "请选择属性标识", + "cronPlaceholder": "如 0 0 9 * * ?", + "cronPresets": "预设", + "cronPresetBtn": "使用预设", + "cronFormatOk": "格式正确", + "cronFormatError": "Cron 表达式格式错误" + }, + "condition": { + "timeRangeMode": "时间段模式", + "modeDaily": "每日", + "modeWeekly": "每周", + "modeHoliday": "节假日", + "startTime": "开始时间", + "endTime": "结束时间", + "expectedState": "预期状态", + "expressionPlaceholder": "输入 Aviator 表达式,支持 ${data.x} / ${meta.x}", + "variableHintLabel": "可用变量" + }, + "action": { + "alarmConfig": "告警配置", + "alarmConfigPlaceholder": "请选择告警配置", + "inputParams": "输入参数(JSON)", + "templateTitle": "标题", + "templateTitlePlaceholder": "支持 ${alarm.name} / ${meta.deviceName}", + "templateBody": "内容", + "templateBodyPlaceholder": "支持 ${data.x} / ${meta.x} / ${alarm.x} / ${trigger.x}", + "userSelectorPlaceholder": "请选择接收用户" }, "node": { "device_state": { diff --git a/apps/web-antd/src/views/iot/rule/chain/__tests__/node-schema.spec.ts b/apps/web-antd/src/views/iot/rule/chain/__tests__/node-schema.spec.ts index 13b139b18..c1d6b85a0 100644 --- a/apps/web-antd/src/views/iot/rule/chain/__tests__/node-schema.spec.ts +++ b/apps/web-antd/src/views/iot/rule/chain/__tests__/node-schema.spec.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom /** * F3 — node-schema mock 映射单元测试 * @@ -6,6 +7,9 @@ * - getMockSchema 未知 providerType 返回 null * - listMockProviderTypes 覆盖所有 F3 Acceptance 要求的 5+ 种节点类型 * - buildMockMetadata 过滤无 schema 的 catalog 项 + * + * 注:node-schema 顶部 import widgets/register 间接触达 window + * (widget .vue 引入 @vben/request → preferences)所以需 happy-dom。 */ import { describe, expect, it } from 'vitest'; @@ -82,14 +86,14 @@ describe('node-schema mock', () => { it('过滤 catalog 中没有 mock schema 的类型', () => { const catalog = [ - { category: 'trigger' as const, icon: 'bell', type: 'device_event' }, + { category: 'trigger' as const, icon: 'bell', type: 'not_a_type_1' }, { category: 'trigger' as const, icon: 'wrench', - type: 'device_service', + type: 'not_a_type_2', }, ]; - // device_event / device_service 在 mock 中未定义 schema + // 传入未定义在 mock 中的 type,buildMockMetadata 应过滤掉 const metadata = buildMockMetadata(catalog); expect(metadata).toHaveLength(0); }); diff --git a/apps/web-antd/src/views/iot/rule/chain/components/node-schema/index.ts b/apps/web-antd/src/views/iot/rule/chain/components/node-schema/index.ts index 4de9a6a51..9f530048b 100644 --- a/apps/web-antd/src/views/iot/rule/chain/components/node-schema/index.ts +++ b/apps/web-antd/src/views/iot/rule/chain/components/node-schema/index.ts @@ -11,6 +11,9 @@ * * 切换到后端真实 metadata:Pinia store loadMetadata() 完成后 getSchemaByType 优先读 * 真实数据,本 mock 仅为后端未就绪时的兜底。 + * + * F4/F5/F6 扩展:补全 6 种 schema(device_event / device_service / time_range / + * condition_device_state / service_invoke / alarm_clear)+ 自定义 widget 注册。 */ import type { @@ -18,6 +21,9 @@ import type { ProviderMetadata, } from '#/views/iot/rule/chain/api/node-metadata'; +// 触发自定义 widget 注册(import 即生效,幂等) +import '#/views/iot/rule/chain/components/widgets/register'; + // ────────────────────────────────────────────── // 复用字段工厂 // ────────────────────────────────────────────── @@ -57,6 +63,24 @@ function propertyIdentifierRule(required = true): Record { }; } +// ────────────────────────────────────────────── +// 复用 ProductSelector 字段工厂(F4 新增) +// ────────────────────────────────────────────── + +function productSelectorRule(required = true): Record { + return { + type: 'ProductSelector', + field: 'productId', + title: 'i18n:iot.dag.trigger.product', + props: { + placeholder: 'i18n:iot.dag.trigger.productPlaceholder', + }, + validate: required + ? [{ required: true, message: 'i18n:iot.dag.validate.productRequired' }] + : [], + }; +} + // ────────────────────────────────────────────── // Trigger schemas // ────────────────────────────────────────────── @@ -115,6 +139,65 @@ const timerTriggerSchema: FormCreateSchema = { ], }; +/** + * F4 — device_event 触发器(新增) + * 字段:productId + eventIdentifier + deviceId(可选) + * ⚠️ 评审 B5:高级过滤如需表达式字段,使用 ${data.x}/${meta.x} 新语法 + */ +const deviceEventTriggerSchema: FormCreateSchema = { + rule: [ + productSelectorRule() as unknown as FormCreateSchema['rule'][number], + { + type: 'EventIdentifier', + field: 'eventIdentifier', + title: 'i18n:iot.dag.trigger.eventIdentifier', + props: { productId: '{{formData.productId}}' }, + validate: [ + { required: true, message: 'i18n:iot.dag.validate.eventRequired' }, + ], + }, + { + type: 'DeviceSelector', + field: 'deviceId', + title: 'i18n:iot.dag.trigger.device_optional', + props: { + productId: '{{formData.productId}}', + placeholder: 'i18n:iot.dag.trigger.deviceOptionalPlaceholder', + }, + validate: [], + }, + ], +}; + +/** + * F4 — device_service 触发器(新增) + * 字段:productId + serviceIdentifier + deviceId(可选) + */ +const deviceServiceTriggerSchema: FormCreateSchema = { + rule: [ + productSelectorRule() as unknown as FormCreateSchema['rule'][number], + { + type: 'ServiceIdentifier', + field: 'serviceIdentifier', + title: 'i18n:iot.dag.trigger.serviceIdentifier', + props: { productId: '{{formData.productId}}' }, + validate: [ + { required: true, message: 'i18n:iot.dag.validate.serviceRequired' }, + ], + }, + { + type: 'DeviceSelector', + field: 'deviceId', + title: 'i18n:iot.dag.trigger.device_optional', + props: { + productId: '{{formData.productId}}', + placeholder: 'i18n:iot.dag.trigger.deviceOptionalPlaceholder', + }, + validate: [], + }, + ], +}; + // ────────────────────────────────────────────── // Condition schemas // ────────────────────────────────────────────── @@ -122,12 +205,11 @@ const timerTriggerSchema: FormCreateSchema = { const expressionConditionSchema: FormCreateSchema = { rule: [ { - type: 'a-textarea', + type: 'AviatorExprEditor', field: 'expression', title: 'i18n:iot.dag.field.expression', props: { placeholder: 'data.temp > 30 && meta.ts > 0', - autoSize: { minRows: 3, maxRows: 8 }, }, validate: [ { @@ -139,6 +221,80 @@ const expressionConditionSchema: FormCreateSchema = { ], }; +/** + * F5 — time_range 条件(新增) + * 字段:mode + startTime + endTime + timezone + */ +const timeRangeConditionSchema: FormCreateSchema = { + rule: [ + { + type: 'a-radio-group', + field: 'mode', + title: 'i18n:iot.dag.condition.timeRangeMode', + value: 'daily', + options: [ + { label: 'i18n:iot.dag.condition.modeDaily', value: 'daily' }, + { label: 'i18n:iot.dag.condition.modeWeekly', value: 'weekly' }, + { label: 'i18n:iot.dag.condition.modeHoliday', value: 'holiday' }, + ], + }, + { + type: 'a-input', + field: 'startTime', + title: 'i18n:iot.dag.condition.startTime', + props: { placeholder: '08:00' }, + validate: [ + { + required: true, + message: 'i18n:iot.dag.validate.startTimeRequired', + }, + ], + }, + { + type: 'a-input', + field: 'endTime', + title: 'i18n:iot.dag.condition.endTime', + props: { placeholder: '18:00' }, + validate: [ + { required: true, message: 'i18n:iot.dag.validate.endTimeRequired' }, + ], + }, + { + type: 'a-input', + field: 'timezone', + title: 'i18n:iot.dag.field.timezone', + value: 'Asia/Shanghai', + }, + ], +}; + +/** + * F5 — condition_device_state 条件(新增) + * 字段:deviceId + expectedState + */ +const conditionDeviceStateConditionSchema: FormCreateSchema = { + rule: [ + { + type: 'DeviceSelector', + field: 'deviceId', + title: 'i18n:iot.dag.field.device', + validate: [ + { required: true, message: 'i18n:iot.dag.validate.deviceRequired' }, + ], + }, + { + type: 'a-radio-group', + field: 'expectedState', + title: 'i18n:iot.dag.condition.expectedState', + value: 'online', + options: [ + { label: 'i18n:iot.dag.option.online', value: 'online' }, + { label: 'i18n:iot.dag.option.offline', value: 'offline' }, + ], + }, + ], +}; + // ────────────────────────────────────────────── // Action schemas // ────────────────────────────────────────────── @@ -237,14 +393,77 @@ const notifyActionSchema: FormCreateSchema = { // 汇总映射 // ────────────────────────────────────────────── +/** + * F6 — service_invoke 动作(新增) + * 字段:productId + deviceId + serviceIdentifier + inputParams (JSON) + */ +const serviceInvokeActionSchema: FormCreateSchema = { + rule: [ + productSelectorRule() as unknown as FormCreateSchema['rule'][number], + { + type: 'DeviceSelector', + field: 'deviceId', + title: 'i18n:iot.dag.field.device', + props: { productId: '{{formData.productId}}' }, + validate: [ + { required: true, message: 'i18n:iot.dag.validate.deviceRequired' }, + ], + }, + { + type: 'ServiceIdentifier', + field: 'serviceIdentifier', + title: 'i18n:iot.dag.trigger.serviceIdentifier', + props: { productId: '{{formData.productId}}' }, + validate: [ + { required: true, message: 'i18n:iot.dag.validate.serviceRequired' }, + ], + }, + { + type: 'a-textarea', + field: 'inputParams', + title: 'i18n:iot.dag.action.inputParams', + props: { + placeholder: '{"key": "value"}', + autoSize: { minRows: 2, maxRows: 6 }, + }, + }, + ], +}; + +/** + * F6 — alarm_clear 动作(新增) + * 字段:alarmConfigId(清除对应告警) + */ +const alarmClearActionSchema: FormCreateSchema = { + rule: [ + { + type: 'AlarmConfigSelector', + field: 'alarmConfigId', + title: 'i18n:iot.dag.action.alarmConfig', + validate: [ + { + required: true, + message: 'i18n:iot.dag.validate.alarmConfigRequired', + }, + ], + }, + ], +}; + /** providerType → schema 映射(mock) */ const SCHEMA_MAP: Readonly> = Object.freeze({ device_state: deviceStateTriggerSchema, device_property: devicePropertyTriggerSchema, + device_event: deviceEventTriggerSchema, + device_service: deviceServiceTriggerSchema, timer: timerTriggerSchema, expression: expressionConditionSchema, + time_range: timeRangeConditionSchema, + condition_device_state: conditionDeviceStateConditionSchema, property_set: propertySetActionSchema, + service_invoke: serviceInvokeActionSchema, alarm_trigger: alarmTriggerActionSchema, + alarm_clear: alarmClearActionSchema, notify: notifyActionSchema, }); diff --git a/apps/web-antd/src/views/iot/rule/chain/components/widgets/AlarmConfigSelector.vue b/apps/web-antd/src/views/iot/rule/chain/components/widgets/AlarmConfigSelector.vue new file mode 100644 index 000000000..f12e47641 --- /dev/null +++ b/apps/web-antd/src/views/iot/rule/chain/components/widgets/AlarmConfigSelector.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/web-antd/src/views/iot/rule/chain/components/widgets/AviatorExprEditor.vue b/apps/web-antd/src/views/iot/rule/chain/components/widgets/AviatorExprEditor.vue new file mode 100644 index 000000000..f7fa25874 --- /dev/null +++ b/apps/web-antd/src/views/iot/rule/chain/components/widgets/AviatorExprEditor.vue @@ -0,0 +1,105 @@ + + +