Selaa lähdekoodia

fix: (合并冲突)

sun-chaochao 3 vuotta sitten
vanhempi
commit
b3a6ac18e2
45 muutettua tiedostoa jossa 1625 lisäystä ja 113 poistoa
  1. 0 0
      public/images/alarm/device.png
  2. 0 0
      public/images/alarm/org.png
  3. 0 0
      public/images/alarm/other.png
  4. 0 0
      public/images/alarm/product.png
  5. BIN
      public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png
  6. BIN
      public/images/scene.png
  7. 1 1
      src/components/CheckButton/index.less
  8. 0 1
      src/components/PermissionButton/index.tsx
  9. 49 0
      src/components/ProTableCard/CardItems/scene.tsx
  10. 29 28
      src/pages/device/Instance/Detail/index.tsx
  11. 24 2
      src/pages/device/components/Metadata/Cat/index.tsx
  12. 37 6
      src/pages/device/components/Metadata/index.tsx
  13. 34 20
      src/pages/notice/Template/Debug/index.tsx
  14. 31 14
      src/pages/notice/Template/Detail/doc/WeixinApp.tsx
  15. 18 20
      src/pages/notice/Template/Detail/index.tsx
  16. 27 4
      src/pages/notice/Template/Log/index.tsx
  17. 1 0
      src/pages/notice/Template/index.tsx
  18. 1 0
      src/pages/notice/Template/typings.d.ts
  19. 1 1
      src/pages/rule-engine/Alarm/Config/index.tsx
  20. 8 0
      src/pages/rule-engine/Alarm/Configuration/Save/index.less
  21. 202 0
      src/pages/rule-engine/Alarm/Configuration/Save/index.tsx
  22. 117 0
      src/pages/rule-engine/Alarm/Configuration/index.tsx
  23. 17 0
      src/pages/rule-engine/Alarm/Configuration/service.ts
  24. 9 0
      src/pages/rule-engine/Alarm/Configuration/typings.d.ts
  25. 162 0
      src/pages/rule-engine/Scene/Save/action/action.tsx
  26. 18 0
      src/pages/rule-engine/Scene/Save/action/index.less
  27. 26 0
      src/pages/rule-engine/Scene/Save/action/index.tsx
  28. 60 0
      src/pages/rule-engine/Scene/Save/action/messageContent.tsx
  29. 28 0
      src/pages/rule-engine/Scene/Save/action/service.ts
  30. 7 0
      src/pages/rule-engine/Scene/Save/components/ItemGroup/index.less
  31. 10 0
      src/pages/rule-engine/Scene/Save/components/ItemGroup/index.tsx
  32. 49 0
      src/pages/rule-engine/Scene/Save/components/TimeSelect/index.tsx
  33. 89 0
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  34. 24 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less
  35. 65 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx
  36. 4 0
      src/pages/rule-engine/Scene/Save/components/index.ts
  37. 67 1
      src/pages/rule-engine/Scene/Save/index.tsx
  38. 10 0
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  39. 360 0
      src/pages/rule-engine/Scene/Save2/index.tsx
  40. 12 4
      src/pages/rule-engine/Scene/index.tsx
  41. 6 3
      src/pages/rule-engine/Scene/typings.d.ts
  42. 4 1
      src/pages/system/Menu/Detail/edit.tsx
  43. 6 5
      src/pages/system/User/ResetPassword/index.tsx
  44. 9 2
      src/utils/menu/index.ts
  45. 3 0
      src/utils/menu/router.ts

+ 0 - 0
public/images/alarm/device.png


+ 0 - 0
public/images/alarm/org.png


+ 0 - 0
public/images/alarm/other.png


+ 0 - 0
public/images/alarm/product.png


BIN
public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png


BIN
public/images/scene.png


+ 1 - 1
src/components/CheckButton/index.less

@@ -1,4 +1,4 @@
-@import '../../../node_modules/antd/lib/style/themes/variable';
+@import '~antd/es/style/themes/default.less';
 
 .box {
   display: flex;

+ 0 - 1
src/components/PermissionButton/index.tsx

@@ -18,7 +18,6 @@ interface PermissionButtonProps extends ButtonProps {
  */
 const PermissionButton = (props: PermissionButtonProps) => {
   const { tooltip, popConfirm, isPermission, ...buttonProps } = props;
-
   const _isPermission =
     'isPermission' in props && props.isPermission
       ? 'disabled' in buttonProps

+ 49 - 0
src/components/ProTableCard/CardItems/scene.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
+
+export interface DeviceCardProps extends SceneItem {
+  tools: React.ReactNode[];
+}
+
+const defaultImage = require('/public/images/scene.png');
+
+export default (props: DeviceCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.tools}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        online: StatusColorEnum.processing,
+        offline: StatusColorEnum.error,
+        notActive: StatusColorEnum.warning,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>触发方式</label>
+              <div className={'ellipsis'}>{'test'}</div>
+            </div>
+            <div>
+              <label>说明</label>
+              <div className={'ellipsis'}>{props.describe || '--'}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 29 - 28
src/pages/device/Instance/Detail/index.tsx

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { InstanceModel, service } from '@/pages/device/Instance';
+import { InstanceModel } from '@/pages/device/Instance';
 import { history, useParams } from 'umi';
-import { Badge, Card, Descriptions, Divider, message, Tooltip } from 'antd';
+import { Badge, Card, Descriptions, Divider, Tooltip } from 'antd';
 import type { ReactNode } from 'react';
 import { useEffect, useState } from 'react';
 import { observer } from '@formily/react';
@@ -23,6 +23,7 @@ import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import { PermissionButton } from '@/components';
 import { QuestionCircleOutlined } from '@ant-design/icons';
+import Service from '@/pages/device/Instance/service';
 
 export const deviceStatus = new Map();
 deviceStatus.set('online', <Badge status="success" text={'在线'} />);
@@ -33,18 +34,18 @@ const InstanceDetail = observer(() => {
   const intl = useIntl();
   const [tab, setTab] = useState<string>('detail');
   const params = useParams<{ id: string }>();
-  const { permission } = PermissionButton.usePermission('device/Instance');
+  const service = new Service('device-instance');
 
-  const resetMetadata = async () => {
-    const resp = await service.deleteMetadata(params.id);
-    if (resp.status === 200) {
-      message.success('操作成功');
-      Store.set(SystemConst.REFRESH_DEVICE, true);
-      setTimeout(() => {
-        Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
-      }, 400);
-    }
-  };
+  // const resetMetadata = async () => {
+  //   const resp = await service.deleteMetadata(params.id);
+  //   if (resp.status === 200) {
+  //     message.success('操作成功');
+  //     Store.set(SystemConst.REFRESH_DEVICE, true);
+  //     setTimeout(() => {
+  //       Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+  //     }, 400);
+  //   }
+  // };
   const baseList = [
     {
       key: 'detail',
@@ -99,21 +100,21 @@ const InstanceDetail = observer(() => {
         <Card>
           <Metadata
             type="device"
-            tabAction={
-              <PermissionButton
-                isPermission={permission.update}
-                popConfirm={{
-                  title: '确认重置?',
-                  onConfirm: resetMetadata,
-                }}
-                tooltip={{
-                  title: '重置后将使用产品的物模型配置',
-                }}
-                key={'reload'}
-              >
-                重置操作
-              </PermissionButton>
-            }
+            // tabAction={
+            //   <PermissionButton
+            //     isPermission={permission.update}
+            //     popConfirm={{
+            //       title: '确认重置?',
+            //       onConfirm: resetMetadata,
+            //     }}
+            //     tooltip={{
+            //       title: '重置后将使用产品的物模型配置',
+            //     }}
+            //     key={'reload'}
+            //   >
+            //     重置操作1
+            //   </PermissionButton>
+            // }
           />
         </Card>
       ),

+ 24 - 2
src/pages/device/components/Metadata/Cat/index.tsx

@@ -3,23 +3,45 @@ import { useEffect, useState } from 'react';
 import { productModel, service } from '@/pages/device/Product';
 import MonacoEditor from 'react-monaco-editor';
 import { observer } from '@formily/react';
+import { InstanceModel } from '@/pages/device/Instance';
+import { useLocation } from 'umi';
+import InstanceService from '@/pages/device/Instance/service';
 
 interface Props {
   visible: boolean;
   close: () => void;
+  type: 'product' | 'device';
 }
 
+const instanceService = new InstanceService('device-instance');
 const Cat = observer((props: Props) => {
+  const location = useLocation<{ id: string }>();
   const [codecs, setCodecs] = useState<{ id: string; name: string }[]>();
-  const metadata = productModel.current?.metadata as string;
+  const metadataMap = {
+    product: productModel.current?.metadata as string,
+    device: InstanceModel.current?.metadata as string, // 有问题
+  };
+  const metadata = metadataMap[props.type];
   const [value, setValue] = useState(metadata);
+  const _path = location.pathname.split('/');
+  const id = _path[_path.length - 1];
   useEffect(() => {
     service.codecs().subscribe({
       next: (data) => {
         setCodecs([{ id: 'jetlinks', name: 'jetlinks' }].concat(data));
       },
     });
-  }, []);
+
+    if (props.type === 'device' && id) {
+      instanceService.detail(id).then((resp) => {
+        if (resp.status === 200) {
+          InstanceModel.current = resp.result;
+          const _metadata = resp.result?.metadata;
+          setValue(_metadata);
+        }
+      });
+    }
+  }, [id]);
 
   const convertMetadata = (key: string) => {
     if (key === 'alink') {

+ 37 - 6
src/pages/device/components/Metadata/index.tsx

@@ -1,16 +1,19 @@
 import { observer } from '@formily/react';
-import { Space, Tabs } from 'antd';
+import { message, Space, Tabs } from 'antd';
 import BaseMetadata from './Base';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Import from './Import';
 import type { ReactNode } from 'react';
 import { useState } from 'react';
 import Cat from './Cat';
-import Service from '@/pages/device/components/Metadata/service';
+// import Service from '@/pages/device/components/Metadata/service';
 import { InfoCircleOutlined } from '@ant-design/icons';
 import styles from './index.less';
-import { InstanceModel } from '@/pages/device/Instance';
+import { InstanceModel, service } from '@/pages/device/Instance';
 import { PermissionButton } from '@/components';
+import { Store } from 'jetlinks-store';
+import SystemConst from '@/utils/const';
+import { useParams } from 'umi';
 
 interface Props {
   tabAction?: ReactNode;
@@ -18,7 +21,7 @@ interface Props {
   independentMetadata?: boolean;
 }
 
-export const service = new Service();
+// export const service = new Service();
 const Metadata = observer((props: Props) => {
   const intl = useIntl();
   const [visible, setVisible] = useState<boolean>(false);
@@ -26,6 +29,20 @@ const Metadata = observer((props: Props) => {
   const { permission } = PermissionButton.usePermission(
     props.type === 'device' ? 'device/Instance' : 'device/Product',
   );
+
+  const params = useParams<{ id: string }>();
+
+  const resetMetadata = async () => {
+    const resp = await service.deleteMetadata(params.id);
+    if (resp.status === 200) {
+      message.success('操作成功');
+      Store.set(SystemConst.REFRESH_DEVICE, true);
+      setTimeout(() => {
+        Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      }, 400);
+    }
+  };
+
   return (
     <div style={{ position: 'relative' }}>
       <div className={styles.tips}>
@@ -38,7 +55,21 @@ const Metadata = observer((props: Props) => {
         className={styles.metadataNav}
         tabBarExtraContent={
           <Space>
-            {props?.tabAction}
+            {props.type === 'device' && (
+              <PermissionButton
+                isPermission={permission.update}
+                popConfirm={{
+                  title: '确认重置?',
+                  onConfirm: resetMetadata,
+                }}
+                tooltip={{
+                  title: '重置后将使用产品的物模型配置',
+                }}
+                key={'reload'}
+              >
+                重置操作
+              </PermissionButton>
+            )}
             <PermissionButton isPermission={permission.update} onClick={() => setVisible(true)}>
               {intl.formatMessage({
                 id: 'pages.device.productDetail.metadata.quickImport',
@@ -94,7 +125,7 @@ const Metadata = observer((props: Props) => {
         </Tabs.TabPane>
       </Tabs>
       <Import visible={visible} close={() => setVisible(false)} />
-      <Cat visible={cat} close={() => setCat(false)} />
+      <Cat visible={cat} close={() => setCat(false)} type={props.type} />
     </div>
   );
 });

+ 34 - 20
src/pages/notice/Template/Debug/index.tsx

@@ -38,27 +38,32 @@ const Debug = observer(() => {
             }
           });
 
-          onFieldReact('variableDefinitions.*.type', (field) => {
+          onFieldReact('variableDefinitions.*.id', (field) => {
             const value = (field as Field).value;
-            const format = field.query('.value').take() as any;
-            switch (value) {
-              case 'date':
-                format.setComponent(DatePicker);
-                break;
-              case 'string':
-                format.setComponent(Input);
-                break;
-              case 'number':
-                format.setComponent(NumberPicker);
-                break;
-              case 'file':
-                format.setComponent(FUpload, {
-                  type: 'file',
-                });
-                break;
-              case 'other':
-                format.setComponent(Input);
-                break;
+            const format = field.query('.value').take() as Field;
+
+            if (format) {
+              switch (value) {
+                case 'date':
+                  format.setComponent(DatePicker, {
+                    showTime: true,
+                  });
+                  break;
+                case 'string':
+                  format.setComponent(Input);
+                  break;
+                case 'number':
+                  format.setComponent(NumberPicker, {});
+                  break;
+                case 'file':
+                  format.setComponent(FUpload, {
+                    type: 'file',
+                  });
+                  break;
+                case 'other':
+                  format.setComponent(Input);
+                  break;
+              }
             }
           });
         },
@@ -83,6 +88,9 @@ const Debug = observer(() => {
       Select,
       ArrayTable,
       PreviewText,
+      NumberPicker,
+      DatePicker,
+      FUpload,
     },
   });
 
@@ -164,6 +172,12 @@ const Debug = observer(() => {
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
                 },
+                type: {
+                  'x-hidden': true,
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
               },
             },
           },

+ 31 - 14
src/pages/notice/Template/Detail/doc/WeixinApp.tsx

@@ -2,35 +2,52 @@ import { Image } from 'antd';
 import './index.less';
 
 const WeixinApp = () => {
-  const agentId = require('/public/images/notice/doc/template/weixin-official/01-Agentid.jpg');
-  const appId = require('/public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.jpg');
+  const appId = require('/public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png');
 
   return (
     <div className="doc">
       <div className="url">
-        微信公众平台:<a href="https://mp.weixin.qq.com/">https://mp.weixin.qq.com/</a>
+        企业微信管理后台:<a href="https://work.weixin.qq.com">https://work.weixin.qq.com</a>
       </div>
       <h1>1. 概述</h1>
       <div>
-        通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用
+        通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务
       </div>
-      <h1>2.通知配置说明</h1>
+      <h1>2.模板配置说明</h1>
       <div>
-        <h2>1. AppID</h2>
-        <div>微信服务号的唯一专属编号。</div>
-        <div>获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”</div>
-        <div className="image">
-          <Image width="100%" src={agentId} />
-        </div>
+        <h2>1. 绑定配置</h2>
+        <div>绑定通知配置</div>
+      </div>
+      <div>
+        <h2>2. 用户标签</h2>
+        <div>以标签的维度通知该标签下所有用户</div>
       </div>
-      <h2>2. AppSecret</h2>
       <div>
-        <div>公众号开发者身份的密码</div>
-        <div>获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”</div>
+        <h2>3. 消息模板</h2>
+        <div>微信公众号中配置的消息模板</div>
+      </div>
+      <div>
+        <h2>4. 模板跳转链接</h2>
+        <div>点击消息之后进行页面跳转</div>
+      </div>
+      <div>
+        <h2>5. 跳转小程序Appid</h2>
+        <div>点击消息之后打开对应的小程序</div>
+      </div>
+      <div>
+        <h2>6. 跳转小程序具体路径</h2>
+        <div>点击消息之后跳转到小程序的具体页面</div>
         <div className="image">
           <Image width="100%" src={appId} />
         </div>
       </div>
+      <div>
+        <h2>7. 模板内容</h2>
+        <div>
+          支持填写带变量的动态模板。变量填写规范示例:${name}
+          。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
+        </div>
+      </div>
     </div>
   );
 };

+ 18 - 20
src/pages/notice/Template/Detail/index.tsx

@@ -16,7 +16,7 @@ import {
   Switch,
 } from '@formily/antd';
 import type { Field } from '@formily/core';
-import { createForm, onFieldInit, onFieldValueChange } from '@formily/core';
+import { createForm, FormPath, onFieldInit, onFieldReact, onFieldValueChange } from '@formily/core';
 import { createSchemaField, observer } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
 import styles from './index.less';
@@ -60,7 +60,7 @@ export const docMap = {
 
 const Detail = observer(() => {
   const { id } = useParams<{ id: string }>();
-  const [provider, setProvider] = useState<string>();
+  const [provider, setProvider] = useState<string>('embedded');
   // 正则提取${}里面的值
   const pattern = /(?<=\$\{).*?(?=\})/g;
 
@@ -218,18 +218,22 @@ const Detail = observer(() => {
                 ?.match(pattern)
                 ?.filter((i: string) => i)
                 .map((item: string) => ({ id: item, type: 'string', format: '--' }));
-            if (idList && idList.length > 0) {
-              form1.setFieldState('variableDefinitions', (state1) => {
-                state1.visible = true;
-              });
-            }
+            form1.setFieldState('variableDefinitions', (state1) => {
+              state1.visible = !!idList && idList.length > 0;
+            });
             if (form1.modified) {
               form1.setValuesIn('variableDefinitions', idList);
             }
           });
-          onFieldValueChange('variableDefinitions.*.type', (field) => {
+          onFieldReact('variableDefinitions.*.type', (field) => {
             const value = (field as Field).value;
-            const format = field.query('.format').take() as any;
+            const formatPath = FormPath.transform(
+              field.path,
+              /\d+/,
+              (index) => `variableDefinitions.${parseInt(index)}.format`,
+            );
+            const format = field.query(formatPath).take() as any;
+            if (!format) return;
             switch (value) {
               case 'date':
                 format.setComponent(Select);
@@ -243,8 +247,9 @@ const Detail = observer(() => {
                 format.setValue('string');
                 break;
               case 'string':
+                console.log('string');
                 format.setComponent(PreviewText.Input);
-                format.setValue('--');
+                format.setValue('%s');
                 break;
               case 'number':
                 format.setComponent(Input);
@@ -473,15 +478,6 @@ const Detail = observer(() => {
               officialMessage: {
                 type: 'void',
                 properties: {
-                  agentId: {
-                    title: 'AgentId',
-                    type: 'string',
-                    'x-decorator': 'FormItem',
-                    'x-component': 'Input',
-                    'x-component-props': {
-                      placeholder: '请输入AgentId',
-                    },
-                  },
                   tagid: {
                     title: '用户标签',
                     type: 'string',
@@ -1000,9 +996,11 @@ const Detail = observer(() => {
           fulfill: {
             state: {
               hidden: '{{$deps[0]==="aliyun"}}',
+              disabled: '{{["aliyunSms","aliyun"].includes($deps[0])}}',
             },
           },
         },
+
         'x-component-props': {
           rows: 5,
           placeholder: '变量格式:${name};\n 示例:尊敬的${name},${time}有设备触发告警,请注意处理',
@@ -1070,7 +1068,7 @@ const Detail = observer(() => {
             column4: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: '格式', width: '150px' },
+              'x-component-props': { title: '格式', width: '300px' },
               required: true,
               properties: {
                 format: {

+ 27 - 4
src/pages/notice/Template/Log/index.tsx

@@ -1,4 +1,4 @@
-import { Modal } from 'antd';
+import { Badge, Modal } from 'antd';
 import { observer } from '@formily/react';
 import { service, state } from '..';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
@@ -15,6 +15,7 @@ const Log = observer(() => {
     {
       dataIndex: 'id',
       title: 'id',
+      width: 200,
     },
     {
       dataIndex: 'sendTime',
@@ -23,6 +24,30 @@ const Log = observer(() => {
     {
       dataIndex: 'state',
       title: '状态',
+      renderText: (text: { value: string; text: string }, record) => {
+        return (
+          <>
+            <Badge status={text.value === 'success' ? 'success' : 'error'} text={text.text} />
+            {text.value !== 'success' && (
+              <a
+                key="info"
+                onClick={() => {
+                  Modal.info({
+                    title: '错误信息',
+                    width: '30vw',
+                    content: (
+                      <div style={{ height: '300px', overflowY: 'auto' }}>{record.errorStack}</div>
+                    ),
+                    onOk() {},
+                  });
+                }}
+              >
+                <InfoCircleOutlined />
+              </a>
+            )}
+          </>
+        );
+      },
     },
     {
       dataIndex: 'action',
@@ -34,9 +59,7 @@ const Log = observer(() => {
             Modal.info({
               title: '详情信息',
               width: '30vw',
-              content: (
-                <div style={{ height: '300px', overflowY: 'auto' }}>{record.errorStack}</div>
-              ),
+              content: <div style={{ height: '300px', overflowY: 'auto' }}>{record.message}</div>,
               onOk() {},
             });
           }}

+ 1 - 0
src/pages/notice/Template/index.tsx

@@ -315,6 +315,7 @@ const Template = observer(() => {
                     actionRef.current?.reset?.();
                   },
                 }}
+                isPermission={templatePermission.delete}
                 key="delete"
               >
                 <DeleteOutlined />

+ 1 - 0
src/pages/notice/Template/typings.d.ts

@@ -16,4 +16,5 @@ type LogItem = {
   sendTime: number;
   state: string;
   errorStack?: string;
+  message?: string;
 };

+ 1 - 1
src/pages/rule-engine/Alarm/Config/index.tsx

@@ -10,7 +10,7 @@ import FLevelInput from '@/components/FLevelInput';
 import { IOConfigItem } from '@/pages/rule-engine/Alarm/Config/typing';
 import Service from '@/pages/rule-engine/Alarm/Config/service';
 
-const service = new Service('alarm/config');
+export const service = new Service('alarm/config');
 const Config = () => {
   const [tab, setTab] = useState<'io' | 'config' | string>('config');
   const SchemaField = createSchemaField({

+ 8 - 0
src/pages/rule-engine/Alarm/Configuration/Save/index.less

@@ -0,0 +1,8 @@
+.form {
+  :global {
+    .ant-radio-button-wrapper {
+      height: 100%;
+      margin-right: 15px;
+    }
+  }
+}

+ 202 - 0
src/pages/rule-engine/Alarm/Configuration/Save/index.tsx

@@ -0,0 +1,202 @@
+import { message, Modal, Typography } from 'antd';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, Radio, Select } from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { PermissionButton } from '@/components';
+import { PlusOutlined } from '@ant-design/icons';
+import Service from '@/pages/rule-engine/Alarm/Configuration/service';
+import { useAsyncDataSource } from '@/utils/util';
+import styles from './index.less';
+import { service as ConfigService } from '../../Config';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+}
+
+const alarm1 = require('/public/images/alarm/alarm1.png');
+const alarm2 = require('/public/images/alarm/alarm2.png');
+const alarm3 = require('/public/images/alarm/alarm3.png');
+const alarm4 = require('/public/images/alarm/alarm4.png');
+const alarm5 = require('/public/images/alarm/alarm5.png');
+
+const service = new Service('alarm/config');
+
+const createImageLabel = (image: string, text: string) => {
+  return (
+    <div style={{ textAlign: 'center', marginTop: 10, fontSize: '15px', width: '90px' }}>
+      <img alt="" height="40px" src={image} />
+      <Typography.Text style={{ maxWidth: '50px', marginBottom: 10 }} ellipsis={{ tooltip: text }}>
+        {text}
+      </Typography.Text>
+    </div>
+  );
+};
+
+const Save = (props: Props) => {
+  const { visible, close } = props;
+
+  const LevelMap = {
+    1: alarm1,
+    2: alarm2,
+    3: alarm3,
+    4: alarm4,
+    5: alarm5,
+  };
+  const getLevel = () => {
+    return ConfigService.queryLevel().then((resp) => {
+      if (resp.status === 200) {
+        console.log(resp, 'resp');
+        return resp.result?.levels?.map((item: { level: number; title: string }) => ({
+          label: createImageLabel(LevelMap[item.level], item.title),
+          value: item.level,
+        }));
+      }
+    });
+  };
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {},
+      }),
+    [],
+  );
+
+  const getSupports = () => service.getTargetTypes();
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+      Radio,
+    },
+  });
+
+  const handleSave = async () => {
+    const data: ConfigItem = await form.submit();
+    console.log(data, 'dat');
+    const resp: any = await service.update(data);
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+  };
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+          },
+          type: {
+            title: '类型',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-reactions': '{{useAsyncDataSource(getSupports)}}',
+            'x-component-props': {
+              placeholder: '请选择类型',
+            },
+          },
+        },
+      },
+      level: {
+        title: '级别',
+        'x-decorator': 'FormItem',
+        'x-component': 'Radio.Group',
+        'x-component-props': {
+          optionType: 'button',
+          placeholder: '请选择类型',
+        },
+        'x-reactions': '{{useAsyncDataSource(getLevel)}}',
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+      },
+      sceneId: {
+        title: '关联触发场景',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-decorator-props': {
+          gridSpan: 1,
+          addonAfter: (
+            <PermissionButton
+              type="link"
+              style={{ padding: 0 }}
+              isPermission={true}
+              onClick={() => {
+                // const tab: any = window.open(`${origin}/#/system/department?save=true`);
+                // tab!.onTabSaveSuccess = (value: any) => {
+                //   form.setFieldState('orgIdList', async (state) => {
+                // state.dataSource = await getOrg().then((resp) =>
+                //   resp.result?.map((item: Record<string, unknown>) => ({
+                //     ...item,
+                //     label: item.name,
+                //     value: item.id,
+                //   })),
+                // );
+                // state.value = [...(state.value || []), value.id];
+                // });
+                // };
+              }}
+            >
+              <PlusOutlined />
+            </PermissionButton>
+          ),
+        },
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+      },
+      description: {
+        title: '说明',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        'x-component-props': {
+          placeholder: '请输入描述信息',
+        },
+      },
+    },
+  };
+
+  return (
+    <Modal
+      width="40vw"
+      visible={visible}
+      onOk={handleSave}
+      onCancel={() => close()}
+      title="新增告警"
+    >
+      <Form className={styles.form} form={form} layout="vertical">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getSupports, getLevel }} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 117 - 0
src/pages/rule-engine/Alarm/Configuration/index.tsx

@@ -0,0 +1,117 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import SearchComponent from '@/components/SearchComponent';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { PermissionButton } from '@/components';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { useRef, useState } from 'react';
+import { Space } from 'antd';
+import ProTableCard from '@/components/ProTableCard';
+import Save from './Save';
+
+const Configuration = () => {
+  const intl = useIntl();
+
+  const columns: ProColumns<ConfigItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '名称',
+    },
+    {
+      title: '类型',
+      dataIndex: 'targetType',
+    },
+    {
+      title: '告警级别',
+      dataIndex: 'level',
+    },
+    {
+      title: '关联场景联动',
+      dataIndex: 'sceneName',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      render: (_, record) => [
+        <PermissionButton
+          isPermission={true}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+          onClick={() => {
+            console.log(record);
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={true}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.remove',
+              defaultMessage: '删除',
+            }),
+          }}
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const actionRef = useRef<ActionType>();
+
+  const [param, setParam] = useState({});
+
+  const [visible, setVisible] = useState<boolean>(false);
+  return (
+    <PageContainer>
+      <SearchComponent
+        field={columns}
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTableCard<ConfigItem>
+        actionRef={actionRef}
+        rowKey="id"
+        search={false}
+        params={param}
+        columns={columns}
+        headerTitle={
+          <Space>
+            <PermissionButton
+              isPermission={true}
+              onClick={() => {
+                setVisible(true);
+              }}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </PermissionButton>
+          </Space>
+        }
+      />
+      <Save visible={visible} close={() => setVisible(false)} />
+    </PageContainer>
+  );
+};
+export default Configuration;

+ 17 - 0
src/pages/rule-engine/Alarm/Configuration/service.ts

@@ -0,0 +1,17 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ConfigItem> {
+  public getTargetTypes = () =>
+    request(`/${SystemConst.API_BASE}/alarm/config/target-type/supports`, {
+      method: 'GET',
+    }).then((resp) => {
+      return resp.result.map((item: { id: string; name: string }) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+}
+
+export default Service;

+ 9 - 0
src/pages/rule-engine/Alarm/Configuration/typings.d.ts

@@ -0,0 +1,9 @@
+type ConfigItem = {
+  name: string;
+  targetType: string;
+  level: number;
+  sceneName: string;
+  sceneId: string;
+  state: string;
+  description: string;
+};

+ 162 - 0
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -0,0 +1,162 @@
+import { Button, Form, InputNumber, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { useRequest } from 'umi';
+import { queryMessageConfig, queryMessageTemplate, queryMessageType } from './service';
+import MessageContent from './messageContent';
+
+interface ActionProps {
+  restField: any;
+  name: number;
+  title?: string;
+  onRemove: () => void;
+}
+
+const ActionItem = (props: ActionProps) => {
+  const { name } = props;
+  const [type1, setType1] = useState('');
+  const [templateData, setTemplateData] = useState<any>(undefined);
+
+  const { data: messageType, run: queryMessageTypes } = useRequest(queryMessageType, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const {
+    data: messageConfig,
+    run: queryMessageConfigs,
+    loading: messageConfigLoading,
+  } = useRequest(queryMessageConfig, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const {
+    data: messageTemplate,
+    run: queryMessageTemplates,
+    loading: messageTemplateLoading,
+  } = useRequest(queryMessageTemplate, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const MessageNodes = (
+    <>
+      <Form.Item {...props.restField} name={[name, 'notify', 'type']}>
+        <Select
+          options={messageType}
+          fieldNames={{ value: 'id', label: 'name' }}
+          placeholder={'请选择通知方式'}
+          style={{ width: 140 }}
+          onChange={async (key: string) => {
+            setTemplateData(undefined);
+            await queryMessageConfigs({
+              terms: [{ column: 'type$IN', value: key }],
+            });
+          }}
+        />
+      </Form.Item>
+      <Form.Item {...props.restField} name={[name, 'notify', 'notifierId']}>
+        <Select
+          options={messageConfig}
+          loading={messageConfigLoading}
+          fieldNames={{ value: 'id', label: 'name' }}
+          onChange={async (key: string) => {
+            setTemplateData(undefined);
+            await queryMessageTemplates({
+              terms: [{ column: 'configId', value: key }],
+            });
+          }}
+          style={{ width: 160 }}
+          placeholder={'请选择通知配置'}
+        />
+      </Form.Item>
+      <Form.Item {...props.restField} name={[name, 'notify', 'templateId']}>
+        <Select
+          options={messageTemplate}
+          loading={messageTemplateLoading}
+          fieldNames={{ value: 'id', label: 'name' }}
+          style={{ width: 160 }}
+          placeholder={'请选择通知模板'}
+          onChange={async (key: string, nodeData: any) => {
+            setTemplateData(nodeData);
+          }}
+        />
+      </Form.Item>
+    </>
+  );
+
+  const DeviceNodes = (
+    <>
+      <Select options={[]} placeholder={'请选择产品'} style={{ width: 220 }} />
+      <Select
+        defaultValue={'1'}
+        options={[
+          { label: '固定设备', value: '1' },
+          { label: '按标签', value: '2' },
+          { label: '按关系', value: '3' },
+        ]}
+        style={{ width: 120 }}
+      />
+      <Select options={[]} placeholder={'请选择'} style={{ width: 180 }} />
+      <Select
+        defaultValue={'1'}
+        options={[
+          { label: '设置属性', value: '1' },
+          { label: '功能调用', value: '2' },
+          { label: '读取属性', value: '3' },
+        ]}
+        style={{ width: 120 }}
+      />
+    </>
+  );
+
+  useEffect(() => {
+    if (type1 === 'message') {
+      queryMessageTypes();
+    }
+  }, [type1]);
+
+  const TimeTypeAfter = (
+    <Select
+      defaultValue={'second'}
+      options={[
+        { label: '秒', value: 'second' },
+        { label: '分', value: 'minute' },
+        { label: '小时', value: 'hour' },
+      ]}
+    />
+  );
+
+  return (
+    <div className={'actions-item'}>
+      <div className={'actions-item-title'}>
+        执行动作 {props.title} <Button onClick={props.onRemove}>删除</Button>
+      </div>
+      <div style={{ display: 'flex', gap: 12 }}>
+        <Form.Item {...props.restField} name={[name, 'executor']}>
+          <Select
+            options={[
+              { label: '消息通知', value: 'message' },
+              { label: '设备输出', value: 'device' },
+              { label: '延迟执行', value: 'delay' },
+            ]}
+            style={{ width: 100 }}
+            onSelect={(key: string) => {
+              setType1(key);
+            }}
+          />
+        </Form.Item>
+        {type1 === 'message' && MessageNodes}
+        {type1 === 'device' && DeviceNodes}
+        {type1 === 'delay' && (
+          <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        )}
+        {type1 === 'message' && templateData ? (
+          <MessageContent template={templateData} name={props.name} />
+        ) : null}
+      </div>
+    </div>
+  );
+};
+
+export default ActionItem;

+ 18 - 0
src/pages/rule-engine/Scene/Save/action/index.less

@@ -0,0 +1,18 @@
+.actions-items {
+  padding: 26px 24px 12px 24px;
+  background-color: #fafafa;
+
+  .actions-item {
+    .actions-item-title {
+      padding-bottom: 18px;
+    }
+
+    &:not(:first-child) {
+      margin-top: 18px;
+    }
+
+    .template-variable {
+      margin-top: 12px;
+    }
+  }
+}

+ 26 - 0
src/pages/rule-engine/Scene/Save/action/index.tsx

@@ -0,0 +1,26 @@
+import ActionItem from './action';
+import './index.less';
+import { ProFormList } from '@ant-design/pro-form';
+
+export default () => {
+  return (
+    <div className={'actions-items'}>
+      <ProFormList
+        name={'actions'}
+        creatorButtonProps={{
+          creatorButtonText: '新增',
+        }}
+      >
+        {(meta, index, action) => {
+          return (
+            <ActionItem
+              onRemove={() => {
+                action.remove?.(index);
+              }}
+            />
+          );
+        }}
+      </ProFormList>
+    </div>
+  );
+};

+ 60 - 0
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -0,0 +1,60 @@
+import { Col, Form, Input, Row, Select, TimePicker } from 'antd';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+
+interface MessageContentProps {
+  name: number;
+  template?: any;
+}
+
+const rowGutter = 12;
+
+export default (props: MessageContentProps) => {
+  const inputNodeByType = (data: any) => {
+    switch (data.type) {
+      case 'enum':
+        return <Select placeholder={`请选择${data.name}`} style={{ width: '100%' }} />;
+      case 'timmer':
+        return <TimePicker style={{ width: '100%' }} />;
+      case 'number':
+        return <Input placeholder={`请输入${data.name}`} style={{ width: '100%' }} />;
+      default:
+        return <Input placeholder={`请输入${data.name}`} />;
+    }
+  };
+
+  return (
+    <>
+      {props.template && (
+        <div className={'template-variable'}>
+          {props.template.variableDefinitions ? (
+            <Row gutter={rowGutter}>
+              {props.template.variableDefinitions.map((item: any) => {
+                // const rules = !item.required ? [{ required: true, message: '请输入'+ item.name }] : undefined
+                return (
+                  <Col span={12} key={item.id}>
+                    <Form.Item
+                      name={[props.name, 'notify', 'variables', item.id]}
+                      label={item.name}
+                    >
+                      <ItemGroup>
+                        <Select
+                          defaultValue={'1'}
+                          options={[
+                            { label: '手动输入', value: '1' },
+                            { label: '内置参数', value: '2' },
+                          ]}
+                          style={{ width: 120 }}
+                        />
+                        {inputNodeByType(item)}
+                      </ItemGroup>
+                    </Form.Item>
+                  </Col>
+                );
+              })}
+            </Row>
+          ) : null}
+        </div>
+      )}
+    </>
+  );
+};

+ 28 - 0
src/pages/rule-engine/Scene/Save/action/service.ts

@@ -0,0 +1,28 @@
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+export const queryMessageType = () =>
+  request(`${SystemConst.API_BASE}/notifier/config/types`, { method: 'GET' });
+
+// 通知配置
+export const queryMessageConfig = (data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/config/_query/no-paging?paging=false`, {
+    method: 'POST',
+    data,
+  });
+
+// 通知模板
+export const queryMessageTemplate = (data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/template/_query/no-paging?paging=false`, {
+    method: 'POST',
+    data,
+  });
+
+export const queryProductList = (data?: any) =>
+  request(`${SystemConst.API_BASE}/device-product/_query/no-paging?paging=false`, {
+    method: 'POST',
+    data,
+  });
+
+export const queryDeviceSelector = () =>
+  request(`${SystemConst.API_BASE}/scene/device-selectors`, { method: 'GET' });

+ 7 - 0
src/pages/rule-engine/Scene/Save/components/ItemGroup/index.less

@@ -0,0 +1,7 @@
+.group-item-compact {
+  display: flex;
+
+  &:nth-last-child(1) {
+    flex: 1;
+  }
+}

+ 10 - 0
src/pages/rule-engine/Scene/Save/components/ItemGroup/index.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+import './index.less';
+
+interface ItemGroupProps {
+  children?: React.ReactNode;
+}
+
+export default (props: ItemGroupProps) => {
+  return <div className={'group-item-compact'}>{props.children}</div>;
+};

+ 49 - 0
src/pages/rule-engine/Scene/Save/components/TimeSelect/index.tsx

@@ -0,0 +1,49 @@
+import { TreeSelect } from 'antd';
+import React, { useCallback, useEffect, useState } from 'react';
+
+type OptionsItemType = {
+  label: string;
+  value: string | number;
+};
+
+interface TimeSelectProps {
+  options?: OptionsItemType[];
+  value?: string;
+  onChange?: (value: string[]) => void;
+  style?: React.CSSProperties;
+}
+
+export default (props: TimeSelectProps) => {
+  const [checkedKeys, setCheckedKeys] = useState<any[]>([]);
+
+  const onChange = useCallback(
+    (keys: string[], _, extra) => {
+      if (extra.triggerValue === 'all') {
+        const newKeys = extra.checked ? ['all', ...props.options!.map((item) => item.value)] : [];
+        setCheckedKeys(newKeys);
+      } else {
+        const noAllKeys = keys.filter((key) => key !== 'all');
+        const newKeys = noAllKeys.length === props.options!.length ? ['all', ...keys] : noAllKeys;
+
+        setCheckedKeys(newKeys);
+      }
+    },
+    [checkedKeys, props.options],
+  );
+
+  useEffect(() => {}, [checkedKeys]);
+
+  return (
+    <TreeSelect
+      treeCheckable
+      value={checkedKeys}
+      onChange={onChange}
+      style={props.style}
+      treeData={
+        props.options && props.options.length
+          ? [{ label: '全部', value: 'all' }, ...props.options]
+          : []
+      }
+    />
+  );
+};

+ 89 - 0
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx

@@ -0,0 +1,89 @@
+import { Input, InputNumber, Select, TimePicker } from 'antd';
+import { TimeSelect } from '@/pages/rule-engine/Scene/Save/components';
+import { useState } from 'react';
+
+export default () => {
+  const [type1, setType1] = useState(1);
+  const [type2, setType2] = useState(1);
+
+  const type1Select = async (key: number) => {
+    setType1(key);
+    if (key !== 3) {
+      // TODO 请求后端返回天数
+    }
+  };
+
+  const type2Select = (key: number) => {
+    setType2(key);
+  };
+
+  const TimeTypeAfter = (
+    <Select
+      defaultValue={'second'}
+      options={[
+        { label: '秒', value: 'second' },
+        { label: '分', value: 'minute' },
+        { label: '小时', value: 'hour' },
+      ]}
+    />
+  );
+
+  const implementNode =
+    type2 === 1 ? (
+      <>
+        <TimePicker.RangePicker />
+        <span> 每 </span>
+        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <span> 执行一次 </span>
+      </>
+    ) : (
+      <>
+        <TimePicker />
+        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <span> 执行一次 </span>
+      </>
+    );
+
+  return (
+    <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
+      <Select
+        options={[
+          { label: '按周', value: 1 },
+          { label: '按月', value: 2 },
+          { label: 'cron表达式', value: 3 },
+        ]}
+        value={type1}
+        onSelect={type1Select}
+        style={{ width: 120 }}
+      />
+      {type1 !== 3 ? (
+        <>
+          <TimeSelect
+            options={[
+              { label: '周一', value: 1 },
+              { label: '周二', value: 2 },
+              { label: '周三', value: 3 },
+              { label: '周四', value: 4 },
+              { label: '周五', value: 5 },
+              { label: '周六', value: 6 },
+              { label: '周末', value: 7 },
+            ]}
+            style={{ width: 150 }}
+          />
+          <Select
+            options={[
+              { label: '周期执行', value: 1 },
+              { label: '执行一次', value: 2 },
+            ]}
+            value={type2}
+            style={{ width: 150 }}
+            onSelect={type2Select}
+          />
+          {implementNode}
+        </>
+      ) : (
+        <Input placeholder={'请输入cron表达式'} style={{ width: 400 }} />
+      )}
+    </div>
+  );
+};

+ 24 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less

@@ -0,0 +1,24 @@
+@import '~antd/es/style/themes/default.less';
+
+.trigger-way-warp {
+  display: flex;
+  gap: 20px;
+  width: 100%;
+
+  .trigger-way-item {
+    padding: 20px 16px;
+    border: 1px solid #e0e4e8;
+    border-radius: 2px;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+      color: @primary-color-hover;
+    }
+
+    &.active {
+      color: @primary-color-active;
+      border-color: @primary-color-active;
+    }
+  }
+}

+ 65 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx

@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react';
+import classNames from 'classnames';
+import './index.less';
+
+interface TriggerWayProps {
+  value?: string;
+  onChange?: (type: string) => void;
+}
+
+enum TriggerWayType {
+  manual = 'manual',
+  timing = 'timer',
+  device = 'device',
+}
+
+export default (props: TriggerWayProps) => {
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  const onSelect = (_type: string) => {
+    setType(_type);
+
+    if (props.onChange) {
+      props.onChange(_type);
+    }
+  };
+
+  return (
+    <div className={'trigger-way-warp'}>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.manual,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.manual);
+        }}
+      >
+        手动触发
+      </div>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.timing,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.timing);
+        }}
+      >
+        定时触发
+      </div>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.device,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.device);
+        }}
+      >
+        设备触发
+      </div>
+    </div>
+  );
+};

+ 4 - 0
src/pages/rule-engine/Scene/Save/components/index.ts

@@ -0,0 +1,4 @@
+export { default as TimeSelect } from './TimeSelect';
+export { default as TimingTrigger } from './TimingTrigger';
+export { default as TriggerWay } from './TriggerWay';
+export { default as ItemGroup } from './ItemGroup';

+ 67 - 1
src/pages/rule-engine/Scene/Save/index.tsx

@@ -1 +1,67 @@
-export default () => {};
+import { PageContainer } from '@ant-design/pro-layout';
+import { Button, Card, Form } from 'antd';
+import { useLocation } from 'umi';
+import { useEffect } from 'react';
+import { PermissionButton } from '@/components';
+import ActionItems from './action/action';
+import { PlusOutlined } from '@ant-design/icons';
+
+export default () => {
+  const location = useLocation();
+  const [form] = Form.useForm();
+
+  const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
+
+  const getDetail = async () => {
+    // TODO 回显数据
+  };
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    const id = params.get('id');
+    if (id) {
+      getDetail();
+    }
+  }, [location]);
+
+  const saveData = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+  };
+
+  return (
+    <PageContainer>
+      <Card>
+        <Form form={form} autoComplete="off">
+          <Form.List name="users">
+            {(fields, { add, remove }) => (
+              <>
+                {fields.map(({ key, name, ...restField }) => (
+                  <ActionItems
+                    key={key}
+                    restField={restField}
+                    onRemove={() => remove(name)}
+                    name={name}
+                  />
+                ))}
+                <Form.Item>
+                  <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
+                    新增
+                  </Button>
+                </Form.Item>
+              </>
+            )}
+          </Form.List>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              Submit
+            </Button>
+          </Form.Item>
+        </Form>
+        <PermissionButton isPermission={getOtherPermission(['add', 'update'])} onClick={saveData}>
+          保存
+        </PermissionButton>
+      </Card>
+    </PageContainer>
+  );
+};

+ 10 - 0
src/pages/rule-engine/Scene/Save/trigger/index.tsx

@@ -0,0 +1,10 @@
+import { Form } from 'antd';
+import TriggerWay from '../components/TriggerWay';
+
+export default () => {
+  return (
+    <Form.Item>
+      <TriggerWay />
+    </Form.Item>
+  );
+};

+ 360 - 0
src/pages/rule-engine/Scene/Save2/index.tsx

@@ -0,0 +1,360 @@
+import {
+  ArrayItems,
+  DatePicker,
+  Editable,
+  FormButtonGroup,
+  FormGrid,
+  FormItem,
+  FormLayout,
+  Input,
+  NumberPicker,
+  Radio,
+  Select,
+  Space,
+  Submit,
+} from '@formily/antd';
+import type { Field } from '@formily/core';
+import { createForm, FieldDataSource, onFieldReact, onFieldValueChange } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { action } from '@formily/reactive';
+import {
+  queryMessageConfig,
+  queryMessageTemplate,
+  queryMessageType,
+  queryProductList,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+import { Card } from 'antd';
+import { useMemo } from 'react';
+import type { ISchema } from '@formily/json-schema';
+
+export default () => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      DatePicker,
+      Space,
+      Radio,
+      Input,
+      Select,
+      ArrayItems,
+      FormLayout,
+      FormGrid,
+      NumberPicker,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          layout: 'vertical',
+        },
+        properties: {
+          actions: {
+            type: 'array',
+            'x-component': 'ArrayItems',
+            'x-decorator': 'FormItem',
+            title: '执行动作',
+            items: {
+              type: 'object',
+              title: '执行动作',
+              properties: {
+                space: {
+                  type: 'void',
+                  'x-component': 'Space',
+                  properties: {
+                    executor: {
+                      type: 'string',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'Select',
+                      'x-component-props': {
+                        style: {
+                          width: 160,
+                        },
+                      },
+                      enum: [
+                        { label: '消息通知', value: 'message' },
+                        { label: '设备输出', value: 'device' },
+                        { label: '延迟执行', value: 'delay' },
+                      ],
+                    },
+                    notify: {
+                      type: 'object',
+                      'x-decorator': 'FormItem',
+                      'x-reactions': [
+                        {
+                          dependencies: ['.executor'],
+                          fulfill: {
+                            state: {
+                              visible: "{{$deps[0] === 'message'}}",
+                            },
+                          },
+                        },
+                      ],
+                      properties: {
+                        grid: {
+                          type: 'void',
+                          'x-component': 'FormGrid',
+                          'x-component-props': {
+                            minColumns: [4, 6, 10],
+                          },
+                          properties: {
+                            messageType: {
+                              type: 'string',
+                              'x-decorator': 'FormItem',
+                              'x-component': 'Select',
+                              'x-component-props': {
+                                style: { width: 160 },
+                                fieldNames: { label: 'name', value: 'id' },
+                              },
+                              'x-reactions': ['{{useAsyncDataSource(getMessageType)}}'],
+                            },
+                            notifierId: {
+                              type: 'string',
+                              'x-decorator': 'FormItem',
+                              'x-component': 'Select',
+                              'x-component-props': {
+                                style: { width: 160 },
+                                fieldNames: { label: 'name', value: 'id' },
+                              },
+                            },
+                            templateId: {
+                              type: 'string',
+                              'x-decorator': 'FormItem',
+                              'x-component': 'Select',
+                              'x-component-props': {
+                                style: { width: 160 },
+                              },
+                            },
+                          },
+                        },
+                      },
+                    },
+                    variables: {
+                      type: 'object',
+                      'x-decorator': 'FormItem',
+                      properties: {},
+                      'x-reactions': [
+                        {
+                          dependencies: ['.executor'],
+                          fulfill: {
+                            state: {
+                              visible: "{{$deps[0] === 'message'}}",
+                            },
+                          },
+                        },
+                      ],
+                    },
+                    device: {
+                      type: 'object',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'Space',
+                      'x-reactions': [
+                        {
+                          dependencies: ['.executor'],
+                          fulfill: {
+                            state: {
+                              visible: "{{$deps[0] === 'device'}}",
+                            },
+                          },
+                        },
+                      ],
+                      properties: {
+                        productId: {
+                          type: 'string',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Select',
+                          'x-component-props': {
+                            style: { width: 200 },
+                            fieldNames: { label: 'name', value: 'id' },
+                          },
+                          'x-reactions': ['{{useAsyncDataSource(getProductList)}}'],
+                        },
+                        selector: {
+                          type: 'string',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Select',
+                          'x-component-props': {
+                            style: { width: 200 },
+                          },
+                          enum: [
+                            { label: '固定设备', value: 'device' },
+                            { label: '按标签', value: 'tag' },
+                            { label: '按关系', value: 'relation' },
+                          ],
+                        },
+                        'message.messageType': {
+                          type: 'string',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Select',
+                          'x-component-props': {
+                            style: { width: 160 },
+                          },
+                          enum: [
+                            { label: '功能调用', value: 'INVOKE_FUNCTION' },
+                            { label: '读取属性', value: 'READ_PROPERTY' },
+                            { label: '设置属性', value: 'WRITE_PROPERTY' },
+                          ],
+                        },
+                        value: {
+                          type: 'object',
+                          'x-decorator': 'FormItem',
+                        },
+                      },
+                    },
+                    delay: {
+                      type: 'object',
+                      'x-decorator': 'FormItem',
+                      'x-reactions': [
+                        {
+                          dependencies: ['.executor'],
+                          fulfill: {
+                            state: {
+                              visible: "{{$deps[0] === 'delay'}}",
+                            },
+                          },
+                        },
+                      ],
+                      properties: {
+                        time: {
+                          type: 'number',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'NumberPicker',
+                          'x-component-props': {
+                            style: {
+                              width: 240,
+                            },
+                          },
+                        },
+                        unit: {
+                          type: 'string',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Select',
+                          'x-component-props': {
+                            style: {
+                              width: 160,
+                            },
+                          },
+                          enum: [
+                            { label: '秒', value: 'seconds' },
+                            { label: '分', value: 'minutes' },
+                            { label: '小时', value: 'hours' },
+                          ],
+                        },
+                      },
+                    },
+                  },
+                },
+                remove: {
+                  type: 'void',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'ArrayItems.Remove',
+                },
+              },
+            },
+            properties: {
+              add: {
+                type: 'void',
+                title: '添加条目',
+                'x-component': 'ArrayItems.Addition',
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const form = useMemo(
+    () =>
+      createForm({
+        effects: () => {
+          onFieldReact('actions.*.notify.notifierId', async (field, f) => {
+            const key = field.query('.messageType').get('value');
+            f.clearFormGraph('.variables');
+            (field as Field).value = undefined;
+            if (key) {
+              (field as Field).loading = true;
+              const resp = await queryMessageConfig({ terms: [{ column: 'type$IN', value: key }] });
+              (field as Field).loading = false;
+              if (resp.status === 200) {
+                (field as Field).dataSource = resp.result;
+              }
+            }
+          });
+          onFieldReact('actions.*.notify.templateId', async (field) => {
+            const key = field.query('.notifierId').get('value');
+            (field as Field).value = undefined;
+            if (key) {
+              (field as Field).loading = true;
+              const resp = await queryMessageTemplate({
+                terms: [{ column: 'configId', value: key }],
+              });
+              (field as Field).loading = false;
+              if (resp.status === 200) {
+                (field as Field).dataSource = resp.result.map((item: any) => ({
+                  label: item.name,
+                  value: item.id,
+                  data: item,
+                }));
+              }
+            }
+          });
+          onFieldValueChange('actions.*.notify.templateId', async (field) => {
+            console.log(field);
+
+            const templateData = field.dataSource.find((item) => item.value === field.value);
+            if (templateData) {
+              const data = templateData.data;
+              if (data.variableDefinitions) {
+                const obj = {};
+                data.variableDefinitions.forEach((item: any) => {
+                  obj[item.id] = {
+                    title: item.name,
+                    type: 'string',
+                    'x-decorator': 'FormItem',
+                    'x-component': 'Input',
+                  };
+                });
+              }
+            }
+          });
+        },
+      }),
+    [],
+  );
+
+  const getMessageType = async () => await queryMessageType();
+
+  const getProductList = async () =>
+    await queryProductList({ sorts: [{ name: 'createTime', order: 'desc' }] });
+
+  const useAsyncDataSource =
+    (services: (arg0: Field) => Promise<FieldDataSource>) => (field: Field) => {
+      field.loading = true;
+      services(field).then(
+        action.bound!((resp: any) => {
+          field.dataSource = resp.result;
+          field.loading = false;
+        }),
+      );
+    };
+
+  return (
+    <Card>
+      <FormProvider form={form}>
+        <SchemaField
+          schema={schema}
+          scope={{ useAsyncDataSource, getMessageType, getProductList }}
+        />
+        <FormButtonGroup>
+          <Submit onSubmit={console.log}>提交</Submit>
+        </FormButtonGroup>
+      </FormProvider>
+    </Card>
+  );
+};

+ 12 - 4
src/pages/rule-engine/Scene/index.tsx

@@ -1,5 +1,5 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useRef, useState } from 'react';
+import React, { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
 import { Badge, message } from 'antd';
@@ -8,7 +8,10 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { PermissionButton, ProTableCard } from '@/components';
 import { statusMap } from '@/pages/device/Instance';
 import SearchComponent from '@/components/SearchComponent';
+import SceneCard from '@/components/ProTableCard/CardItems/scene';
 import Service from './service';
+import { useHistory } from 'umi';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export const service = new Service('rule-engine/scene');
 
@@ -17,8 +20,9 @@ const Scene = () => {
   const actionRef = useRef<ActionType>();
   const { permission } = PermissionButton.usePermission('rule-engine/Scene');
   const [searchParams, setSearchParams] = useState<any>({});
+  const history = useHistory();
 
-  const Tools = (record: any, type: 'card' | 'table') => {
+  const Tools = (record: any, type: 'card' | 'table'): React.ReactNode[] => {
     return [
       <PermissionButton
         key={'update'}
@@ -189,7 +193,7 @@ const Scene = () => {
           setSearchParams(data);
         }}
       />
-      <ProTableCard
+      <ProTableCard<SceneItem>
         columns={columns}
         actionRef={actionRef}
         params={searchParams}
@@ -213,7 +217,10 @@ const Scene = () => {
             icon={<PlusOutlined />}
             type="primary"
             isPermission={permission.add}
-            onClick={() => {}}
+            onClick={() => {
+              const url = getMenuPathByCode('rule-engine/Scene/Save');
+              history.push(url);
+            }}
           >
             {intl.formatMessage({
               id: 'pages.data.option.add',
@@ -221,6 +228,7 @@ const Scene = () => {
             })}
           </PermissionButton>,
         ]}
+        cardRender={(record) => <SceneCard {...record} tools={Tools(record, 'card')} />}
       />
     </PageContainer>
   );

+ 6 - 3
src/pages/rule-engine/Scene/typings.d.ts

@@ -1,4 +1,4 @@
-import type { BaseItem, State } from '@/utils/typings';
+import type { State } from '@/utils/typings';
 
 type Action = {
   executor: string;
@@ -10,9 +10,12 @@ type Trigger = {
   device: Record<string, unknown>;
 };
 
-type SceneItem = {
+interface SceneItem {
   parallel: boolean;
   state: State;
   actions: Action[];
   triggers: Trigger[];
-} & BaseItem;
+  id: string;
+  name: string;
+  describe: string;
+}

+ 4 - 1
src/pages/system/Menu/Detail/edit.tsx

@@ -159,7 +159,10 @@ export default (props: EditProps) => {
                       defaultMessage: '名称',
                     })}
                     required={true}
-                    rules={[{ required: true, message: '请输入名称' }]}
+                    rules={[
+                      { required: true, message: '请输入名称' },
+                      { max: 64, message: '最多可输入64个字符' },
+                    ]}
                   >
                     <Input disabled={disabled} placeholder={'请输入名称'} />
                   </Form.Item>

+ 6 - 5
src/pages/system/User/ResetPassword/index.tsx

@@ -56,6 +56,7 @@ const ResetPassword = (props: Props) => {
             message: '密码最多可输入128位',
           },
           {
+            min: 8,
             message: '密码不能少于6位',
           },
           {
@@ -82,8 +83,8 @@ const ResetPassword = (props: Props) => {
             message: '密码最多可输入128位',
           },
           {
-            min: 6,
-            message: '密码不能少于6位',
+            min: 8,
+            message: '密码不能少于8位',
           },
           {
             required: true,
@@ -107,7 +108,7 @@ const ResetPassword = (props: Props) => {
     },
   };
 
-  const form = useMemo(() => createForm({}), []);
+  const form = useMemo(() => createForm({}), [props.visible]);
   return (
     <Modal
       title="重置密码"
@@ -119,10 +120,10 @@ const ResetPassword = (props: Props) => {
           const resp = await service.resetPassword(props.data.id, value.confirmPassword);
           if (resp.status === 200) {
             message.success('操作成功');
+            props.close();
           }
-        } else {
-          props.close();
         }
+        props.close();
       }}
     >
       <Form form={form} layout="vertical">

+ 9 - 2
src/utils/menu/index.ts

@@ -1,7 +1,8 @@
 // 路由components映射
 import type { IRouteProps } from 'umi';
 import type { MenuItem } from '@/pages/system/Menu/typing';
-import { BUTTON_PERMISSION, getDetailNameByCode, MENUS_CODE, MENUS_CODE_TYPE } from './router';
+import type { BUTTON_PERMISSION, MENUS_CODE_TYPE } from './router';
+import { getDetailNameByCode, MENUS_CODE } from './router';
 
 /** localStorage key */
 export const MENUS_DATA_CACHE = 'MENUS_DATA_CACHE';
@@ -29,6 +30,12 @@ const extraRouteObj = {
       { code: 'Playback', name: '回放' },
     ],
   },
+  'rule-engine/Scene': {
+    children: [
+      { code: 'Save', name: '详情' },
+      { code: 'Save2', name: '测试详情' },
+    ],
+  },
 };
 
 /**
@@ -257,7 +264,7 @@ export const getButtonPermission = (
  * 通过缓存的数据取出相应的路由url
  * @param code
  */
-export const getMenuPathByCode = (code: string): string => {
+export const getMenuPathByCode = (code: MENUS_CODE_TYPE): string => {
   const menusStr = localStorage.getItem(MENUS_DATA_CACHE) || '{}';
   const menusData = JSON.parse(menusStr);
   return menusData[code];

+ 3 - 0
src/utils/menu/router.ts

@@ -63,6 +63,9 @@ export enum MENUS_CODE {
   'rule-engine/Alarm/Log' = 'rule-engine/Alarm/Log',
   'rule-engine/Alarm/Log/Detail' = 'rule-engine/Alarm/Log/Detail',
   'rule-engine/Alarm/Config' = 'rule-engine/Alarm/Config',
+  'rule-engine/Scene/Save' = 'rule-engine/Scene/Save',
+  'rule-engine/Scene/Save2' = 'rule-engine/Scene/Save2',
+  'rule-engine/Alarm/Configuration' = 'rule-engine/Alarm/Configuration',
   'simulator/Device' = 'simulator/Device',
   'system/DataSource' = 'system/DataSource',
   'system/Department/Assets' = 'system/Department/Assets',