Wzyyy98 преди 3 години
родител
ревизия
32dc1e5bee
променени са 43 файла, в които са добавени 2505 реда и са изтрити 166 реда
  1. 3 1
      .github/workflows/docker.yml
  2. 46 0
      src/components/ProTableCard/CardItems/noticeConfig.tsx
  3. 46 10
      src/components/ProTableCard/CardItems/noticeTemplate.tsx
  4. 55 14
      src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx
  5. 45 31
      src/pages/rule-engine/Scene/Save/action/ListItem/List.tsx
  6. 37 0
      src/pages/rule-engine/Scene/Save/action/ListItem/index.less
  7. 36 6
      src/pages/rule-engine/Scene/Save/action/Modal/add.tsx
  8. 5 0
      src/pages/rule-engine/Scene/Save/action/index.less
  9. 24 5
      src/pages/rule-engine/Scene/Save/action/index.tsx
  10. 102 3
      src/pages/rule-engine/Scene/Save/action/notify/NotifyConfig.tsx
  11. 92 3
      src/pages/rule-engine/Scene/Save/action/notify/NotifyTemplate.tsx
  12. 47 14
      src/pages/rule-engine/Scene/Save/action/notify/NotifyWay.tsx
  13. 172 0
      src/pages/rule-engine/Scene/Save/action/notify/VariableDefinitions.tsx
  14. 13 21
      src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.less
  15. 13 7
      src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.tsx
  16. 140 0
      src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/buildIn.tsx
  17. 69 0
      src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/inputFile.tsx
  18. 67 0
      src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/org.tsx
  19. 50 0
      src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/tag.tsx
  20. 315 0
      src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/user.tsx
  21. 129 34
      src/pages/rule-engine/Scene/Save/action/notify/index.tsx
  22. 12 0
      src/pages/rule-engine/Scene/Save/action/service.ts
  23. 3 1
      src/pages/rule-engine/Scene/Save/components/Buttons/AddButton.tsx
  24. 98 0
      src/pages/rule-engine/Scene/Save/components/Buttons/Dropdown.tsx
  25. 35 1
      src/pages/rule-engine/Scene/Save/components/Buttons/index.less
  26. 1 0
      src/pages/rule-engine/Scene/Save/components/Buttons/index.tsx
  27. 25 0
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/components/MTimePicker/index.less
  28. 22 0
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/components/MTimePicker/index.tsx
  29. 57 0
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.less
  30. 112 0
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.tsx
  31. 1 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType.tsx
  32. 1 2
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less
  33. 3 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx
  34. 86 1
      src/pages/rule-engine/Scene/Save/device/index.tsx
  35. 57 0
      src/pages/rule-engine/Scene/Save/index.tsx
  36. 6 2
      src/pages/rule-engine/Scene/Save/save.tsx
  37. 104 0
      src/pages/rule-engine/Scene/Save/terms/branchItem.tsx
  38. 147 0
      src/pages/rule-engine/Scene/Save/terms/index.less
  39. 29 0
      src/pages/rule-engine/Scene/Save/terms/index.tsx
  40. 87 0
      src/pages/rule-engine/Scene/Save/terms/paramsItem.tsx
  41. 98 0
      src/pages/rule-engine/Scene/Save/terms/term.tsx
  42. 7 7
      src/pages/rule-engine/Scene/index.tsx
  43. 8 3
      src/pages/rule-engine/Scene/typings.d.ts

+ 3 - 1
.github/workflows/docker.yml

@@ -1,5 +1,7 @@
 name: build images
-on: [push]
+on: 
+    push: 
+        branches: ["master", "2.0", "next"]
 jobs:
   build-and-deploy:
     runs-on: ubuntu-latest

+ 46 - 0
src/components/ProTableCard/CardItems/noticeConfig.tsx

@@ -3,13 +3,59 @@ import { Ellipsis, TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
 import { imgMap, typeList } from './noticeTemplate';
+import { CheckOutlined } from '@ant-design/icons';
 
 export interface NoticeCardProps extends ConfigItem {
   detail?: React.ReactNode;
   actions?: React.ReactNode[];
   avatarSize?: number;
+  className?: string;
+  onUnBind?: (e: any) => void;
+  showBindBtn?: boolean;
+  cardType?: 'bind' | 'unbind';
+  showTool?: boolean;
+  onClick?: () => void;
 }
 
+export const ExtraNoticeConfigCard = (props: NoticeCardProps) => {
+  return (
+    <TableCard
+      showTool={props.showTool}
+      showMask={false}
+      showStatus={false}
+      actions={props.actions}
+      onClick={props.onClick}
+      className={props.className}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={imgMap[props.type][props.provider]} alt={props.type} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>通知方式</label>
+              <Ellipsis title={typeList[props.type][props.provider] || '暂无'} />
+            </div>
+            <div>
+              <label>说明</label>
+              <Ellipsis title={props.description} />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className={'checked-icon'}>
+        <div>
+          <CheckOutlined />
+        </div>
+      </div>
+    </TableCard>
+  );
+};
+
 export default (props: NoticeCardProps) => {
   return (
     <TableCard detail={props.detail} actions={props.actions} showStatus={false} showMask={false}>

+ 46 - 10
src/components/ProTableCard/CardItems/noticeTemplate.tsx

@@ -2,11 +2,18 @@ import React from 'react';
 import { Ellipsis, TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
+import { CheckOutlined } from '@ant-design/icons';
 
 export interface NoticeCardProps extends TemplateItem {
   detail?: React.ReactNode;
   actions?: React.ReactNode[];
   avatarSize?: number;
+  className?: string;
+  onUnBind?: (e: any) => void;
+  showBindBtn?: boolean;
+  cardType?: 'bind' | 'unbind';
+  showTool?: boolean;
+  onClick?: () => void;
 }
 
 export const imgMap = {
@@ -55,6 +62,45 @@ export const typeList = {
   },
 };
 
+export const ExtraNoticeTemplateCard = (props: NoticeCardProps) => {
+  return (
+    <TableCard
+      showTool={props.showTool}
+      showMask={false}
+      showStatus={false}
+      actions={props.actions}
+      onClick={props.onClick}
+      className={props.className}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={imgMap[props.type][props.provider]} alt={props.type} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>通知方式</label>
+              <Ellipsis title={typeList[props.type][props.provider] || '暂无'} />
+            </div>
+            <div>
+              <label>说明</label>
+              <Ellipsis title={props.description} />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className={'checked-icon'}>
+        <div>
+          <CheckOutlined />
+        </div>
+      </div>
+    </TableCard>
+  );
+};
+
 export default (props: NoticeCardProps) => {
   return (
     <TableCard actions={props.actions} showStatus={false} detail={props.detail} showMask={false}>
@@ -64,11 +110,6 @@ export default (props: NoticeCardProps) => {
         </div>
         <div className={'card-item-body'}>
           <div className={'card-item-header'}>
-            {/*<span className={'card-item-header-name ellipsis'}>*/}
-            {/*  <Tooltip placement="topLeft" title={props.name}>*/}
-            {/*    {props.name}*/}
-            {/*  </Tooltip>*/}
-            {/*</span>*/}
             <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
           </div>
           <div className={'card-item-content'}>
@@ -80,11 +121,6 @@ export default (props: NoticeCardProps) => {
             <div>
               <label>说明</label>
               <Ellipsis tooltip={{ placement: 'topLeft' }} title={props.description} />
-              {/*<div className={'ellipsis'}>*/}
-              {/*  <Tooltip placement="topLeft" title={props.description}>*/}
-              {/*    {props.description}*/}
-              {/*  </Tooltip>*/}
-              {/*</div>*/}
             </div>
           </div>
         </div>

+ 55 - 14
src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx

@@ -1,26 +1,67 @@
-import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
 import { useState } from 'react';
-export type ItemType = 'serial' | 'parallel';
 import Modal from '../Modal/add';
+import type { ActionsType } from '@/pages/rule-engine/Scene/typings';
+import { DeleteOutlined } from '@ant-design/icons';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+
+export enum ParallelEnum {
+  'parallel' = 'parallel',
+  'serial' = 'serial',
+}
+
+export type ParallelType = keyof typeof ParallelEnum;
 interface ItemProps {
   name: number;
-  resetField: any;
-  remove: (index: number | number[]) => void;
-  type: ItemType;
+  data: ActionsType;
+  type: ParallelType;
 }
 
 export default (props: ItemProps) => {
   const [visible, setVisible] = useState<boolean>(false);
   return (
-    <div
-      className="actions-item"
-      onClick={() => {
-        setVisible(true);
-      }}
-    >
-      {props.name}
-      <AddButton>点击配置执行动作</AddButton>
-      {visible && <Modal />}
+    <div className="actions-item-warp">
+      <div className="actions-item">
+        <div className="item-options-warp">
+          <div className="type">
+            <img src="" />
+          </div>
+          <div
+            onClick={() => {
+              setVisible(true);
+            }}
+          >
+            {'item'}
+          </div>
+        </div>
+        <div className="item-number">{props.name + 1}</div>
+        <div
+          className="item-delete"
+          onClick={() => {
+            const indexOf = FormModel.actions.findIndex((item) => item.key === props.data.key);
+            if (props.data.key && indexOf !== -1) {
+              FormModel.actions.splice(indexOf, 1);
+            }
+          }}
+        >
+          <DeleteOutlined />
+        </div>
+      </div>
+      {props.type === 'serial' ? (
+        props.data.terms?.length ? (
+          <div></div>
+        ) : (
+          <div>添加过滤条件</div>
+        )
+      ) : null}
+      {visible && (
+        <Modal
+          name={props.name}
+          data={props.data}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
     </div>
   );
 };

+ 45 - 31
src/pages/rule-engine/Scene/Save/action/ListItem/List.tsx

@@ -1,40 +1,54 @@
-import { Button, Form } from 'antd';
-import { PlusOutlined } from '@ant-design/icons';
-import { Item } from './index';
-import type { ItemType } from './Item';
+import { useState } from 'react';
+import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
+import Modal from '../Modal/add';
 import './index.less';
-
-interface PropsType {
-  type: ItemType;
+import type { ActionsType } from '@/pages/rule-engine/Scene/typings';
+import Item from './Item';
+import type { ParallelType } from './Item';
+interface ListProps {
+  type: ParallelType;
+  actions: ActionsType[];
 }
 
-export default (props: PropsType) => {
-  const [form] = Form.useForm();
+export default (props: ListProps) => {
+  const [visible, setVisible] = useState<boolean>(false);
 
   return (
     <div className="action-list-content">
-      <Form form={form} colon={false} layout={'vertical'} preserve={false} name="actions">
-        <Form.List name="actions" initialValue={[{}]}>
-          {(fields, { add, remove }) => (
-            <div className="actions-list_form">
-              {fields.map(({ key, name, ...resetField }) => (
-                <Item
-                  key={key}
-                  name={name}
-                  resetField={resetField}
-                  remove={remove}
-                  type={props.type}
-                />
-              ))}
-              <div className="action-list-add">
-                <Button type="primary" onClick={add} ghost icon={<PlusOutlined />}>
-                  新增
-                </Button>
-              </div>
-            </div>
-          )}
-        </Form.List>
-      </Form>
+      {props.actions.map((item, index) => (
+        <Item name={index} data={item} type={props.type} key={item.key} />
+      ))}
+      <AddButton
+        onClick={() => {
+          setVisible(true);
+          // const addItem: ActionsType = {
+          //   executor: 'device',
+          //   device: {
+          //     selector: 'all',
+          //     source: 'fixed'
+          //   },
+          //   key: `${props.type}_${props.actions.length}`
+          // }
+
+          // if (props.type === 'serial') {
+          //   addItem.terms = []
+          // }
+          // console.log(addItem);
+
+          // FormModel?.actions?.push(addItem)
+        }}
+      >
+        点击配置执行动作
+      </AddButton>
+      {visible && (
+        <Modal
+          name={props.actions.length + 1}
+          data={{}}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
     </div>
   );
 };

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

@@ -1,4 +1,6 @@
 .action-list-content {
+  padding: 8px;
+
   .actions-list_form {
     .action-list-add {
       display: flex;
@@ -9,3 +11,38 @@
     }
   }
 }
+
+.actions-item {
+  position: relative;
+  margin-bottom: 24px;
+  padding: 16px;
+  border: 1px dashed #999;
+  border-radius: 2px;
+
+  .item-number {
+    position: absolute;
+    top: 0;
+    left: 16px;
+    font-weight: 800;
+    transform: translateY(-50%);
+  }
+
+  .item-delete {
+    position: absolute;
+    top: 0;
+    right: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    color: #e50012;
+    background-color: rgba(229, 0, 18, 0.1);
+    border-radius: 50%;
+    transform: translate(50%, -50%);
+
+    &:hover {
+      background-color: rgba(#e50012, 0.2);
+    }
+  }
+}

+ 36 - 6
src/pages/rule-engine/Scene/Save/action/Modal/add.tsx

@@ -1,18 +1,46 @@
 import { Modal, Form } from 'antd';
-import ActionsType from '@/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType';
-import { useState } from 'react';
+import ActionsTypeComponent from '@/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType';
+import { useEffect, useState } from 'react';
 import Notify from '../notify';
+import type { ActionsType } from '@/pages/rule-engine/Scene/typings';
+
+interface Props {
+  close: () => void;
+  data: Partial<ActionsType>;
+  name: number;
+}
 import Device from '../DeviceOutput';
-export default () => {
+export default (props: Props) => {
   const [form] = Form.useForm();
   const [actionType, setActionType] = useState<string>('');
 
+  useEffect(() => {
+    if (props.data?.executor) {
+      form.setFieldsValue({
+        type: props.data.executor,
+      });
+    }
+  }, [props.data]);
+
   const actionTypeComponent = (type: string) => {
     switch (type) {
       case 'device':
         return <Device />;
       case 'notify':
-        return <Notify />;
+        return (
+          <Notify
+            value={props.data?.notify || {}}
+            save={(data: any) => {
+              console.log(data); // value
+              setActionType('');
+              props.close();
+            }}
+            name={props.name}
+            cancel={() => {
+              setActionType('');
+            }}
+          />
+        );
       default:
         return null;
     }
@@ -23,7 +51,9 @@ export default () => {
       title="类型"
       open
       width={800}
-      onCancel={() => {}}
+      onCancel={() => {
+        props.close();
+      }}
       onOk={async () => {
         const values = await form.validateFields();
         setActionType(values.type);
@@ -36,7 +66,7 @@ export default () => {
           required
           rules={[{ required: true, message: '请选择类型' }]}
         >
-          <ActionsType />
+          <ActionsTypeComponent />
         </Form.Item>
       </Form>
       {actionTypeComponent(actionType)}

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

@@ -31,4 +31,9 @@
       width: 100%;
     }
   }
+
+  .panel-tip {
+    padding-left: 8px;
+    color: rgba(#000, 0.45);
+  }
 }

+ 24 - 5
src/pages/rule-engine/Scene/Save/action/index.tsx

@@ -1,5 +1,8 @@
 import { Collapse } from 'antd';
 import { List } from './ListItem';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import './index.less';
+import { Observer } from '@formily/react';
 
 const { Panel } = Collapse;
 
@@ -8,28 +11,44 @@ export default () => {
     <div className="actions">
       <div className="actions-title">执行</div>
       <div className="actions-warp">
-        <Collapse defaultActiveKey={['1']}>
+        <Collapse defaultActiveKey={['1', '2']}>
           <Panel
             header={
               <span>
-                串行<span>按顺序依次执行动作</span>
+                串行<span className="panel-tip">按顺序依次执行动作</span>
               </span>
             }
             key="1"
           >
             <div className="actions-list">
-              <List type="serial" />
+              <Observer>
+                {() => (
+                  <List
+                    type="serial"
+                    actions={FormModel.actions.filter((item) => 'terms' in item)}
+                  />
+                )}
+              </Observer>
             </div>
           </Panel>
           <Panel
             header={
               <span>
-                并行<span>同时执行所有动作</span>
+                并行<span className="panel-tip">同时执行所有动作</span>
               </span>
             }
             key="2"
           >
-            <div className="actions-list"></div>
+            <div className="actions-list">
+              <Observer>
+                {() => (
+                  <List
+                    type="parallel"
+                    actions={FormModel.actions.filter((item) => !('terms' in item))}
+                  />
+                )}
+              </Observer>
+            </div>
           </Panel>
         </Collapse>
       </div>

+ 102 - 3
src/pages/rule-engine/Scene/Save/action/notify/NotifyConfig.tsx

@@ -1,3 +1,102 @@
-export default () => {
-  return <div>通知配置</div>;
-};
+import { useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+import { ProTableCard } from '@/components';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { queryMessageConfigPaging } from '../service';
+import { ExtraNoticeConfigCard } from '@/components/ProTableCard/CardItems/noticeConfig';
+import { observer } from '@formily/react';
+import { NotifyModel } from './index';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const [searchParam, setSearchParam] = useState({});
+
+  const columns: ProColumns<ConfigItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+    },
+    {
+      dataIndex: 'name',
+      title: '名称',
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+    },
+  ];
+
+  return (
+    <div>
+      <SearchComponent<ConfigItem>
+        field={columns}
+        enableSave={false}
+        model={'simple'}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="scene-notify-config"
+      />
+      <div
+        style={{
+          height: 'calc(100vh - 440px)',
+          overflowY: 'auto',
+        }}
+      >
+        <ProTableCard<ConfigItem>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          cardRender={(record) => (
+            <ExtraNoticeConfigCard
+              showBindBtn={false}
+              showTool={false}
+              cardType={'bind'}
+              {...record}
+            />
+          )}
+          rowSelection={{
+            selectedRowKeys: [NotifyModel.notify?.notifierId || ''],
+            onChange: (selectedRowKeys) => {
+              if (selectedRowKeys.length) {
+                NotifyModel.notify.notifierId = String(selectedRowKeys[selectedRowKeys.length - 1]);
+              }
+            },
+          }}
+          request={(params) =>
+            queryMessageConfigPaging({
+              ...params,
+              terms: params?.terms
+                ? [
+                    ...params?.terms,
+                    {
+                      terms: [
+                        {
+                          column: 'type',
+                          termType: 'eq',
+                          value: NotifyModel.notify?.notifyType || '',
+                        },
+                      ],
+                    },
+                  ]
+                : [
+                    {
+                      column: 'type',
+                      termType: 'eq',
+                      value: NotifyModel.notify?.notifyType || '',
+                    },
+                  ],
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            })
+          }
+          params={searchParam}
+          height={'none'}
+        />
+      </div>
+    </div>
+  );
+});

+ 92 - 3
src/pages/rule-engine/Scene/Save/action/notify/NotifyTemplate.tsx

@@ -1,3 +1,92 @@
-export default () => {
-  return <div>通知模板</div>;
-};
+import { useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+import { ProTableCard } from '@/components';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { queryMessageTemplatePaging } from '../service';
+import { ExtraNoticeTemplateCard } from '@/components/ProTableCard/CardItems/noticeTemplate';
+import { observer } from '@formily/react';
+import { NotifyModel } from './index';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const [searchParam, setSearchParam] = useState({});
+
+  const columns: ProColumns<TemplateItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+    },
+    {
+      dataIndex: 'name',
+      title: '名称',
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+    },
+  ];
+
+  return (
+    <div>
+      <SearchComponent<TemplateItem>
+        field={columns}
+        enableSave={false}
+        model={'simple'}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="scene-notify-template"
+      />
+      <div
+        style={{
+          height: 'calc(100vh - 440px)',
+          overflowY: 'auto',
+        }}
+      >
+        <ProTableCard<TemplateItem>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          cardRender={(record) => (
+            <ExtraNoticeTemplateCard
+              showBindBtn={false}
+              showTool={false}
+              cardType={'bind'}
+              {...record}
+            />
+          )}
+          rowSelection={{
+            selectedRowKeys: [NotifyModel.notify?.templateId || ''],
+            onChange: (selectedRowKeys) => {
+              if (selectedRowKeys.length) {
+                NotifyModel.notify.templateId = String(selectedRowKeys[selectedRowKeys.length - 1]);
+              }
+            },
+          }}
+          request={async (params) => {
+            const resp = await queryMessageTemplatePaging(NotifyModel.notify?.notifierId || '', {
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            });
+            return {
+              code: resp.message,
+              result: {
+                data: resp.result ? resp.result : [],
+                pageIndex: 0,
+                pageSize: resp.result.length,
+                total: resp.result.length,
+              },
+              status: resp.status,
+            };
+          }}
+          params={searchParam}
+          height={'none'}
+        />
+      </div>
+    </div>
+  );
+});

+ 47 - 14
src/pages/rule-engine/Scene/Save/action/notify/NotifyWay.tsx

@@ -1,30 +1,63 @@
-import { Form } from 'antd';
+import { Form, Spin } from 'antd';
 import NotifyType from './components/notifyType';
-import { useEffect, useState } from 'react';
+import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
 import { queryMessageType } from '@/pages/rule-engine/Scene/Save/action/service';
 
-export default () => {
+interface NotifyWayProps {
+  value: string | undefined;
+}
+export default forwardRef((props: NotifyWayProps, ref) => {
   const [form] = Form.useForm();
   const [list, setList] = useState<any[]>([]);
+  const [loading, setLoading] = useState<boolean>(false);
 
   useEffect(() => {
+    setLoading(true);
     queryMessageType().then((resp) => {
       if (resp.status === 200) {
         setList(resp.result);
       }
+      setLoading(false);
     });
   }, []);
 
+  useEffect(() => {
+    if (props.value) {
+      form.setFieldsValue({
+        notifyType: props.value || '',
+      });
+    }
+  }, [props.value]);
+
+  const getValue = () => {
+    return new Promise(async (resolve) => {
+      const formData = await form.validateFields().catch(() => {
+        resolve(false);
+      });
+      if (formData?.notifyType) {
+        resolve(formData?.notifyType);
+      } else {
+        resolve(false);
+      }
+    });
+  };
+
+  useImperativeHandle(ref, () => ({
+    save: getValue,
+  }));
+
   return (
-    <Form form={form} layout={'vertical'}>
-      <Form.Item
-        name="notifyType"
-        label="应用"
-        required
-        rules={[{ required: true, message: '请选择类型' }]}
-      >
-        <NotifyType options={list} />
-      </Form.Item>
-    </Form>
+    <Spin spinning={loading}>
+      <Form form={form} layout={'vertical'}>
+        <Form.Item
+          name="notifyType"
+          label="应用"
+          required
+          rules={[{ required: true, message: '请选择类型' }]}
+        >
+          <NotifyType options={list} />
+        </Form.Item>
+      </Form>
+    </Spin>
   );
-};
+});

+ 172 - 0
src/pages/rule-engine/Scene/Save/action/notify/VariableDefinitions.tsx

@@ -0,0 +1,172 @@
+import { NotifyModel } from './index';
+import { Empty } from '@/components';
+import { Form, Input } from 'antd';
+import User from './components/variableItem/user';
+import Org from './components/variableItem/org';
+import Tag from './components/variableItem/tag';
+import BuildIn from './components/variableItem/buildIn';
+import InputFile from './components/variableItem/inputFile';
+import { forwardRef, useCallback, useImperativeHandle } from 'react';
+
+interface Props {
+  name: number;
+}
+
+export default forwardRef((props: Props, ref) => {
+  const [form] = Form.useForm();
+  const typeComponents = (item: any) => {
+    const type = item.expands?.businessType || item.type;
+    switch (type) {
+      case 'user':
+        return <User />;
+      case 'org':
+        return <Org />;
+      case 'tag':
+        return <Tag />;
+      case 'file':
+        return <InputFile />;
+      case 'link':
+        return <Input placeholder={'请输入' + item.name} />;
+      default:
+        return <BuildIn data={item} name={props.name} />;
+    }
+  };
+
+  const getRules = useCallback(
+    (item: any, type: string): any[] => {
+      const rules: any[] = [];
+      if (item?.required) {
+        if (type === 'user') {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (!value) {
+                return Promise.reject(new Error('请选择' + item.name));
+              } else {
+                if (value.source === 'fixed' && !value.value) {
+                  return Promise.reject(new Error('请输入' + item.name));
+                } else if (value.source === 'relation' && !value.value && !value.relation) {
+                  return Promise.reject(new Error('请选择' + item.name));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        } else {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (type === 'file' || type === 'link') {
+                if (!value) {
+                  return Promise.reject(new Error('请输入' + item.name));
+                }
+              } else {
+                if (!value || !value.value) {
+                  if (['date', 'org'].includes(type)) {
+                    return Promise.reject(new Error('请选择' + item.name));
+                  } else {
+                    return Promise.reject(new Error('请输入' + item.name));
+                  }
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+      }
+
+      if (type === 'link') {
+        rules.push({ max: 64, message: '最多64个字符' });
+      }
+
+      if (type === 'user') {
+        if (NotifyModel.notify?.notifyType === 'email') {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (value.value) {
+                const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
+                if (!reg.test(value.value)) {
+                  return Promise.reject(new Error('请输入正确的邮箱地址'));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+
+        if (
+          NotifyModel.notify?.notifyType &&
+          ['sms', 'voice'].includes(NotifyModel.notify?.notifyType)
+        ) {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (value.value) {
+                const reg = /^[1][3-9]\d{9}$/;
+                if (!reg.test(value.value)) {
+                  return Promise.reject(new Error('请输入正确的手机号码'));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+      }
+
+      return rules;
+    },
+    [NotifyModel.notify?.notifyType],
+  );
+
+  const saveBtn = () => {
+    return new Promise(async (resolve) => {
+      const formData = await form.validateFields().catch(() => {
+        resolve(false);
+      });
+      console.log(formData);
+      if (formData) {
+        resolve(formData);
+      } else {
+        resolve(false);
+      }
+    });
+  };
+
+  useImperativeHandle(ref, () => ({
+    save: saveBtn,
+  }));
+
+  return NotifyModel.variable.length ? (
+    <div>
+      <Form form={form} layout={'vertical'}>
+        {(NotifyModel?.variable || []).map((item) => {
+          const type = item.expands?.businessType || item.type;
+          let initialValue = undefined;
+          const rules = getRules(item, type);
+          if (type === 'user') {
+            initialValue = {
+              source: 'relation',
+              value: undefined,
+            };
+          } else if (['date', 'number', 'string'].includes(type)) {
+            initialValue = {
+              source: 'fixed',
+              value: undefined,
+            };
+          }
+          return (
+            <Form.Item
+              key={item.id}
+              name={item.id}
+              label={item.name}
+              initialValue={initialValue}
+              required={!!item.required}
+              rules={rules}
+            >
+              {typeComponents(item)}
+            </Form.Item>
+          );
+        })}
+      </Form>
+    </div>
+  ) : (
+    <Empty />
+  );
+});

+ 13 - 21
src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.less

@@ -1,38 +1,30 @@
 @import '~antd/es/style/themes/default.less';
 
-.trigger-way-warp {
+.notify-type-warp {
   display: flex;
   flex-wrap: wrap;
   gap: 8px;
   width: 100%;
 
-  .trigger-way-item {
+  .notify-type-item {
     display: flex;
-    justify-content: space-between;
-    width: 237px;
-    //width: 100%;
-    padding: 16px;
+    flex-direction: column;
+    align-items: center;
+    width: 172px;
     border: 1px solid #e0e4e8;
     border-radius: 2px;
     cursor: pointer;
-    opacity: 0.6;
     transition: all 0.3s;
 
-    .way-item-title {
-      p {
-        margin-bottom: 8px;
-        font-weight: bold;
-        font-size: 16px;
-      }
-
-      span {
-        color: rgba(#000, 0.35);
-        font-size: 12px;
-      }
+    .notify-type-item-title {
+      margin-bottom: 8px;
+      font-weight: 500;
+      font-size: 14px;
     }
 
-    .way-item-image {
-      margin: 0 !important;
+    .notify-type-item-image {
+      width: 106px;
+      margin: 16px 33px;
     }
 
     &:hover {
@@ -47,7 +39,7 @@
   }
 
   &.disabled {
-    .trigger-way-item {
+    .notify-type-item {
       cursor: not-allowed;
 
       &:hover {

+ 13 - 7
src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.tsx

@@ -11,6 +11,14 @@ interface NotifyTypeProps {
   options: any[];
 }
 
+const iconMap = new Map();
+iconMap.set('dingTalk', require('/public/images/notice/dingtalk.png'));
+iconMap.set('weixin', require('/public/images/notice/wechat.png'));
+iconMap.set('email', require('/public/images/notice/email.png'));
+iconMap.set('voice', require('/public/images/notice/voice.png'));
+iconMap.set('sms', require('/public/images/notice/sms.png'));
+iconMap.set('webhook', require('/public/images/notice/webhook.png'));
+
 export default (props: NotifyTypeProps) => {
   const [type, setType] = useState(props.value || '');
 
@@ -29,23 +37,21 @@ export default (props: NotifyTypeProps) => {
   };
 
   return (
-    <div className={classNames('trigger-way-warp', props.className, { disabled: props.disabled })}>
+    <div className={classNames('notify-type-warp', props.className, { disabled: props.disabled })}>
       {(props?.options || []).map((item) => (
         <div
           key={item.id}
-          className={classNames('trigger-way-item', {
+          className={classNames('notify-type-item', {
             active: type === item.id,
           })}
           onClick={() => {
             onSelect(item.id);
           }}
         >
-          <div className={'way-item-title'}>
-            <p>{item.name}</p>
-          </div>
-          <div className={'way-item-image'}>
-            <img width={48} src={item.image} />
+          <div className={'notify-type-item-image'}>
+            <img width={'100%'} src={iconMap.get(item.id)} />
           </div>
+          <div className={'notify-type-item-title'}>{item.name}</div>
         </div>
       ))}
     </div>

+ 140 - 0
src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/buildIn.tsx

@@ -0,0 +1,140 @@
+import { DatePicker, Input, InputNumber, Select, TreeSelect } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { queryBuiltInParams } from '@/pages/rule-engine/Scene/Save/action/service';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import { BuiltInParamsHandleTreeData } from '@/pages/rule-engine/Scene/Save/components/BuiltInParams';
+import moment from 'moment';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  upperKey?: string;
+};
+
+interface BuiltInProps {
+  value?: ChangeType;
+  data?: any;
+  onChange?: (value: ChangeType) => void;
+  name: number;
+}
+
+export default (props: BuiltInProps) => {
+  const [source, setSource] = useState(props.value?.source);
+  const [value, setValue] = useState(props.value?.value);
+  const [upperKey, setUpperKey] = useState(props.value?.upperKey);
+
+  const [builtInList, setBuiltInList] = useState<any[]>([]);
+
+  const onChange = (_source: string = 'fixed', _value?: any, _upperKey?: string) => {
+    const obj: ChangeType = {
+      source: _source,
+    };
+    if (_value) {
+      obj.value = _value;
+    }
+    if (_upperKey) {
+      obj.upperKey = _upperKey;
+    }
+    if (props.onChange) {
+      props.onChange(obj);
+    }
+  };
+
+  const sourceChangeEvent = async () => {
+    const params = props.name - 1 >= 0 ? { action: props.name - 1 } : undefined;
+    queryBuiltInParams(FormModel, params).then((res: any) => {
+      if (res.status === 200) {
+        const _data = BuiltInParamsHandleTreeData(res.result);
+        setBuiltInList(_data);
+      }
+    });
+  };
+
+  useEffect(() => {
+    if (source === 'upper') {
+      sourceChangeEvent();
+    }
+  }, [source]);
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    setValue(props.value?.value);
+    setUpperKey(props.value?.upperKey);
+  }, [props.value]);
+
+  const itemOnChange = useCallback(
+    (_value: any) => {
+      onChange(source, _value);
+    },
+    [source],
+  );
+
+  const sourceComponents = () => {
+    if (source === 'upper') {
+      return (
+        <TreeSelect
+          value={upperKey}
+          treeData={builtInList}
+          onChange={(key) => {
+            onChange(source, undefined, key);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择参数'}
+        />
+      );
+    } else {
+      const type = props.data?.type;
+      if (type === 'date') {
+        // @ts-ignore
+        return (
+          <DatePicker
+            value={value ? moment(value, 'YYYY-MM-DD HH:mm:ss') : undefined}
+            style={{ width: '100%' }}
+            format={'YYYY-MM-DD HH:mm:ss'}
+            onChange={(_: any, dateString) => {
+              itemOnChange(dateString);
+            }}
+          />
+        );
+      } else if (type === 'number') {
+        return (
+          <InputNumber
+            value={value}
+            placeholder={`请输入${props.data.name}`}
+            style={{ width: '100%' }}
+            onChange={itemOnChange}
+          />
+        );
+      } else {
+        return (
+          <Input
+            value={props.value?.value}
+            placeholder={`请输入${props.data.name}`}
+            onChange={(e) => {
+              onChange(source, e.target.value);
+            }}
+          />
+        );
+      }
+    }
+  };
+
+  return (
+    <ItemGroup compact>
+      <Select
+        value={source}
+        options={[
+          { label: '手动输入', value: 'fixed' },
+          { label: '内置参数', value: 'upper' },
+        ]}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onChange(key, undefined, undefined);
+        }}
+      />
+      {sourceComponents()}
+    </ItemGroup>
+  );
+};

+ 69 - 0
src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/inputFile.tsx

@@ -0,0 +1,69 @@
+import { useEffect, useState } from 'react';
+import { Button, Input, Upload } from 'antd';
+import { UploadChangeParam } from 'antd/lib/upload/interface';
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+
+interface InputUploadProps {
+  value?: string;
+  onChange?: (value?: string) => void;
+  id?: string;
+}
+
+export default (props: InputUploadProps) => {
+  const { onChange } = props;
+
+  const [url, setUrl] = useState(props.value || undefined);
+  const [loading, setLoading] = useState<boolean>(false);
+
+  useEffect(() => {
+    setUrl(url);
+  }, [props.value]);
+
+  const handleChange = (info: UploadChangeParam) => {
+    if (info.file.status === 'uploading') {
+      setLoading(true);
+    }
+    if (info.file.status === 'done') {
+      info.file.url = info.file.response?.result;
+      setLoading(false);
+      if (onChange) {
+        const result = info.file.response?.result;
+        setUrl(result);
+        onChange(result);
+      }
+    }
+  };
+
+  const UploadNode = (
+    <Upload
+      action={`/${SystemConst.API_BASE}/file/static`}
+      headers={{
+        'X-Access-Token': Token.get(),
+      }}
+      showUploadList={false}
+      onChange={handleChange}
+      disabled={loading}
+    >
+      <Button type={'link'} style={{ height: 30 }}>
+        {loading ? <LoadingOutlined /> : <PlusOutlined />}
+        上传附件
+      </Button>
+    </Upload>
+  );
+
+  return (
+    <Input
+      value={url}
+      id={props.id}
+      onChange={(e) => {
+        if (onChange) {
+          onChange(e.target.value);
+        }
+      }}
+      addonAfter={UploadNode}
+      placeholder={'请上传文件'}
+    />
+  );
+};

+ 67 - 0
src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/org.tsx

@@ -0,0 +1,67 @@
+import { TreeSelect } from 'antd';
+import { useEffect, useState } from 'react';
+import {
+  queryDingTalkDepartments,
+  queryWechatDepartments,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+import { NotifyModel } from '../../index';
+
+type ChangeType = {
+  source?: string;
+  value?: string[];
+};
+
+interface OrgProps {
+  value?: ChangeType;
+  onChange?: (value: ChangeType) => void;
+}
+
+export default (props: OrgProps) => {
+  const [keys, setKeys] = useState<any[]>(props.value?.value || []);
+  const [departmentTree, setDepartmentTree] = useState([]);
+
+  const getDepartment = async (id: string) => {
+    if (NotifyModel.notify?.notifyType === 'dingTalk') {
+      const resp = await queryDingTalkDepartments(id);
+      if (resp.status === 200) {
+        setDepartmentTree(resp.result);
+      }
+    } else {
+      const resp = await queryWechatDepartments(id);
+      if (resp.status === 200) {
+        setDepartmentTree(resp.result);
+      }
+    }
+  };
+
+  useEffect(() => {
+    if (NotifyModel.notify?.notifierId) {
+      getDepartment(NotifyModel.notify.notifierId);
+    }
+  }, []);
+
+  useEffect(() => {
+    setKeys(props.value?.value || []);
+  }, [props.value]);
+
+  return (
+    <TreeSelect
+      value={keys}
+      listHeight={200}
+      treeData={departmentTree}
+      fieldNames={{
+        label: 'name',
+        value: 'id',
+      }}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange({
+            source: 'fixed',
+            value: key,
+          });
+        }
+      }}
+      placeholder={'请选择组织'}
+    />
+  );
+};

+ 50 - 0
src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/tag.tsx

@@ -0,0 +1,50 @@
+import { Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { queryTag } from '@/pages/rule-engine/Scene/Save/action/service';
+import { NotifyModel } from '../../index';
+
+interface TagSelectProps {
+  value?: string;
+  onChange?: (value: string) => void;
+}
+
+export default (props: TagSelectProps) => {
+  const [value, setValue] = useState<string | undefined>(props.value);
+  const [options, setOptions] = useState([]);
+
+  useEffect(() => {
+    if (NotifyModel.notify?.notifierId) {
+      queryTag(NotifyModel.notify.notifierId).then((res) => {
+        if (res.status === 200) {
+          setOptions(res.result);
+        } else {
+          setOptions([]);
+        }
+      });
+    } else {
+      setOptions([]);
+    }
+  }, [NotifyModel.notify.notifierId]);
+
+  useEffect(() => {
+    setValue(props.value);
+  }, [props.value]);
+
+  return (
+    <Select
+      value={value}
+      placeholder={'请选择标签'}
+      options={options}
+      fieldNames={{
+        label: 'name',
+        value: 'id',
+      }}
+      style={{ width: '100%' }}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange(key);
+        }
+      }}
+    />
+  );
+};

+ 315 - 0
src/pages/rule-engine/Scene/Save/action/notify/components/variableItem/user.tsx

@@ -0,0 +1,315 @@
+// 收信人
+import { useEffect, useState } from 'react';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import { Input, Select, TreeSelect } from 'antd';
+import {
+  queryDingTalkUsers,
+  queryPlatformUsers,
+  queryRelationUsers,
+  queryWechatUsers,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+import { useLocation } from '@/hooks';
+import { observer } from '@formily/react';
+import { NotifyModel } from '../../index';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  relation?: any;
+};
+
+interface UserProps {
+  value?: ChangeType;
+  onChange?: (value: ChangeType) => void;
+  type?: string;
+}
+
+export default observer((props: UserProps) => {
+  const [source, setSource] = useState<string | undefined>(props.value?.source || '');
+  const [value, setValue] = useState<string | undefined>(undefined);
+  const [relationList, setRelationList] = useState<any[]>([]);
+  const [treeData, setTreeData] = useState<any[]>([
+    { name: '平台用户', id: 'p1', selectable: false, children: [] },
+  ]);
+
+  const location = useLocation();
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    if (props.value?.source === 'relation') {
+      const relation = props.value?.relation;
+      if (relation) {
+        if (relation.objectId) {
+          // 平台用户
+          setValue(relation.objectId);
+        } else {
+          // 关系用户
+          setValue(relation.related?.relation);
+        }
+      }
+    } else {
+      setValue(props.value?.value);
+    }
+  }, [props.value]);
+
+  const getPlatformUser = async () => {
+    const newTree = [...treeData];
+    const platformResp = await queryPlatformUsers();
+
+    if (platformResp.status === 200) {
+      newTree[0].children = platformResp.result;
+    }
+
+    setTreeData(newTree);
+  };
+
+  const getRelationUsers = async (notifyType: string, configId: string) => {
+    if (notifyType === 'dingTalk') {
+      const resp = await queryDingTalkUsers(configId);
+      if (resp.status === 200) {
+        setRelationList(resp.result);
+      }
+    } else {
+      const resp = await queryWechatUsers(configId);
+      if (resp.status === 200) {
+        setRelationList(resp.result);
+      }
+    }
+  };
+
+  useEffect(() => {
+    if (
+      source === 'fixed' &&
+      ['dingTalk', 'weixin'].includes(NotifyModel.notify?.notifyType || '')
+    ) {
+      // 钉钉,微信用户
+      getRelationUsers(NotifyModel.notify?.notifyType || '', NotifyModel.notify?.notifierId || '');
+    } else {
+      getPlatformUser();
+    }
+  }, [source, NotifyModel.notify.notifyType]);
+
+  const options = [
+    { label: '平台用户', value: 'relation' },
+    {
+      label: NotifyModel.notify.notifyType === 'dingTalk' ? '钉钉用户' : '微信用户',
+      value: 'fixed',
+    },
+  ];
+
+  const otherOptions = [
+    { label: '平台用户', value: 'relation' },
+    {
+      label:
+        NotifyModel.notify.notifyType && NotifyModel.notify.notifyType === 'email'
+          ? '固定邮箱'
+          : '固定号码',
+      value: 'fixed',
+    },
+  ];
+
+  const onchange = (_source: string = 'fixed', _value?: string, isRelation?: boolean) => {
+    const obj: any = {
+      source: _source,
+    };
+    if (_source === 'relation') {
+      if (isRelation) {
+        obj.relation = {
+          objectType: 'device',
+          objectSource: {
+            source: 'upper',
+            upperKey: 'deviceId',
+          },
+          related: {
+            objectType: 'user',
+            relation: _value,
+          },
+        };
+      } else {
+        obj.relation = {
+          objectType: 'user',
+          objectId: _value,
+        };
+      }
+    } else {
+      obj.value = _value;
+    }
+
+    if (props.onChange) {
+      props.onChange(obj);
+    }
+  };
+
+  useEffect(() => {
+    if (props.type && source === 'relation') {
+      const newTree = [...treeData];
+      if (props.type === 'device') {
+        queryRelationUsers().then((relationResp) => {
+          if (relationResp.status === 200) {
+            newTree.push({
+              name: '关系用户',
+              id: 'p2',
+              selectable: false,
+              children: relationResp.result,
+            });
+            setTreeData(newTree);
+          }
+        });
+      } else {
+        if (newTree.length > 1) {
+          newTree.splice(1, 1);
+          setTreeData(newTree);
+        }
+      }
+
+      if (!location.query?.id) {
+        onchange(props.value?.source, '');
+      }
+    }
+  }, [props.type, source]);
+
+  const filterOption = (input: string, option: any) => {
+    return option.name ? option.name.toLowerCase().includes(input.toLowerCase()) : false;
+  };
+
+  const createTreeNode = (data: any): React.ReactNode => {
+    return data.map((item: any) => {
+      if (item.children) {
+        return (
+          <TreeSelect.TreeNode value={item.id} title={item.name} selectable={false}>
+            {createTreeNode(item.children)}
+          </TreeSelect.TreeNode>
+        );
+      } else {
+        return (
+          <TreeSelect.TreeNode
+            name={item.name}
+            value={item.id}
+            title={
+              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                <span>{item.name}</span>
+                <span style={{ color: '#cfcfcf' }}>{item.username}</span>
+              </div>
+            }
+          />
+        );
+      }
+    });
+  };
+
+  const userSelect =
+    source === 'relation' ? (
+      <TreeSelect
+        showSearch
+        allowClear
+        value={value}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        placeholder={'请选择收信人'}
+        onSelect={(key: any, node: any) => {
+          setValue(key);
+          onchange(source, key, node.isRelation);
+        }}
+        filterTreeNode={filterOption}
+      >
+        {createTreeNode(treeData)}
+      </TreeSelect>
+    ) : (
+      <Select
+        showSearch
+        allowClear
+        value={value}
+        options={relationList}
+        listHeight={200}
+        onChange={(key) => {
+          setValue(key);
+          onchange(source, key);
+        }}
+        fieldNames={{ label: 'name', value: 'id' }}
+        placeholder={'请选择收信人'}
+        filterOption={(input: string, option: any) => {
+          return option.name ? option.name.toLowerCase().includes(input.toLowerCase()) : false;
+        }}
+      />
+    );
+
+  const emailSelect =
+    source === 'relation' ? (
+      <TreeSelect
+        showSearch
+        allowClear
+        value={value}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        placeholder={'请选择收信人'}
+        onSelect={(key: any, node: any) => {
+          setValue(key);
+          onchange(source, key, node.isRelation);
+        }}
+        filterTreeNode={filterOption}
+      >
+        {createTreeNode(treeData)}
+      </TreeSelect>
+    ) : (
+      <Input
+        value={value}
+        placeholder={'请输入固定邮箱'}
+        onChange={(e) => {
+          onchange(source, e.target.value);
+        }}
+      />
+    );
+
+  const voiceSelect =
+    source === 'relation' ? (
+      <TreeSelect
+        showSearch
+        allowClear
+        value={value}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        placeholder={'请选择收信人'}
+        onSelect={(key: any, node: any) => {
+          setValue(key);
+          onchange(source, key, node.isRelation);
+        }}
+        filterTreeNode={filterOption}
+      >
+        {createTreeNode(treeData)}
+      </TreeSelect>
+    ) : (
+      <Input
+        value={value}
+        placeholder={'请输入固定号码'}
+        onChange={(e) => {
+          onchange(source, e.target.value);
+        }}
+      />
+    );
+
+  return (
+    <ItemGroup compact>
+      <Select
+        value={source}
+        options={
+          NotifyModel.notify.notifyType &&
+          ['dingTalk', 'weixin'].includes(NotifyModel.notify.notifyType)
+            ? options
+            : otherOptions
+        }
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onchange(key, undefined);
+        }}
+      />
+      {NotifyModel.notify.notifyType &&
+      ['dingTalk', 'weixin'].includes(NotifyModel.notify.notifyType)
+        ? userSelect
+        : null}
+      {NotifyModel.notify.notifyType && ['email'].includes(NotifyModel.notify.notifyType)
+        ? emailSelect
+        : null}
+      {NotifyModel.notify.notifyType && ['sms', 'voice'].includes(NotifyModel.notify.notifyType)
+        ? voiceSelect
+        : null}
+    </ItemGroup>
+  );
+});

+ 129 - 34
src/pages/rule-engine/Scene/Save/action/notify/index.tsx

@@ -1,76 +1,171 @@
 import { Modal, Button, Steps } from 'antd';
-import React from 'react';
+import { useEffect, useRef } from 'react';
 import { observer } from '@formily/react';
 import { model } from '@formily/reactive';
 import NotifyWay from './NotifyWay';
 import NotifyConfig from './NotifyConfig';
 import NotifyTemplate from './NotifyTemplate';
+import VariableDefinitions from './VariableDefinitions';
 import './index.less';
-const initSteps = [
-  {
-    key: 'way',
-    title: '通知方式',
-    content: <NotifyWay />,
-  },
-  {
-    key: 'config',
-    title: '通知配置',
-    content: <NotifyConfig />,
-  },
-  {
-    key: 'template',
-    title: '通知模板',
-    content: <NotifyTemplate />,
-  },
-];
+import { onlyMessage } from '@/utils/util';
+import { queryMessageTemplateDetail } from '@/pages/rule-engine/Scene/Save/action/service';
+import { NotifyProps } from '@/pages/rule-engine/Scene/typings';
+
+interface Props {
+  value: Partial<NotifyProps>;
+  save: (notify: Partial<NotifyProps>) => void;
+  cancel: () => void;
+  name: number;
+}
 
-const NotifyModel = model<{
-  steps: { key: string; title: string; content: React.ReactNode }[];
+export const NotifyModel = model<{
+  steps: { key: string; title: string }[];
   current: number;
+  notify: Partial<NotifyProps>;
+  variable: any[];
 }>({
-  steps: initSteps,
+  steps: [
+    {
+      key: 'way',
+      title: '通知方式',
+    },
+    {
+      key: 'config',
+      title: '通知配置',
+    },
+    {
+      key: 'template',
+      title: '通知模板',
+    },
+    {
+      key: 'variable',
+      title: '模板变量',
+    },
+  ],
   current: 0,
+  notify: {
+    notifyType: '',
+    notifierId: '',
+    templateId: '',
+  },
+  variable: [],
 });
 
-export default observer(() => {
-  const next = () => {
-    NotifyModel.current += 1;
+export default observer((props: Props) => {
+  const WayRef = useRef<{ save: any }>();
+  const VariableRef = useRef<{ save: any }>();
+
+  useEffect(() => {
+    NotifyModel.notify = props.value;
+  }, [props.value]);
+
+  const renderComponent = (type: string) => {
+    switch (type) {
+      case 'way':
+        return <NotifyWay ref={WayRef} value={NotifyModel.notify?.notifyType} />;
+      case 'config':
+        return <NotifyConfig />;
+      case 'template':
+        return <NotifyTemplate />;
+      case 'variable':
+        return <VariableDefinitions name={props.name} ref={VariableRef} />;
+      default:
+        return null;
+    }
   };
 
   const prev = () => {
     NotifyModel.current -= 1;
   };
 
+  const next = async () => {
+    if (NotifyModel.current === 0) {
+      const val = await WayRef.current?.save();
+      if (val) {
+        NotifyModel.notify.notifyType = val;
+        NotifyModel.current += 1;
+      }
+    } else if (NotifyModel.current === 1) {
+      if (NotifyModel.notify?.notifierId) {
+        NotifyModel.current += 1;
+      } else {
+        onlyMessage('请选择通知配置', 'error');
+      }
+    } else if (NotifyModel.current === 2) {
+      if (NotifyModel.notify?.templateId) {
+        const resp = await queryMessageTemplateDetail(NotifyModel.notify.templateId);
+        if (resp.status === 200) {
+          NotifyModel.variable = resp.result?.variableDefinitions || [];
+          NotifyModel.current += 1;
+        }
+      } else {
+        onlyMessage('请选择通知模板', 'error');
+      }
+    } else if (NotifyModel.current === 3) {
+      const resp = await VariableRef.current?.save();
+      if (resp) {
+        NotifyModel.notify.variables = resp;
+        props.save(NotifyModel.notify);
+        NotifyModel.current = 0;
+      }
+    }
+  };
+
   return (
     <Modal
       title={'执行动作'}
       open
-      width={1000}
+      width={800}
+      onCancel={() => {
+        props.cancel();
+        NotifyModel.current = 0;
+      }}
       footer={
         <div className="steps-action">
-          {NotifyModel.current === 0 && <Button onClick={() => {}}>取消</Button>}
+          {NotifyModel.current === 0 && (
+            <Button
+              onClick={() => {
+                props.cancel();
+                NotifyModel.current = 0;
+              }}
+            >
+              取消
+            </Button>
+          )}
+          {NotifyModel.current > 0 && (
+            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+              上一步
+            </Button>
+          )}
           {NotifyModel.current < NotifyModel.steps.length - 1 && (
-            <Button type="primary" onClick={() => next()}>
+            <Button
+              type="primary"
+              onClick={() => {
+                next();
+              }}
+            >
               下一步
             </Button>
           )}
           {NotifyModel.current === NotifyModel.steps.length - 1 && (
-            <Button type="primary" onClick={() => {}}>
+            <Button
+              type="primary"
+              onClick={() => {
+                next();
+              }}
+            >
               确定
             </Button>
           )}
-          {NotifyModel.current > 0 && (
-            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
-              上一步
-            </Button>
-          )}
         </div>
       }
     >
       <div className="steps-steps">
         <Steps current={NotifyModel.current} items={NotifyModel.steps} />
       </div>
-      <div className="steps-content">{NotifyModel.steps[NotifyModel.current]?.content}</div>
+      <div className="steps-content">
+        {renderComponent(NotifyModel.steps[NotifyModel.current]?.key)}
+      </div>
     </Modal>
   );
 });

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

@@ -11,6 +11,12 @@ export const queryMessageConfig = (data: any) =>
     data,
   });
 
+export const queryMessageConfigPaging = (data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/config/_query`, {
+    method: 'POST',
+    data,
+  });
+
 // 通知模板
 export const queryMessageTemplate = (data: any) =>
   request(`${SystemConst.API_BASE}/notifier/template/_query/no-paging?paging=false`, {
@@ -18,6 +24,12 @@ export const queryMessageTemplate = (data: any) =>
     data,
   });
 
+export const queryMessageTemplatePaging = (id: string, data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/template/${id}/_query`, {
+    method: 'POST',
+    data,
+  });
+
 export const queryMessageTemplateDetail = (id: string) =>
   request(`${SystemConst.API_BASE}/notifier/template/${id}/detail`);
 

+ 3 - 1
src/pages/rule-engine/Scene/Save/components/Buttons/AddButton.tsx

@@ -9,7 +9,9 @@ interface ButtonProps {
 const AddButton = (props: ButtonProps) => {
   return (
     <div className="rule-button-warp" style={props.style}>
-      <div className="rule-button add-button">{props.children}</div>
+      <div className="rule-button add-button" onClick={props.onClick}>
+        {props.children}
+      </div>
     </div>
   );
 };

+ 98 - 0
src/pages/rule-engine/Scene/Save/components/Buttons/Dropdown.tsx

@@ -0,0 +1,98 @@
+import { useEffect, useMemo, useState } from 'react';
+import { Dropdown, Tree } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+type DropdownButtonOptions = {
+  title: string;
+  key: string;
+  children?: DropdownButtonOptions[];
+  [key: string]: any;
+};
+
+interface DropdownButtonProps {
+  className?: string;
+  placeholder?: string;
+  value?: string;
+  onChange?: (value?: string) => void;
+  options: DropdownButtonOptions[];
+  isTree?: boolean;
+  type: 'param' | 'termType' | 'value' | 'type';
+}
+
+const TypeStyle = {
+  param: styles.parameter,
+  termType: styles.termType,
+  value: styles.value,
+  type: styles.type,
+};
+
+export default (props: DropdownButtonProps) => {
+  const [myValue, setMyValue] = useState(props.value);
+  const [label, setLabel] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  const typeClassName = TypeStyle[props.type];
+
+  const menuOnSelect = ({ key, item }: { key: string; item: any }) => {
+    props.onChange?.(key);
+    setMyValue(key);
+    setLabel(item.props.title);
+  };
+
+  const treeSelect = (selectedKeys: (string | number)[], e: any) => {
+    props.onChange?.(selectedKeys[0] as string);
+    setMyValue(selectedKeys[0] as string);
+    setLabel(e.node.title);
+  };
+
+  const menuOptions = {
+    selectedKeys: myValue ? [myValue] : [],
+    items: props.options.map((item) => ({ ...item, label: item.title })),
+    onClick: menuOnSelect,
+  };
+
+  const DropdownRender = useMemo(() => {
+    return (
+      <Tree
+        selectedKeys={myValue ? [myValue] : []}
+        onSelect={treeSelect}
+        treeData={props.options}
+      />
+    );
+  }, [props.options]);
+
+  const _options = !props.isTree ? { menu: menuOptions } : { dropdownRender: () => DropdownRender };
+
+  const findLable = (value: string, data: DropdownButtonOptions[]): boolean => {
+    let isLabel = false;
+    return data.some((item) => {
+      if (item.key === value) {
+        setLabel(item.title);
+        isLabel = true;
+      } else if (item.children) {
+        isLabel = findLable(value, item.children);
+      }
+      return isLabel;
+    });
+  };
+
+  useEffect(() => {
+    setMyValue(props.value);
+  }, [props.value]);
+
+  useEffect(() => {
+    if (myValue && !loading) {
+      findLable(myValue, props.options);
+      setLoading(true);
+    }
+  }, [props.options]);
+
+  return (
+    <Dropdown {..._options} trigger={['click']}>
+      <div className={classNames(styles['dropdown-button'], props.className, typeClassName)}>
+        {label || props.placeholder}
+      </div>
+    </Dropdown>
+  );
+};

+ 35 - 1
src/pages/rule-engine/Scene/Save/components/Buttons/index.less

@@ -11,10 +11,44 @@
     line-height: 22px;
     background-color: #fff;
     border: 1px solid #e0e0e0;
-    border-radius: 15px;
+    border-radius: 22px;
   }
 
   .add-button {
     color: #bdbdbd;
+    &:hover,
+    &:active {
+      border-color: #d0d0d0;
+    }
   }
 }
+
+.dropdown-button {
+  display: inline-block;
+  padding: 6px 8px;
+  border: 1px solid #d9d9d9;
+  border-radius: 8px;
+  cursor: pointer;
+}
+
+.parameter {
+  color: #00a4fe;
+  background-color: rgba(154, 219, 255, 0.3);
+  border-color: rgba(0, 164, 254, 0.3);
+}
+
+.termType {
+  color: #2f54eb;
+  background-color: rgba(163, 202, 255, 0.3);
+  border-color: rgba(47, 84, 235, 0.3);
+}
+
+.value {
+  color: #692ca7;
+  background-color: rgba(188, 125, 238, 0.1);
+  border-color: rgba(188, 125, 238, 0.5);
+}
+
+.type {
+  padding: 5px 10px;
+}

+ 1 - 0
src/pages/rule-engine/Scene/Save/components/Buttons/index.tsx

@@ -1,2 +1,3 @@
 import './index.less';
 export { default as AddButton } from './AddButton';
+export { default as DropdownButton } from './Dropdown';

+ 25 - 0
src/pages/rule-engine/Scene/Save/components/ParamsSelect/components/MTimePicker/index.less

@@ -0,0 +1,25 @@
+.manual-box {
+  position: relative;
+  width: 100%;
+  .manual-time-picker {
+    position: absolute;
+    top: -2px;
+    left: 0;
+    border: none;
+    visibility: hidden;
+    .ant-picker-input {
+      display: none !important;
+    }
+  }
+}
+
+.my-manual-time-picker {
+  width: 100%;
+  .ant-picker-panel-container {
+    position: relative;
+    box-shadow: none !important;
+    .ant-picker-panel {
+      width: 100%;
+    }
+  }
+}

+ 22 - 0
src/pages/rule-engine/Scene/Save/components/ParamsSelect/components/MTimePicker/index.tsx

@@ -0,0 +1,22 @@
+import { TimePicker } from 'antd';
+import './index.less';
+import { TimePickerProps } from 'antd/lib/time-picker';
+
+export default (props: TimePickerProps) => {
+  return (
+    <div id={'manual-box'} className={'manual-box'}>
+      <TimePicker
+        {...props}
+        value={props.value}
+        onChange={props.onChange}
+        className={'manual-time-picker'}
+        popupClassName={'my-manual-time-picker'}
+        open
+        // @ts-ignore
+        getPopupContainer={(trigger) => {
+          return trigger && trigger?.parentNode ? trigger.parentNode : document.body;
+        }}
+      />
+    </div>
+  );
+};

+ 57 - 0
src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.less

@@ -0,0 +1,57 @@
+.select-wrapper {
+  position: relative;
+  width: 100%;
+  margin-bottom: 20px;
+  .select-container {
+    position: absolute;
+    top: 32px;
+    right: auto;
+    left: 0;
+    z-index: 99;
+    width: 100%;
+    min-width: 200px;
+    min-height: 340px;
+    padding: 8px 4px;
+    overflow: hidden;
+    overflow-y: auto;
+    color: rgba(0, 0, 0, 0.88);
+    font-size: 14px;
+    background-color: white;
+    border-radius: 2px;
+    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.27);
+    .select-box {
+      padding: 4px;
+      .select-box-header-top {
+        width: 100%;
+        overflow-x: auto;
+        border-bottom: 1px solid #f0f0f0;
+        .select-box-header {
+          display: flex;
+          justify-content: flex-start;
+          justify-items: center;
+          width: max-content;
+          .select-header-title {
+            padding: 7px 14px;
+            overflow: hidden;
+            color: #999;
+            font-weight: 500;
+            font-size: 16px;
+            white-space: nowrap;
+            cursor: pointer;
+            &.active {
+              color: #323130;
+              border-bottom: 2px solid #2f54eb;
+            }
+          }
+        }
+
+        &::-webkit-scrollbar {
+          width: 0;
+        }
+      }
+      .select-box-content {
+        width: 100%;
+      }
+    }
+  }
+}

+ 112 - 0
src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.tsx

@@ -0,0 +1,112 @@
+import { DownOutlined } from '@ant-design/icons';
+import { Input } from 'antd';
+import { useState, useEffect, useRef, ReactNode } from 'react';
+import './index.less';
+import classNames from 'classnames';
+import { InputProps } from 'antd/lib/input/Input';
+
+export interface ItemProps {
+  key: string;
+  label: string;
+  content: ReactNode;
+}
+
+interface Props {
+  placeholder?: string;
+  value: any;
+  onChange: (dt: any) => void;
+  inputProps?: InputProps;
+  itemList: ItemProps[];
+  style?: object;
+  tabKey: string;
+}
+
+export default (props: Props) => {
+  const [visible, setVisible] = useState<boolean>(false);
+  const [tabKey, setTabKey] = useState<string>(props?.tabKey || props.itemList[0]?.key);
+  const wrapperRef = useRef<any>(null);
+  const nodeRef = useRef<any>(null);
+  const [value, setValue] = useState<any>(props.value);
+  // const [showValue, setShowValue] = useState<string | undefined>('');
+
+  useEffect(() => {
+    setTabKey(props.tabKey);
+  }, [props.tabKey]);
+
+  useEffect(() => {
+    setValue(props.value);
+  }, [props.value]);
+
+  const handleClick = (e: any) => {
+    if (visible && e.target) {
+      if (!(wrapperRef.current && wrapperRef.current.contains(e.target))) {
+        setVisible(false);
+      }
+    }
+  };
+
+  useEffect(() => {
+    window.addEventListener('click', handleClick);
+    return () => {
+      window.removeEventListener('click', handleClick);
+    };
+  });
+
+  // const contentRender = (item: ItemProps | undefined) => {
+  //   switch (item?.type) {
+  //     case 'time-picker':
+  //       return <MTimePicker {...item.children} value={value} onChange={(time: any, timeString: string) => {
+  //         setShowValue(timeString)
+  //         console.log(timeString)
+  //         setValue(time)
+  //       }} />;
+  //     case 'tree':
+  //       return <Tree {...item.children} height={300} defaultExpandAll />
+  //     default:
+  //       return null;
+  //   }
+  // }
+
+  return (
+    <div className={'select-wrapper'} ref={wrapperRef} style={props.style}>
+      <Input
+        suffix={<DownOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
+        {...props.inputProps}
+        value={value}
+        onFocus={() => {
+          setVisible(true);
+        }}
+      />
+      {visible && (
+        <div className={'select-container'} ref={nodeRef}>
+          <div className={'select-box'}>
+            <div className={'select-box-header-top'}>
+              <div className={'select-box-header'}>
+                {(props.itemList || []).map((item) => (
+                  <div
+                    key={item.key}
+                    className={classNames(
+                      'select-header-title',
+                      item.key === tabKey ? 'active' : '',
+                    )}
+                    onClick={() => {
+                      setTabKey(item.key);
+                    }}
+                  >
+                    {item.label}
+                  </div>
+                ))}
+              </div>
+            </div>
+            <div className={'select-box-content'}>
+              {
+                (props?.itemList || []).find((item) => item.key === tabKey)?.content || ''
+                // contentRender((props?.itemList || []).find(item => item.key === tabKey))
+              }
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};

+ 1 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType.tsx

@@ -74,6 +74,7 @@ export default (props: ActionsTypeProps) => {
           className={classNames('trigger-way-item', {
             active: type === item.value,
           })}
+          style={{ width: 237 }}
           onClick={() => {
             onSelect(item.value);
           }}

+ 1 - 2
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less

@@ -9,8 +9,7 @@
   .trigger-way-item {
     display: flex;
     justify-content: space-between;
-    width: 237px;
-    //width: 100%;
+    // width: 100%;
     padding: 16px;
     border: 1px solid #e0e4e8;
     border-radius: 2px;

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

@@ -42,6 +42,7 @@ export default (props: TriggerWayProps) => {
         className={classNames('trigger-way-item', {
           active: type === TriggerWayType.device,
         })}
+        style={{ width: 204 }}
         onClick={() => {
           onSelect(TriggerWayType.device);
         }}
@@ -58,6 +59,7 @@ export default (props: TriggerWayProps) => {
         className={classNames('trigger-way-item', {
           active: type === TriggerWayType.manual,
         })}
+        style={{ width: 204 }}
         onClick={() => {
           onSelect(TriggerWayType.manual);
         }}
@@ -74,6 +76,7 @@ export default (props: TriggerWayProps) => {
         className={classNames('trigger-way-item', {
           active: type === TriggerWayType.timing,
         })}
+        style={{ width: 204 }}
         onClick={() => {
           onSelect(TriggerWayType.timing);
         }}

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

@@ -1,3 +1,88 @@
+import Terms from '@/pages/rule-engine/Scene/Save/terms';
+import ParamsSelect, { ItemProps } from '@/pages/rule-engine/Scene/Save/components/ParamsSelect';
+import { useState } from 'react';
+import { DataNode } from 'antd/es/tree';
+import { Tree } from 'antd';
+import MTimePicker from '../components/ParamsSelect/components/MTimePicker';
+
 export default () => {
-  return <div>设备触发</div>;
+  const [value, setValue] = useState<any>(null);
+  const treeData: DataNode[] = [
+    {
+      title: 'parent 1',
+      key: '0-0',
+      children: [
+        {
+          title: 'parent 1-0',
+          key: '0-0-0',
+          children: [
+            {
+              title: 'leaf',
+              key: '0-0-0-0',
+            },
+            {
+              title: 'leaf',
+              key: '0-0-0-1',
+            },
+          ],
+        },
+        {
+          title: 'parent 1-1',
+          key: '0-0-1',
+          children: [{ title: 'sss', key: '0-0-1-0' }],
+        },
+      ],
+    },
+  ];
+  const [showValue, setShowValue] = useState<any>('');
+  const itemList: ItemProps[] = [
+    {
+      label: `手动输入`,
+      key: 'manual',
+      content: (
+        <MTimePicker
+          value={value}
+          onChange={(time: any, timeString: string) => {
+            setShowValue(timeString);
+            setValue(time);
+          }}
+        />
+      ),
+    },
+    {
+      label: `内置参数`,
+      key: 'built-in',
+      content: (
+        <Tree
+          treeData={treeData}
+          height={300}
+          defaultExpandAll
+          onSelect={(selectedKeys) => {
+            setValue(selectedKeys[0]);
+            setShowValue(selectedKeys[0]);
+          }}
+        />
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <div>
+        <ParamsSelect
+          style={{ width: 250 }}
+          inputProps={{
+            placeholder: '请选择',
+          }}
+          tabKey={'manual'}
+          itemList={itemList}
+          value={showValue}
+          onChange={(val: any) => {
+            setValue(val.timeString);
+          }}
+        />
+      </div>
+      <Terms />
+    </div>
+  );
 };

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

@@ -5,10 +5,67 @@ import Device from '../Save/device/index';
 import Manual from '../Save/manual/index';
 import Timer from '../Save/timer/index';
 import { TitleComponent } from '@/components';
+import { observable } from '@formily/reactive';
+import type { FormModelType } from '@/pages/rule-engine/Scene/typings';
+import { useEffect } from 'react';
+import { service } from '@/pages/rule-engine/Scene';
+
+export const FormModel = observable<FormModelType>({
+  actions: [],
+  branches: [
+    {
+      when: [
+        {
+          terms: [
+            {
+              column: undefined,
+              value: undefined,
+              key: 'params_1',
+            },
+          ],
+          type: 'and',
+          key: 'terms_1',
+        },
+      ],
+      key: 'branckes_1',
+      shakeLimit: {
+        enabled: false,
+        groupType: 'device',
+        time: 1,
+        threshold: 1,
+        alarmFirst: false,
+      },
+      then: [],
+    },
+    {
+      when: [],
+      key: 'branckes_2',
+      shakeLimit: {
+        enabled: false,
+        groupType: 'device',
+        time: 1,
+        threshold: 1,
+        alarmFirst: false,
+      },
+      then: [],
+    },
+  ],
+});
 
 export default () => {
   const location = useLocation();
   const triggerType = location?.query?.triggerType || '';
+  const id = location?.query?.id || '';
+
+  useEffect(() => {
+    if (id) {
+      service.detail(id).then((resp) => {
+        if (resp.status === 200) {
+          Object.assign(FormModel, resp.result);
+        }
+      });
+    }
+  }, [id]);
 
   const triggerRender = (type: string) => {
     switch (type) {

+ 6 - 2
src/pages/rule-engine/Scene/Save/save.tsx

@@ -25,7 +25,7 @@ export default (props: Props) => {
     <Modal
       title={props.data?.id ? '编辑' : '新增'}
       maskClosable={false}
-      visible
+      open
       onCancel={() => {
         props.close();
       }}
@@ -33,7 +33,11 @@ export default (props: Props) => {
         const values = await form.validateFields();
         props.close();
         const url = getMenuPathByCode('rule-engine/Scene/Save');
-        history.push(`${url}?triggerType=${values.trigger?.type}`);
+        if (props.data?.id) {
+          history.push(`${url}?triggerType=${values.trigger?.type}&id=${props.data?.id}`);
+        } else {
+          history.push(`${url}?triggerType=${values.trigger?.type}`);
+        }
       }}
       width={700}
     >

+ 104 - 0
src/pages/rule-engine/Scene/Save/terms/branchItem.tsx

@@ -0,0 +1,104 @@
+import { observer, Observer } from '@formily/react';
+import { useState } from 'react';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import { PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { ActionBranchesProps } from '@/pages/rule-engine/Scene/typings';
+import Term from './term';
+import classNames from 'classnames';
+
+interface BranchesItemProps {
+  name: number;
+  data: ActionBranchesProps;
+  isFrist: boolean;
+}
+
+export default observer((props: BranchesItemProps) => {
+  const [deleteVisible, setDeleteVisible] = useState(false);
+
+  const deleteTerms = (index: number) => {
+    FormModel.branches?.splice(index, 1);
+  };
+
+  const addWhen = (index: number) => {
+    const lastBranch = FormModel.branches![index].when;
+    lastBranch.push({
+      terms: [
+        {
+          column: undefined,
+          value: undefined,
+          key: 'params_1',
+        },
+      ],
+      type: 'and',
+      key: 'terms_1',
+    });
+    // 增加下一个否则, '当' 排除
+    if (index > 0) {
+      FormModel.branches?.push({
+        when: [],
+        key: 'branch_' + FormModel.branches.length + 1,
+        shakeLimit: {
+          enabled: false,
+          groupType: 'device',
+          time: 1,
+          threshold: 1,
+          alarmFirst: false,
+        },
+        then: [],
+      });
+    }
+  };
+
+  return (
+    <div className="actions-terms-warp">
+      <div className="actions-terms-title">{props.isFrist ? '当' : '否则'}</div>
+      <div
+        className={classNames('actions-terms-options', { border: !props.isFrist })}
+        onMouseOver={() => setDeleteVisible(true)}
+        onMouseOut={() => setDeleteVisible(false)}
+      >
+        {!props.isFrist && props.data.when?.length ? (
+          <div
+            className={classNames('terms-params-delete denger', { show: deleteVisible })}
+            onClick={() => deleteTerms(props.name)}
+          >
+            <DeleteOutlined />
+          </div>
+        ) : null}
+        <div className="actions-terms-list">
+          <Observer>
+            {() =>
+              props.data.when?.length ? (
+                props.data.when.map((data, dIndex) => {
+                  return (
+                    <Term
+                      pName={[props.name, 'when']}
+                      name={dIndex}
+                      data={data}
+                      key={data.key}
+                      isLast={dIndex === props.data.when!.length - 1}
+                    />
+                  );
+                })
+              ) : (
+                <span
+                  style={{
+                    fontSize: 14,
+                    color: '#2F54EB',
+                    cursor: 'pointer',
+                    padding: props.isFrist ? '16px 0' : 0,
+                  }}
+                  onClick={() => addWhen(props.name)}
+                >
+                  {' '}
+                  <PlusCircleOutlined style={{ padding: 4 }} /> 添加过滤条件
+                </span>
+              )
+            }
+          </Observer>
+        </div>
+        <div className="actions-branchs"></div>
+      </div>
+    </div>
+  );
+});

+ 147 - 0
src/pages/rule-engine/Scene/Save/terms/index.less

@@ -0,0 +1,147 @@
+.deleteBtn() {
+  position: absolute;
+  top: -10px;
+  right: -10px;
+  display: none;
+  width: 20px;
+  height: 20px;
+  color: #999;
+  line-height: 20px;
+  text-align: center;
+  background-color: #f1f1f1;
+  border-radius: 50%;
+  cursor: pointer;
+
+  &.show {
+    display: block;
+  }
+
+  &:hover {
+    background-color: #f3f3f3;
+  }
+}
+
+.actions-terms {
+  .actions-terms-warp {
+    display: flex;
+
+    &:not(:last-child) {
+      margin-bottom: 24px;
+    }
+
+    .actions-terms-title {
+      width: 40px;
+      padding-top: 16px;
+      color: #6968be;
+      font-weight: 800;
+      font-size: 16px;
+    }
+
+    .actions-terms-options {
+      position: relative;
+      display: flex;
+      flex-grow: 1;
+      width: 0;
+
+      &.border {
+        padding: 18px;
+        border: 1px dashed #999;
+        border-radius: 2px;
+      }
+
+      .actions-terms-list {
+        display: flex;
+        flex-wrap: wrap;
+        row-gap: 16px;
+      }
+    }
+  }
+
+  .terms-params-delete {
+    .deleteBtn();
+
+    &.denger {
+      color: #e50012;
+      background-color: rgba(229, 0, 18, 0.1);
+    }
+  }
+}
+
+.terms-params {
+  display: inline-block;
+
+  // &:not(:first-child) {
+  //   margin-bottom: 16px;
+  // }
+
+  .terms-params-warp {
+    display: flex;
+    align-items: center;
+  }
+
+  .terms-params-content {
+    position: relative;
+    display: flex;
+    flex-wrap: wrap;
+    padding: 8px;
+    background-color: #fafafa;
+    row-gap: 16px;
+  }
+
+  .terms-add {
+    display: inline-block;
+    width: 66px;
+    margin-left: 16px;
+    padding: 2px 8px;
+    color: rgba(0, 0, 0, 0.3);
+    background: #fff;
+    border: 1px dashed rgba(0, 0, 0, 0.3);
+    border-radius: 30px;
+    cursor: pointer;
+  }
+
+  .term-type-warp {
+    display: inline-block;
+    width: 60px;
+    margin: 0 16px;
+    .term-type {
+      padding-top: 4px;
+      padding-bottom: 4px;
+      border-radius: 2px;
+    }
+  }
+}
+
+.terms-params-item {
+  .params-button {
+    display: inline-block;
+    padding: 6px 8px;
+    border: 1px solid #d9d9d9;
+    border-radius: 8px;
+    cursor: pointer;
+  }
+
+  .params-item_button {
+    position: relative;
+    display: inline-block;
+    padding: 4px;
+    border: 1px solid rgba(0, 0, 0, 0.1);
+    border-radius: 2px;
+
+    .button-delete {
+      .deleteBtn();
+    }
+  }
+
+  .term-add {
+    display: inline-block;
+    width: 66px;
+    margin-left: 16px;
+    padding: 4px 8px;
+    color: #333;
+    background: #e8e8e8;
+    border: 1px solid #f0f0f0;
+    border-radius: 30px;
+    cursor: pointer;
+  }
+}

+ 29 - 0
src/pages/rule-engine/Scene/Save/terms/index.tsx

@@ -0,0 +1,29 @@
+import { useEffect } from 'react';
+import { TitleComponent } from '@/components';
+import { observer, Observer } from '@formily/react';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import BranchItem from './branchItem';
+
+export default observer(() => {
+  const queryColumn = () => {};
+
+  useEffect(() => {
+    if (FormModel.trigger?.device) {
+      queryColumn();
+    }
+  }, [FormModel.trigger?.device]);
+
+  return (
+    <div className="actions-terms">
+      <TitleComponent style={{ fontSize: 14 }} data="触发条件" />
+      <Observer>
+        {() =>
+          FormModel.branches?.map((item, index) => {
+            const isFrist = index === 0;
+            return <BranchItem data={item} isFrist={isFrist} name={index} />;
+          })
+        }
+      </Observer>
+    </div>
+  );
+});

+ 87 - 0
src/pages/rule-engine/Scene/Save/terms/paramsItem.tsx

@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import type { TermsType } from '@/pages/rule-engine/Scene/typings';
+import { DropdownButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
+import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import { observer } from '@formily/react';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import { get, set } from 'lodash';
+import './index.less';
+
+interface ParamsItemProps {
+  data: TermsType;
+  pName: (number | string)[];
+  name: number;
+  isLast: boolean;
+}
+
+export default observer((props: ParamsItemProps) => {
+  const [deleteVisible, setDeleteVisible] = useState(false);
+  const [paramOptions] = useState([]);
+  const [termTypeOptions] = useState([]);
+  const [valueOptions] = useState([]);
+
+  const deleteItem = () => {
+    const data = get(FormModel.branches, [...props.pName, 'terms']);
+    const indexOf = data?.findIndex((item: TermsType) => item.key === props.data.key);
+    if (indexOf !== undefined && indexOf !== -1) {
+      data!.splice(indexOf, 1);
+    }
+    set(FormModel.branches!, [...props.pName, 'terms'], data);
+  };
+
+  const addItem = () => {
+    const key = 'params_' + new Date().getTime();
+    const data = get(FormModel.branches, [...props.pName, 'terms']);
+    data?.push({
+      type: 'and',
+      column: undefined,
+      value: undefined,
+      termType: undefined,
+      key,
+    });
+  };
+
+  return (
+    <div className="terms-params-item">
+      <div
+        className="params-item_button"
+        onMouseOver={() => setDeleteVisible(true)}
+        onMouseOut={() => setDeleteVisible(false)}
+      >
+        <DropdownButton
+          options={paramOptions}
+          type="param"
+          placeholder="请选择参数"
+        ></DropdownButton>
+        <DropdownButton
+          options={termTypeOptions}
+          type="termType"
+          placeholder="操作符"
+        ></DropdownButton>
+        <DropdownButton options={valueOptions} type="value" placeholder="参数值"></DropdownButton>
+        <div className={classNames('button-delete', { show: deleteVisible })} onClick={deleteItem}>
+          <CloseOutlined />
+        </div>
+      </div>
+      {!props.isLast ? (
+        <div className="term-type-warp">
+          <DropdownButton
+            options={[
+              { title: '并且', key: 'and' },
+              { title: '或者', key: 'or' },
+            ]}
+            isTree={false}
+            type="type"
+            value="and"
+          ></DropdownButton>
+        </div>
+      ) : (
+        <div className="term-add" onClick={addItem}>
+          <PlusOutlined style={{ fontSize: 12, paddingRight: 4 }} />
+          <span>条件</span>
+        </div>
+      )}
+    </div>
+  );
+});

+ 98 - 0
src/pages/rule-engine/Scene/Save/terms/term.tsx

@@ -0,0 +1,98 @@
+import { observer, Observer } from '@formily/react';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import ParamsItem from './paramsItem';
+import { useState } from 'react';
+import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
+import { DropdownButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
+import classNames from 'classnames';
+import type { TermsType } from '@/pages/rule-engine/Scene/typings';
+import { get, set } from 'lodash';
+import './index.less';
+interface TermsProps {
+  data: TermsType;
+  pName: (number | string)[];
+  name: number;
+  isLast: boolean;
+}
+
+export default observer((props: TermsProps) => {
+  const [deleteVisible, setDeleteVisible] = useState(false);
+
+  const deleteTerms = () => {
+    const data = get(FormModel.branches, [...props.pName]);
+    const indexOf = data!.findIndex((item: TermsType) => item.key === props.data.key);
+    if (indexOf !== undefined && indexOf !== -1) {
+      data!.splice(indexOf, 1);
+    }
+    set(FormModel.branches!, [...props.pName], data);
+  };
+
+  const addTerms = () => {
+    const data = get(FormModel.branches, [...props.pName]);
+    const key = 'terms_' + new Date().getTime();
+    const defaultValue = {
+      type: 'and',
+      terms: [
+        {
+          column: undefined,
+          value: undefined,
+          key: 'params_1',
+          type: 'and',
+        },
+      ],
+      key,
+    };
+    data?.push(defaultValue);
+    console.log(FormModel.branches);
+  };
+
+  return (
+    <div className="terms-params">
+      <div className="terms-params-warp">
+        <div
+          className="terms-params-content"
+          onMouseOver={() => setDeleteVisible(true)}
+          onMouseOut={() => setDeleteVisible(false)}
+        >
+          <Observer>
+            {() =>
+              props.data.terms?.map((item, index) => (
+                <ParamsItem
+                  pName={[...props.pName, props.name]}
+                  name={index}
+                  data={item}
+                  key={item.key}
+                  isLast={index === props.data.terms!.length - 1}
+                />
+              ))
+            }
+          </Observer>
+          <div
+            className={classNames('terms-params-delete', { show: deleteVisible })}
+            onClick={deleteTerms}
+          >
+            <CloseOutlined />
+          </div>
+        </div>
+        {!props.isLast ? (
+          <div className="term-type-warp">
+            <DropdownButton
+              options={[
+                { title: '并且', key: 'and' },
+                { title: '或者', key: 'or' },
+              ]}
+              isTree={false}
+              type="type"
+              value={props.data.type}
+            ></DropdownButton>
+          </div>
+        ) : (
+          <div className="terms-add" onClick={addTerms}>
+            <PlusOutlined style={{ fontSize: 12, paddingRight: 4 }} />
+            <span>分组</span>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+});

+ 7 - 7
src/pages/rule-engine/Scene/index.tsx

@@ -204,7 +204,9 @@ const Scene = () => {
           }}
           onClick={() => {
             const url = getMenuPathByCode('rule-engine/Scene/Save');
-            history.push(`${url}?id=${record.id}`, { view: true });
+            history.push(`${url}?id=${record.id}&triggerType=${record.triggerType}`, {
+              view: true,
+            });
           }}
         >
           <EyeOutlined />
@@ -342,8 +344,6 @@ const Scene = () => {
             type="primary"
             isPermission={permission.add}
             onClick={() => {
-              // const url = getMenuPathByCode('rule-engine/Scene/Save');
-              // history.push(url);
               setCurrent({});
               setVisible(true);
             }}
@@ -367,10 +367,10 @@ const Scene = () => {
                   title: '查看',
                 }}
                 onClick={() => {
-                  // const url = getMenuPathByCode('rule-engine/Scene/Save');
-                  // history.push(`${url}?id=${record.id}`, { view: true });
-                  // setCurrent({})
-                  // setVisible(true)
+                  const url = getMenuPathByCode('rule-engine/Scene/Save');
+                  history.push(`${url}?id=${record.id}&triggerType=${record.triggerType}`, {
+                    view: true,
+                  });
                 }}
               >
                 <EyeOutlined />

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

@@ -164,6 +164,7 @@ export type TermsType = {
   termType?: string;
   options?: any[];
   terms?: TermsType[];
+  key?: string;
 };
 
 export type PlatformRelation = {
@@ -215,10 +216,10 @@ export type ActionDeviceMessageType = {
 };
 
 export interface ActionsDeviceProps {
-  productId?: string;
-  message?: ActionDeviceMessageType;
   selector: keyof typeof ActionDeviceSelector;
   source: keyof typeof ActionDeviceSource;
+  productId?: string;
+  message?: ActionDeviceMessageType;
   selectorValues?: SelectorValuesType[];
   /** 来源为upper时不能为空 */
   upperKey?: string;
@@ -229,12 +230,14 @@ export interface ActionsDeviceProps {
 export interface BranchesThen {
   parallel: boolean;
   actions: ActionsType;
+  key?: string;
 }
 
 export interface ActionBranchesProps {
   when: TermsType[];
   shakeLimit: ShakeLimitType;
   then: BranchesThen[];
+  key?: string;
 }
 
 export interface ActionsType {
@@ -251,6 +254,8 @@ export interface ActionsType {
     mode: keyof typeof ActionAlarmMode;
   };
   terms?: TermsType[];
+  /** map中的key,用于删除 */
+  key?: string;
 }
 
 export interface FormModelType {
@@ -267,7 +272,7 @@ export interface FormModelType {
   /**
    * 执行动作
    */
-  actions?: ActionsType[];
+  actions: ActionsType[];
   /**
    * 动作分支
    */