Przeglądaj źródła

feat(场景联动): 添加执行动作

xieyonghong 3 lat temu
rodzic
commit
a3c9dd96ab
27 zmienionych plików z 1654 dodań i 240 usunięć
  1. 22 6
      src/pages/device/Instance/Detail/Functions/form.tsx
  2. 1 0
      src/pages/device/Instance/index.tsx
  3. 4 1
      src/pages/device/Instance/service.ts
  4. 0 175
      src/pages/rule-engine/Scene/Save/action/VariableItem.tsx
  5. 139 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx
  6. 1 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/email.tsx
  7. 3 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/index.ts
  8. 63 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/org.tsx
  9. 232 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx
  10. 57 41
      src/pages/rule-engine/Scene/Save/action/action.tsx
  11. 152 0
      src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx
  12. 181 0
      src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx
  13. 144 0
      src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx
  14. 177 0
      src/pages/rule-engine/Scene/Save/action/device/index.tsx
  15. 24 0
      src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx
  16. 13 0
      src/pages/rule-engine/Scene/Save/action/device/service.ts
  17. 0 0
      src/pages/rule-engine/Scene/Save/action/device/sourceItem.tsx
  18. 261 0
      src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx
  19. 41 11
      src/pages/rule-engine/Scene/Save/action/messageContent.tsx
  20. 36 0
      src/pages/rule-engine/Scene/Save/action/service.ts
  21. 29 0
      src/pages/rule-engine/Scene/Save/components/DatePickerFormat/index.tsx
  22. 53 0
      src/pages/rule-engine/Scene/Save/components/InputNumber.tsx
  23. 1 1
      src/pages/rule-engine/Scene/Save/components/InputUpload/index.tsx
  24. 5 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx
  25. 2 0
      src/pages/rule-engine/Scene/Save/components/index.ts
  26. 12 4
      src/pages/rule-engine/Scene/Save/index.tsx
  27. 1 1
      src/pages/system/Department/Assets/product/bind.tsx

+ 22 - 6
src/pages/device/Instance/Detail/Functions/form.tsx

@@ -27,8 +27,20 @@ export default (props: FunctionProps) => {
   const formRef = useRef<ProFormInstance<any>>();
   const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
 
-  const getItemNode = (type: string) => {
+  const getItemNode = (record: any) => {
+    const type = record.type;
+    const name = record.name;
+
     switch (type) {
+      case 'enum':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+          />
+        );
       case 'boolean':
         return (
           <Select
@@ -37,19 +49,21 @@ export default (props: FunctionProps) => {
               { label: 'true', value: true },
               { label: 'false', value: false },
             ]}
-            placeholder={'请选择'}
+            placeholder={'请选择' + name}
           />
         );
       case 'int':
       case 'long':
       case 'float':
       case 'double':
-        return <InputNumber style={{ width: '100%' }} placeholder={'请输入'} />;
+        return <InputNumber style={{ width: '100%' }} placeholder={'请输入' + name} />;
       case 'date':
         // @ts-ignore
-        return <DatePicker style={{ width: '100%' }} />;
+        return (
+          <DatePicker format={record.format || 'YYYY-MM-DD HH:mm:ss'} style={{ width: '100%' }} />
+        );
       default:
-        return <Input placeholder={'请输入'} />;
+        return <Input placeholder={'请输入' + name} />;
     }
   };
 
@@ -75,7 +89,7 @@ export default (props: FunctionProps) => {
       align: 'center',
       width: 260,
       renderFormItem: (_, row: any) => {
-        return getItemNode(row.record.type);
+        return getItemNode(row.record);
       },
     },
   ];
@@ -89,6 +103,8 @@ export default (props: FunctionProps) => {
         id: datum.id,
         name: datum.name,
         type: datum.valueType ? datum.valueType.type : '-',
+        format: datum.valueType ? datum.valueType.format : undefined,
+        options: datum.valueType ? datum.valueType.elements : undefined,
         value: undefined,
       });
     }

+ 1 - 0
src/pages/device/Instance/index.tsx

@@ -84,6 +84,7 @@ const Instance = () => {
           value: location.state[key],
         });
       });
+      console.log(_terms);
       setJumpParams([
         {
           terms: _terms,

+ 4 - 1
src/pages/device/Instance/service.ts

@@ -10,7 +10,10 @@ class Service extends BaseService<DeviceInstance> {
 
   // 查询产品列表
   public getProductList = (params?: any) =>
-    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
 
   // 批量删除设备
   public batchDeleteDevice = (params: any) =>

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

@@ -1,175 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import { DatePicker, Input, InputNumber, Select, TreeSelect } from 'antd';
-import type { FormInstance } from 'antd';
-import { InputFile, ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
-import { TriggerWayType } from '@/pages/rule-engine/Scene/Save/components/TriggerWay';
-
-type ChangeType = {
-  source?: string;
-  value?: string;
-  upperKey?: string;
-};
-
-interface VariableItemProps {
-  data?: any;
-  value?: any;
-  notifyType?: string;
-  form: FormInstance;
-  onChange?: (value: ChangeType) => void;
-}
-
-const BuiltInSelectOptions = {
-  [TriggerWayType.timing]: [
-    { label: '设备名称', value: 'device-name' },
-    { label: '设备ID', value: 'device-id' },
-    { label: '产品名称', value: 'product-name' },
-    { label: '产品ID', value: 'product-id' },
-    { label: '系统时间', value: 'device-name' },
-    { label: '设备名称', value: 'device-name' },
-  ],
-  [TriggerWayType.manual]: [],
-  [TriggerWayType.device]: [],
-};
-
-export default (props: VariableItemProps) => {
-  const [value, setValue] = useState<ChangeType | undefined>(props.value);
-  const [type, setType] = useState('');
-
-  const [triggerType, setTriggerType] = useState('');
-
-  const getTrigger = () => {
-    const { trigger } = props.form.getFieldsValue([['trigger', 'type']]);
-    if (trigger) {
-      setTriggerType(trigger.type);
-    }
-  };
-
-  useEffect(() => {
-    setValue(props.value);
-  }, [props.value]);
-
-  useEffect(() => {
-    setType(props.data.expands ? props.data.expands.businessType : props.data.type);
-  }, []);
-
-  const itemOnChange = useCallback(
-    (val: any) => {
-      setValue({
-        source: value?.source,
-        value: val,
-      });
-      if (props.onChange) {
-        props.onChange({
-          source: value?.source,
-          value: val,
-        });
-      }
-    },
-    [props.onChange, value],
-  );
-
-  const inputNodeByType = useCallback(
-    (data: any) => {
-      const _type = data.expands ? data.expands.businessType : data.type;
-      switch (_type) {
-        case 'enum':
-          return (
-            <Select
-              placeholder={`请选择${data.name}`}
-              style={{ width: '100%' }}
-              onChange={itemOnChange}
-            />
-          );
-        case 'date':
-          return (
-            // @ts-ignore
-            <DatePicker
-              style={{ width: '100%' }}
-              format={data.format || 'YYYY-MM-DD HH:mm:ss'}
-              onChange={(date) => {
-                itemOnChange(date?.format(data.format || 'YYYY-MM-DD HH:mm:ss'));
-              }}
-            />
-          );
-        case 'number':
-          return (
-            <InputNumber
-              placeholder={`请输入${data.name}`}
-              style={{ width: '100%' }}
-              onChange={itemOnChange}
-            />
-          );
-        case 'file':
-          return <InputFile onChange={itemOnChange} />;
-        case 'org':
-          return <TreeSelect onChange={itemOnChange} />;
-        case 'user':
-          return <Select onChange={itemOnChange} />;
-        default:
-          return (
-            <Input
-              placeholder={`请输入${data.name}`}
-              onChange={(e) => itemOnChange(e.target.value)}
-            />
-          );
-      }
-    },
-    [props.onChange],
-  );
-
-  const addonBefore = useCallback(
-    (_type: string) => {
-      switch (_type) {
-        case 'org':
-          return null;
-        case 'user':
-          const options = [
-            { label: '平台用户', value: 'fixed' },
-            { label: props.notifyType === 'dingTalk' ? '钉钉用户' : '微信用户', value: 'relation' },
-          ];
-          return <Select value={value && value.source} options={options} style={{ width: 120 }} />;
-        default:
-          return (
-            <Select
-              value={value && value.source}
-              options={[
-                { label: '手动输入', value: 'fixed' },
-                { label: '内置参数', value: 'upper' },
-              ]}
-              style={{ width: 120 }}
-              onChange={(key) => {
-                if (key === 'upper') {
-                  getTrigger();
-                }
-                if (props.onChange) {
-                  props.onChange({ source: key, value: undefined });
-                }
-                setValue({ source: key, value: undefined });
-              }}
-            />
-          );
-      }
-    },
-    [props.onChange, value, props.notifyType],
-  );
-
-  return (
-    <ItemGroup>
-      {addonBefore(type)}
-      {value && value.source === 'upper' ? (
-        <Select
-          value={value.upperKey}
-          options={BuiltInSelectOptions[triggerType] || []}
-          style={{ width: '100%' }}
-          onChange={(key) => {
-            if (props.onChange) {
-              props.onChange({ source: value.source, value: undefined, upperKey: key });
-            }
-          }}
-        />
-      ) : (
-        <div> {inputNodeByType(props.data)} </div>
-      )}
-    </ItemGroup>
-  );
-};

+ 139 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx

@@ -0,0 +1,139 @@
+import { DatePicker, Input, InputNumber, Select } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { useRequest } from 'umi';
+import { queryBuiltInParams } from '@/pages/rule-engine/Scene/Save/action/service';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import moment from 'moment';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  upperKey?: string;
+};
+
+interface BuiltInProps {
+  value?: ChangeType;
+  data?: any;
+  type?: string;
+  onChange?: (value: ChangeType) => void;
+}
+
+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([]);
+
+  const { run: getBuiltInList } = useRequest(queryBuiltInParams, {
+    manual: true,
+    formatResult: (res) => res.result,
+    onSuccess: (res) => {
+      setBuiltInList(res);
+    },
+  });
+
+  useEffect(() => {
+    if (source === 'upper') {
+      getBuiltInList({
+        trigger: { type: props.type },
+      });
+    }
+  }, [source, props.type]);
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    setValue(props.value?.value);
+    setUpperKey(props.value?.upperKey);
+  }, [props.value]);
+
+  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 itemOnChange = useCallback(
+    (_value: any) => {
+      onChange(source, _value);
+    },
+    [source],
+  );
+
+  const inputNodeByType = useCallback(
+    (data: any) => {
+      switch (data.type) {
+        case 'date':
+          return (
+            // @ts-ignore
+            <DatePicker
+              value={value ? moment(value) : undefined}
+              style={{ width: '100%' }}
+              format={data.format || 'YYYY-MM-DD HH:mm:ss'}
+              onChange={(date) => {
+                itemOnChange(date?.format(data.format || 'YYYY-MM-DD HH:mm:ss'));
+              }}
+            />
+          );
+        case 'number':
+          return (
+            <InputNumber
+              value={value}
+              placeholder={`请输入${data.name}`}
+              style={{ width: '100%' }}
+              onChange={itemOnChange}
+            />
+          );
+        default:
+          return (
+            <Input
+              value={value}
+              placeholder={`请输入${data.name}`}
+              onChange={(e) => itemOnChange(e.target.value)}
+            />
+          );
+      }
+    },
+    [value],
+  );
+
+  return (
+    <ItemGroup>
+      <Select
+        value={source}
+        options={[
+          { label: '手动输入', value: 'fixed' },
+          { label: '内置参数', value: 'upper' },
+        ]}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onChange(key, undefined, undefined);
+        }}
+      ></Select>
+      {source === 'upper' ? (
+        <Select
+          value={upperKey}
+          options={builtInList}
+          onChange={(key) => {
+            onChange(source, undefined, key);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择参数'}
+        />
+      ) : (
+        <div>{inputNodeByType(props.data)}</div>
+      )}
+    </ItemGroup>
+  );
+};

+ 1 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/email.tsx

@@ -0,0 +1 @@
+export default () => {};

+ 3 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/index.ts

@@ -0,0 +1,3 @@
+export { default as UserList } from './user';
+export { default as OrgList } from './org';
+export { default as BuiltIn } from './builtIn';

+ 63 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/org.tsx

@@ -0,0 +1,63 @@
+import { TreeSelect } from 'antd';
+import { useEffect, useState } from 'react';
+import {
+  queryDingTalkDepartments,
+  queryWechatDepartments,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+
+type ChangeType = {
+  value?: string[];
+};
+
+interface OrgProps {
+  value?: ChangeType;
+  data?: any;
+  notifyType: string;
+  configId: string;
+  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 (props.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(() => {
+    getDepartment(props.configId);
+  }, []);
+
+  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({ value: key });
+        }
+      }}
+      placeholder={'请选择部门'}
+    />
+  );
+};

+ 232 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx

@@ -0,0 +1,232 @@
+// 收信人
+import { useEffect, useState } from 'react';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import { Select } from 'antd';
+import {
+  queryDingTalkUsers,
+  queryPlatformUsers,
+  queryRelationUsers,
+  queryWechatUsers,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  relation?: any;
+};
+
+interface UserProps {
+  notifyType: string;
+  configId: string;
+  value?: ChangeType;
+  type?: string;
+  onChange?: (value: ChangeType) => void;
+}
+
+export default (props: UserProps) => {
+  const [source, setSource] = useState(props.value?.source);
+  const [value, setValue] = useState<string | undefined>('');
+  const [userList, setUserList] = useState({ platform: [], relation: [] });
+  const [relationList, setRelationList] = useState([]);
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    if (props.value?.source === 'relation') {
+      const relation = props.value?.relation;
+      console.log(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 _userList: any = {
+      platform: [],
+      relation: [],
+    };
+    const resp1 = await queryPlatformUsers();
+    if (resp1.status === 200) {
+      _userList.platform = resp1.result.map((item: any) => ({ label: item.name, value: item.id }));
+    }
+
+    const resp2 = await queryRelationUsers();
+    if (resp2.status === 200) {
+      _userList.relation = resp2.result.map((item: any) => ({
+        label: item.name,
+        value: item.relation,
+      }));
+    }
+
+    setUserList(_userList);
+  };
+
+  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') {
+      // 钉钉,微信用户
+      getRelationUsers(props.notifyType, props.configId);
+    } else {
+      getPlatformUser();
+    }
+  }, [source, props.notifyType]);
+
+  const options = [
+    { label: '平台用户', value: 'relation' },
+    { label: props.notifyType === 'dingTalk' ? '钉钉用户' : '微信用户', value: 'fixed' },
+  ];
+
+  /**
+   * 收信人-平台用户格式
+   * {
+   *   source: 'relation',
+   *   relation: {
+   *     related: {
+   *       objectType: 'user',
+   *       relation: 'userId'
+   *     }
+   *   }
+   * }
+   * 收信人-平台-关系用户格式
+   * {
+   *   source: 'relation',
+   *   relation: {
+   *     objectType: 'device',
+   *     objectSource: {
+   *       source: 'upper',
+   *       upperKey: 'deviceId'
+   *     },
+   *     related: {
+   *       objectType: 'user',
+   *       relation: 'userId'
+   *     }
+   *   }
+   * }
+   * 收信人-钉钉/微信用户
+   * {
+   *   source: 'relation',
+   *   value: 'userId'
+   * }
+   * @param _source
+   * @param _value
+   * @param type
+   */
+  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;
+    }
+    console.log(obj);
+    if (props.onChange) {
+      props.onChange(obj);
+    }
+  };
+
+  const filterOption = (input: string, option: any) => {
+    return option.children ? option.children.toLowerCase().includes(input.toLowerCase()) : false;
+  };
+
+  return (
+    <ItemGroup>
+      <Select
+        value={source}
+        options={options}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onchange(key, undefined);
+        }}
+      />
+      {source === 'relation' ? (
+        <Select
+          showSearch
+          value={value}
+          onChange={(key, node) => {
+            setValue(key);
+            onchange(source, key, node.isRelation);
+          }}
+          placeholder={'请选择收信人'}
+          listHeight={200}
+          filterOption={filterOption}
+        >
+          {userList.platform.length ? (
+            <Select.OptGroup label={'平台用户'}>
+              {userList.platform.map((item: any) => (
+                <Select.Option value={item.value} isRelation={false}>
+                  {item.label}
+                </Select.Option>
+              ))}
+            </Select.OptGroup>
+          ) : null}
+          {userList.relation.length ? (
+            <Select.OptGroup label={'关系用户'}>
+              {userList.relation.map((item: any) => (
+                <Select.Option value={item.value} isRelation={true}>
+                  {item.label}
+                </Select.Option>
+              ))}
+            </Select.OptGroup>
+          ) : null}
+        </Select>
+      ) : (
+        <Select
+          showSearch
+          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;
+          }}
+        ></Select>
+      )}
+    </ItemGroup>
+  );
+};

+ 57 - 41
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -1,4 +1,4 @@
-import { Button, InputNumber, Select, Form } from 'antd';
+import { Button, Select, Form } from 'antd';
 import type { FormInstance } from 'antd';
 import { useEffect, useState } from 'react';
 import { useRequest } from 'umi';
@@ -9,20 +9,32 @@ import {
   queryMessageTemplateDetail,
 } from './service';
 import MessageContent from './messageContent';
+import DeviceSelect, { MessageTypeEnum } from './device';
+import WriteProperty from './device/WriteProperty';
+import ReadProperty from './device/readProperty';
+import FunctionCall from './device/functionCall';
+import { InputNumber } from '../components';
 
 interface ActionProps {
   restField: any;
   name: number;
   form: FormInstance;
   title?: string;
+  triggerType: string;
   onRemove: () => void;
 }
 
 const ActionItem = (props: ActionProps) => {
   const { name } = props;
   const [type1, setType1] = useState('');
+  // 消息通知
   const [notifyType, setNotifyType] = useState('');
+  const [configId, setConfigId] = useState('');
   const [templateData, setTemplateData] = useState<any>(undefined);
+  // 设备输出
+  const [deviceMessageType, setDeviceMessageType] = useState('WRITE_PROPERTY');
+  const [properties, setProperties] = useState([]); // 物模型-属性
+  const [functionList, setFunctionList] = useState([]); // 物模型-功能
 
   const { data: messageType, run: queryMessageTypes } = useRequest(queryMessageType, {
     manual: true,
@@ -71,11 +83,16 @@ const ActionItem = (props: ActionProps) => {
           options={messageConfig}
           loading={messageConfigLoading}
           fieldNames={{ value: 'id', label: 'name' }}
-          onChange={async (key: string) => {
+          onChange={async (key: string, node: any) => {
+            setConfigId(key);
             setTemplateData(undefined);
             props.form.resetFields([['actions', name, 'notify', 'templateId']]);
+
             await queryMessageTemplates({
-              terms: [{ column: 'configId', value: key }],
+              terms: [
+                { column: 'type', value: notifyType },
+                { column: 'provider', value: node.provider },
+              ],
             });
           }}
           style={{ width: 160 }}
@@ -100,48 +117,12 @@ const ActionItem = (props: ActionProps) => {
     </>
   );
 
-  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'}>
@@ -166,9 +147,21 @@ const ActionItem = (props: ActionProps) => {
           />
         </Form.Item>
         {type1 === 'message' && MessageNodes}
-        {type1 === 'device' && DeviceNodes}
+        {type1 === 'device' && (
+          <DeviceSelect
+            name={props.name}
+            form={props.form}
+            triggerType={props.triggerType}
+            onProperties={setProperties}
+            onMessageTypeChange={setDeviceMessageType}
+            onFunctionChange={setFunctionList}
+            restField={props.restField}
+          />
+        )}
         {type1 === 'delay' && (
-          <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+          <Form.Item name={[name, 'delay']}>
+            <InputNumber />
+          </Form.Item>
         )}
       </div>
       {type1 === 'message' && templateData ? (
@@ -177,8 +170,31 @@ const ActionItem = (props: ActionProps) => {
           template={templateData}
           name={props.name}
           notifyType={notifyType}
+          triggerType={props.triggerType}
+          configId={configId}
         />
       ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.WRITE_PROPERTY &&
+      properties.length ? (
+        <Form.Item name={[name, 'device', 'message', 'properties']}>
+          <WriteProperty properties={properties} type={props.triggerType} form={props.form} />
+        </Form.Item>
+      ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.READ_PROPERTY &&
+      properties.length ? (
+        <Form.Item name={[name, 'device', 'message', 'properties']}>
+          <ReadProperty properties={properties} />
+        </Form.Item>
+      ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.INVOKE_FUNCTION &&
+      functionList.length ? (
+        <Form.Item name={[name, 'device', 'message', 'inputs']}>
+          <FunctionCall functionData={functionList} />
+        </Form.Item>
+      ) : null}
     </div>
   );
 };

+ 152 - 0
src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx

@@ -0,0 +1,152 @@
+import { DatePicker, FormInstance, Input, InputNumber, Select, Space } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { useRequest } from '@@/plugin-request/request';
+import { queryBuiltInParams } from '@/pages/rule-engine/Scene/Save/action/service';
+import moment from 'moment';
+
+interface WritePropertyProps {
+  properties: any[];
+  type: string;
+  form: FormInstance;
+  value?: any;
+  onChange?: (value?: any) => void;
+}
+
+export default (props: WritePropertyProps) => {
+  console.log(props.properties);
+  const [source, setSource] = useState('fixed');
+  const [builtInList, setBuiltInList] = useState([]);
+  const [propertiesKey, setPropertiesKey] = useState<string | undefined>(undefined);
+  const [propertiesValue, setPropertiesValue] = useState(undefined);
+  const [propertiesType, setPropertiesType] = useState('');
+
+  const { run: getBuiltInList } = useRequest(queryBuiltInParams, {
+    manual: true,
+    formatResult: (res) => res.result,
+    onSuccess: (res) => {
+      setBuiltInList(res);
+    },
+  });
+
+  useEffect(() => {
+    if (source === 'upper') {
+      getBuiltInList({
+        trigger: { type: props.type },
+      });
+    }
+  }, [source, props.type]);
+
+  useEffect(() => {
+    if (props.value) {
+      if (0 in props.value) {
+        setPropertiesValue(props.value[0]);
+      } else {
+        Object.keys(props.value).forEach((key: string) => {
+          setPropertiesKey(key);
+          setPropertiesValue(props.value[key]);
+        });
+      }
+    }
+  }, [props.value]);
+
+  const onChange = (key?: string, value?: any) => {
+    if (props.onChange) {
+      props.onChange({
+        [key || 0]: value,
+      });
+    }
+  };
+
+  const inputNodeByType = useCallback(
+    (type: string) => {
+      switch (type) {
+        case 'boolean':
+          return (
+            <Select
+              style={{ width: 300, textAlign: 'left' }}
+              value={propertiesValue}
+              options={[
+                { label: 'true', value: true },
+                { label: 'false', value: false },
+              ]}
+              placeholder={'请选择'}
+              onChange={(value) => {
+                onChange(propertiesKey, value);
+              }}
+            />
+          );
+        case 'int':
+        case 'long':
+        case 'float':
+        case 'double':
+          return (
+            <InputNumber
+              style={{ width: 300 }}
+              value={propertiesValue}
+              placeholder={'请输入'}
+              onChange={(value) => {
+                onChange(propertiesKey, value);
+              }}
+            />
+          );
+        case 'date':
+          return (
+            // @ts-ignore
+            <DatePicker
+              style={{ width: 300 }}
+              value={propertiesValue ? moment(propertiesValue) : undefined}
+              onChange={(date) => {
+                onChange(propertiesKey, date ? date.format('YYYY-MM-DD HH:mm:ss') : undefined);
+              }}
+            />
+          );
+        default:
+          return (
+            <Input
+              style={{ width: 300 }}
+              value={propertiesValue}
+              placeholder={'请输入'}
+              onChange={(e) => onChange(propertiesKey, e.target.value)}
+            />
+          );
+      }
+    },
+    [propertiesKey, propertiesValue],
+  );
+
+  return (
+    <Space>
+      <Select
+        value={propertiesKey}
+        options={props.properties}
+        fieldNames={{ label: 'name', value: 'id' }}
+        style={{ width: 120 }}
+        onSelect={(key: any, node: any) => {
+          onChange(key, undefined);
+          setPropertiesType(node.valueType.type);
+        }}
+        placeholder={'请选择属性'}
+      ></Select>
+      <Select
+        value={source}
+        options={[
+          { label: '手动输入', value: 'fixed' },
+          { label: '内置参数', value: 'upper' },
+        ]}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+        }}
+      />
+      {source === 'upper' ? (
+        <Select
+          options={builtInList}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择参数'}
+        />
+      ) : (
+        <div>{inputNodeByType(propertiesType)}</div>
+      )}
+    </Space>
+  );
+};

+ 181 - 0
src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx

@@ -0,0 +1,181 @@
+import { Badge, Input, message, Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { DeviceItem } from '@/pages/system/Department/typings';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import SearchComponent from '@/components/SearchComponent';
+import ProTable from '@jetlinks/pro-table';
+import { queryDevice } from '@/pages/rule-engine/Scene/Save/action/device/service';
+
+interface DeviceModelProps {
+  value?: ChangeValueType[];
+  onChange?: (value: ChangeValueType[]) => void;
+  productId?: string;
+}
+
+type DeviceBadgeProps = {
+  type: string;
+  text: string;
+};
+
+type ChangeValueType = {
+  name: string;
+  value: string;
+};
+
+const DeviceBadge = (props: DeviceBadgeProps) => {
+  const STATUS = {
+    notActive: 'processing',
+    offline: 'error',
+    online: 'success',
+  };
+  return <Badge status={STATUS[props.type]} text={props.text} />;
+};
+
+export default (props: DeviceModelProps) => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [visible, setVisible] = useState(false);
+  const [selectKeys, setSelectKeys] = useState<ChangeValueType[]>(props.value || []);
+  const [searchParam, setSearchParam] = useState({});
+  const [value, setValue] = useState<ChangeValueType[]>(props.value || []);
+
+  useEffect(() => {
+    console.log('deviceModal', props.value);
+    setValue(props.value || []);
+    setSelectKeys(props.value || []);
+  }, [props.value]);
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      valueType: 'dateTime',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        all: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.all',
+            defaultMessage: '全部',
+          }),
+          status: 'Default',
+        },
+        onLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'onLine',
+        },
+        offLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offLine',
+        },
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '未启用',
+          }),
+          status: 'notActive',
+        },
+      },
+      search: false,
+      render: (_, row) => <DeviceBadge type={row.state.value} text={row.state.text} />,
+    },
+  ];
+
+  console.log(selectKeys.map((item) => item.value));
+
+  return (
+    <>
+      {visible && (
+        <Modal
+          visible={visible}
+          title={'设备'}
+          width={880}
+          onOk={() => {
+            if (props.onChange) {
+              props.onChange(selectKeys);
+            }
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+          onCancel={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+            setSelectKeys([]);
+          }}
+        >
+          <SearchComponent<DeviceItem>
+            field={columns}
+            enableSave={false}
+            // pattern={'simple'}
+            onSearch={async (data) => {
+              actionRef.current?.reset?.();
+              setSearchParam(data);
+            }}
+            defaultParam={[{ column: 'productId', value: props.productId! }]}
+            target="scene-actions-device"
+          />
+          <ProTable<DeviceItem>
+            actionRef={actionRef}
+            columns={columns}
+            rowKey="id"
+            search={false}
+            rowSelection={{
+              selectedRowKeys: selectKeys.map((item) => item.value),
+              onSelect: (a: any, b: any, selectedRowKeys) => {
+                console.log(selectedRowKeys);
+              },
+              onChange: (selectedRowKeys, selectedRows) => {
+                console.log(selectedRows);
+                setSelectKeys(selectedRows.map((item) => ({ name: item.name, value: item.id })));
+              },
+            }}
+            request={(params) => queryDevice(params)}
+            params={searchParam}
+          ></ProTable>
+        </Modal>
+      )}
+      <Input
+        placeholder={'请选择设备'}
+        onClick={() => {
+          if (!props.productId) {
+            message.warning('请选择产品');
+          } else {
+            setVisible(true);
+            setSelectKeys([...value]);
+          }
+        }}
+        style={{ width: 300 }}
+        value={value.map((item) => item.name).toString()}
+        readOnly
+      />
+    </>
+  );
+};

+ 144 - 0
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -0,0 +1,144 @@
+import type { ProColumns } from '@jetlinks/pro-table';
+import { EditableProTable } from '@jetlinks/pro-table';
+import { DatePicker, Input, InputNumber, Select } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import ProForm from '@ant-design/pro-form';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import moment from 'moment';
+
+type FunctionTableDataType = {
+  id: string;
+  name: string;
+  type: string;
+};
+
+interface FunctionCallProps {
+  functionData: any[];
+  value?: any;
+  onChange?: (data: any) => void;
+}
+
+export default (props: FunctionCallProps) => {
+  const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
+  const formRef = useRef<ProFormInstance<any>>();
+
+  useEffect(() => {
+    setEditableRowKeys(props.functionData.map((d) => d.id));
+    formRef.current?.setFieldsValue({
+      table: props.functionData,
+    });
+  }, [props.functionData]);
+
+  useEffect(() => {
+    if (props.functionData && props.functionData.length && props.value) {
+      formRef.current?.setFieldsValue({
+        table: props.functionData.map((item: any) => {
+          const oldValue = props.value.find((oldItem: any) => oldItem.name === item.id);
+          if (oldValue) {
+            return {
+              ...item,
+              value: oldValue.value,
+            };
+          }
+          return item;
+        }),
+      });
+    }
+    console.log(props.value);
+  }, []);
+
+  const getItemNode = (record: any) => {
+    const type = record.type;
+    const name = record.name;
+
+    switch (type) {
+      case 'enum':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+          />
+        );
+      case 'boolean':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={[
+              { label: 'true', value: true },
+              { label: 'false', value: false },
+            ]}
+            placeholder={'请选择' + name}
+          />
+        );
+      case 'int':
+      case 'long':
+      case 'float':
+      case 'double':
+        return <InputNumber style={{ width: '100%' }} placeholder={'请输入' + name} />;
+      case 'date':
+        // @ts-ignore
+        return (
+          <DatePicker format={record.format || 'YYYY-MM-DD HH:mm:ss'} style={{ width: '100%' }} />
+        );
+      default:
+        return <Input placeholder={'请输入' + name} />;
+    }
+  };
+
+  const columns: ProColumns<FunctionTableDataType>[] = [
+    {
+      dataIndex: 'name',
+      title: '参数名称',
+      width: 200,
+      editable: false,
+    },
+    {
+      dataIndex: 'type',
+      title: '类型',
+      width: 200,
+      editable: false,
+    },
+    {
+      title: '值',
+      dataIndex: 'value',
+      align: 'center',
+      width: 260,
+      renderFormItem: (_, row: any) => {
+        return getItemNode(row.record);
+      },
+    },
+  ];
+
+  return (
+    <ProForm<{ table: FunctionTableDataType[] }>
+      formRef={formRef}
+      submitter={false}
+      onValuesChange={() => {
+        const values = formRef.current?.getFieldsValue();
+        if (props.onChange) {
+          props.onChange(
+            values.table.map((item: any) => ({
+              name: item.id,
+              value: item.type === 'date' ? moment(item.value).format(item.format) : item.value,
+            })),
+          );
+        }
+      }}
+    >
+      <EditableProTable
+        rowKey="id"
+        name="table"
+        columns={columns}
+        recordCreatorProps={false}
+        size={'small'}
+        editable={{
+          type: 'multiple',
+          editableKeys,
+          onChange: setEditableRowKeys,
+        }}
+      />
+    </ProForm>
+  );
+};

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

@@ -0,0 +1,177 @@
+import { Form, Input, Select } from 'antd';
+import type { FormInstance } from 'antd';
+import { useEffect, useState } from 'react';
+import { getProductList } from '@/pages/rule-engine/Scene/Save/action/device/service';
+import Device from './deviceModal';
+import TagModal from './tagModal';
+
+interface DeviceProps {
+  name: number;
+  triggerType: string;
+  form?: FormInstance;
+  restField?: any;
+  onProperties: (data: any) => void;
+  onMessageTypeChange: (type: string) => void;
+  onFunctionChange: (functionItem: any) => void;
+}
+
+enum SourceEnum {
+  'fixed' = 'fixed',
+  'tag' = 'tag',
+  'relation' = '',
+}
+
+const DefaultSourceOptions = [
+  { label: '固定设备', value: SourceEnum.fixed },
+  { label: '按标签', value: SourceEnum.tag },
+];
+
+export enum MessageTypeEnum {
+  'WRITE_PROPERTY' = 'WRITE_PROPERTY',
+  'READ_PROPERTY' = 'READ_PROPERTY',
+  'INVOKE_FUNCTION' = 'INVOKE_FUNCTION',
+}
+
+export default (props: DeviceProps) => {
+  const { name } = props;
+
+  const [productId, setProductId] = useState<string>('');
+  const [sourceList, setSourceList] = useState(DefaultSourceOptions);
+  const [productList, setProductList] = useState<any[]>([]);
+  const [selector, setSelector] = useState(SourceEnum.fixed);
+  const [messageType, setMessageType] = useState(MessageTypeEnum.WRITE_PROPERTY);
+  const [functionList, setFunctionList] = useState([]);
+  const [tagList, setTagList] = useState([]);
+
+  const getProducts = async () => {
+    const resp = await getProductList({ paging: false });
+    if (resp.status === 200) {
+      setProductList(resp.result);
+    }
+  };
+  useEffect(() => {
+    props.form?.resetFields([['actions', name, 'device', 'selector']]);
+    if (props.triggerType === 'device') {
+      setSourceList([...DefaultSourceOptions, { label: '按关系', value: SourceEnum.relation }]);
+    } else {
+      setSourceList(DefaultSourceOptions);
+    }
+  }, [props.triggerType]);
+
+  useEffect(() => {
+    getProducts();
+  }, []);
+
+  const handleMetadata = (metadata?: string) => {
+    try {
+      const metadataObj = JSON.parse(metadata || '{}');
+      if (props.onProperties) {
+        props.onProperties(metadataObj.properties || []);
+      }
+      if (!metadataObj.functions) {
+        if (props.onFunctionChange) {
+          props.onFunctionChange([]);
+        }
+      }
+      setTagList(metadataObj.tags || []);
+      setFunctionList(metadataObj.functions || []);
+    } catch (err) {
+      console.warn('handleMetadata === ', err);
+    }
+  };
+
+  return (
+    <>
+      <Form.Item name={[name, 'device', 'productId']}>
+        <Select
+          options={productList}
+          placeholder={'请选择产品'}
+          style={{ width: 220 }}
+          listHeight={220}
+          onChange={(key: any, node: any) => {
+            props.form?.resetFields([['actions', name, 'device', 'selector']]);
+            props.form?.resetFields([['actions', name, 'device', 'selectorValues']]);
+            props.form?.resetFields([['actions', name, 'device', 'message', 'functionId']]);
+            // setMessageType(MessageTypeEnum.WRITE_PROPERTY)
+            setProductId(key);
+            handleMetadata(node.metadata);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+        />
+      </Form.Item>
+      <Form.Item
+        name={[name, 'device', 'selector']}
+        initialValue={SourceEnum.fixed}
+        {...props.restField}
+      >
+        <Select
+          options={sourceList}
+          style={{ width: 120 }}
+          onChange={(key) => {
+            setSelector(key);
+          }}
+        />
+      </Form.Item>
+      {selector === SourceEnum.fixed && (
+        <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+          <Device productId={productId} />
+        </Form.Item>
+      )}
+      {selector === SourceEnum.tag && (
+        <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+          <TagModal tagData={tagList} />
+        </Form.Item>
+      )}
+      <Form.Item
+        name={[name, 'device', 'message', 'messageType']}
+        initialValue={MessageTypeEnum.WRITE_PROPERTY}
+        {...props.restField}
+      >
+        <Select
+          options={[
+            { label: '功能调用', value: MessageTypeEnum.INVOKE_FUNCTION },
+            { label: '读取属性', value: MessageTypeEnum.READ_PROPERTY },
+            { label: '设置属性', value: MessageTypeEnum.WRITE_PROPERTY },
+          ]}
+          onSelect={(key: any) => {
+            if (props.onMessageTypeChange) {
+              props.onMessageTypeChange(key);
+            }
+            setMessageType(key);
+          }}
+          style={{ width: 120 }}
+        />
+      </Form.Item>
+      {messageType === MessageTypeEnum.INVOKE_FUNCTION ? (
+        <Form.Item name={[name, 'device', 'message', 'functionId']}>
+          <Select
+            options={functionList}
+            fieldNames={{ label: 'name', value: 'id' }}
+            style={{ width: 120 }}
+            placeholder={'请选择功能'}
+            onSelect={(_: any, data: any) => {
+              const properties = data.valueType ? data.valueType.properties : data.inputs;
+              if (props.onFunctionChange) {
+                const array = [];
+                for (const datum of properties) {
+                  array.push({
+                    id: datum.id,
+                    name: datum.name,
+                    type: datum.valueType ? datum.valueType.type : '-',
+                    format: datum.valueType ? datum.valueType.format : undefined,
+                    options: datum.valueType ? datum.valueType.elements : undefined,
+                    value: undefined,
+                  });
+                }
+                props.onFunctionChange(array);
+              }
+            }}
+          />
+        </Form.Item>
+      ) : null}
+      <Form.Item name={[name, 'device', 'source']} hidden>
+        <Input />
+      </Form.Item>
+    </>
+  );
+};

+ 24 - 0
src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx

@@ -0,0 +1,24 @@
+import { Select } from 'antd';
+
+interface ReadPropertyProps {
+  properties: any[];
+  value?: any;
+  onChange?: (value?: any) => void;
+}
+
+export default (props: ReadPropertyProps) => {
+  return (
+    <Select
+      value={props.value ? props.value[0] : undefined}
+      options={props.properties}
+      fieldNames={{ label: 'name', value: 'id' }}
+      style={{ width: 120 }}
+      onSelect={(key: any) => {
+        if (props.onChange) {
+          props.onChange([key]);
+        }
+      }}
+      placeholder={'请选择属性'}
+    ></Select>
+  );
+};

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

@@ -0,0 +1,13 @@
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+// 获取设备
+export const queryDevice = (data: any) =>
+  request(`${SystemConst.API_BASE}/device-instance/_query`, {
+    method: 'POST',
+    data: data,
+  });
+
+// 获取产品
+export const getProductList = (params?: any) =>
+  request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });

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


+ 261 - 0
src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx

@@ -0,0 +1,261 @@
+import { Modal, Input, Space, Select, InputNumber, DatePicker, Row, Col, Button } from 'antd';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
+
+type TagValueType = {
+  column: string;
+  value: any;
+  type: string;
+};
+
+interface TagModalProps {
+  tagData: any[];
+  value?: TagValueType[];
+  onChange?: (value: any[]) => void;
+}
+
+/**
+ * 数据格式 [{"value":{"key":"value"},"name":"标签名称"}]
+ * @param props
+ */
+export default (props: TagModalProps) => {
+  const [visible, setVisible] = useState(false);
+  const [tagList, setTagList] = useState<any[]>([{}]);
+  const [options, setOptions] = useState<any[]>([]);
+  const [nameList, setNameList] = useState<string[]>([]);
+
+  const handleItem = (data: any) => {
+    return {
+      ...data,
+      valueType: data.valueType ? data.valueType.type : '-',
+      format: data.valueType ? data.valueType.format : undefined,
+      options: data.valueType ? data.valueType.elements : undefined,
+      value: data.value,
+    };
+  };
+
+  useEffect(() => {
+    if (visible) {
+      setOptions(
+        props.tagData.map((item: any) => {
+          return { label: item.name, value: item.id, ...item };
+        }),
+      );
+    }
+  }, [visible, props.tagData]);
+
+  useEffect(() => {
+    if (props.value) {
+      const names: string[] = [];
+      const newTagList = props.value
+        .filter((valueItem) => {
+          return props.tagData.some((item) => valueItem.column === item.id);
+        })
+        .map((valueItem) => {
+          const oldItem = props.tagData.find((item) => item.id === valueItem.column);
+          if (oldItem) {
+            names.push(oldItem.name);
+            return {
+              ...handleItem(oldItem),
+              type: valueItem.type,
+            };
+          }
+          return valueItem;
+        });
+      console.log(names);
+      setNameList(names);
+      setTagList(newTagList);
+    } else {
+      setTagList([{}]);
+    }
+  }, [props.value]);
+
+  const getItemNode = (record: any) => {
+    const type = record.valueType;
+    const name = record.name;
+
+    switch (type) {
+      case 'enum':
+        return (
+          <Select
+            value={record.value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'boolean':
+        return (
+          <Select
+            value={record.value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={[
+              { label: 'true', value: true },
+              { label: 'false', value: false },
+            ]}
+            placeholder={'请选择' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'int':
+      case 'long':
+      case 'float':
+      case 'double':
+        return (
+          <InputNumber
+            value={record.value}
+            style={{ width: '100%' }}
+            placeholder={'请输入' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'date':
+        // @ts-ignore
+        return (
+          <DatePicker
+            value={record.value && moment(record.value)}
+            format={record.format || 'YYYY-MM-DD HH:mm:ss'}
+            style={{ width: '100%' }}
+            onChange={(_, date) => {
+              record.value = date;
+            }}
+          />
+        );
+      default:
+        return (
+          <Input
+            value={record.value}
+            placeholder={'请输入标签值'}
+            onChange={(e) => {
+              record.value = e.target.value;
+            }}
+          />
+        );
+    }
+  };
+
+  return (
+    <>
+      <Modal
+        visible={visible}
+        title={'设备'}
+        width={660}
+        onOk={() => {
+          const newValue = tagList
+            .filter((item) => !!item.value)
+            .map((item: any) => {
+              return {
+                column: item.value,
+                type: item.type,
+                name: item.id,
+              };
+            });
+          if (props.onChange) {
+            props.onChange(newValue);
+          }
+          console.log(newValue);
+          setVisible(false);
+          setTagList([{}]);
+        }}
+        onCancel={() => {
+          setVisible(false);
+          setTagList([{}]);
+          setOptions([]);
+        }}
+      >
+        <div>
+          {tagList.map((tag, index) => (
+            <Row gutter={12} key={tag.id || index} style={{ marginBottom: 12 }}>
+              <Col span={4}>
+                {index === 0 ? (
+                  <span>标签选择</span>
+                ) : (
+                  <Select
+                    value={tag.type}
+                    options={[
+                      { label: '并且', value: 'and' },
+                      { label: '或者', value: 'or' },
+                    ]}
+                    style={{ width: '100%' }}
+                    onSelect={(key: string) => {
+                      const indexItem = tagList[index];
+                      indexItem.type = key;
+                      tagList[index] = indexItem;
+                      setTagList([...tagList]);
+                    }}
+                  />
+                )}
+              </Col>
+              <Col span={16}>
+                <Row gutter={12}>
+                  <Col flex="120px">
+                    <Select
+                      value={tag.id}
+                      style={{ width: '120px' }}
+                      options={options}
+                      onSelect={(_: any, data: any) => {
+                        const newList = [...tagList];
+                        const indexType = newList[index].type;
+                        newList.splice(
+                          index,
+                          1,
+                          handleItem({ ...data, value: undefined, type: indexType }),
+                        );
+                        setTagList(newList);
+                      }}
+                      placeholder={'请选择标签'}
+                    />
+                  </Col>
+                  <Col flex={'auto'}>{getItemNode(tag)}</Col>
+                </Row>
+              </Col>
+              <Col span={4}>
+                <Space>
+                  <Button
+                    style={{ padding: '0 8px' }}
+                    onClick={() => {
+                      setTagList([...tagList, { type: 'and' }]);
+                    }}
+                  >
+                    <PlusOutlined />
+                  </Button>
+                  {tagList.length !== 1 && (
+                    <Button
+                      style={{ padding: '0 8px' }}
+                      onClick={() => {
+                        const newTagList = [...tagList];
+                        newTagList.splice(index, 1);
+                        setTagList(newTagList);
+                      }}
+                      danger
+                    >
+                      <DeleteOutlined />
+                    </Button>
+                  )}
+                </Space>
+              </Col>
+            </Row>
+          ))}
+        </div>
+      </Modal>
+      <Input
+        value={nameList.length ? nameList.toString() : undefined}
+        readOnly
+        style={{ width: 300 }}
+        onClick={() => {
+          setVisible(true);
+        }}
+        placeholder={'请选择标签'}
+      />
+    </>
+  );
+};

+ 41 - 11
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -1,12 +1,15 @@
 import { Col, Form, Row } from 'antd';
 import type { FormInstance } from 'antd';
-import VarItem from './VariableItem';
+import { BuiltIn, OrgList, UserList } from '@/pages/rule-engine/Scene/Save/action/VariableItems';
+import { InputFile } from '@/pages/rule-engine/Scene/Save/components';
 
 interface MessageContentProps {
   name: number;
   template?: any;
   form: FormInstance;
   notifyType: string;
+  triggerType: string;
+  configId: string;
 }
 
 const rowGutter = 12;
@@ -19,18 +22,45 @@ export default (props: MessageContentProps) => {
           {props.template.variableDefinitions ? (
             <Row gutter={rowGutter}>
               {props.template.variableDefinitions.map((item: any, index: number) => {
+                const type = item.expands?.businessType || item.type;
+                const _name = [props.name, 'notify', 'variables', item.id];
                 return (
                   <Col span={12} key={`${item.id}_${index}`}>
-                    <Form.Item
-                      name={[props.name, 'notify', 'variables', item.id]}
-                      label={item.name}
-                      initialValue={{
-                        source: 'fixed',
-                        value: undefined,
-                      }}
-                    >
-                      <VarItem form={props.form} data={item} notifyType={props.notifyType} />
-                    </Form.Item>
+                    {type === 'user' ? (
+                      <Form.Item
+                        name={_name}
+                        label={item.name}
+                        initialValue={{
+                          source: 'relation',
+                          value: undefined,
+                        }}
+                      >
+                        <UserList
+                          notifyType={props.notifyType}
+                          configId={props.configId}
+                          type={props.triggerType}
+                        />
+                      </Form.Item>
+                    ) : type === 'org' ? (
+                      <Form.Item name={_name} label={item.name}>
+                        <OrgList notifyType={props.notifyType} configId={props.configId} />
+                      </Form.Item>
+                    ) : type === 'file' ? (
+                      <Form.Item name={_name} label={item.name}>
+                        <InputFile />
+                      </Form.Item>
+                    ) : (
+                      <Form.Item
+                        name={_name}
+                        label={item.name}
+                        initialValue={{
+                          source: 'fixed',
+                          value: undefined,
+                        }}
+                      >
+                        <BuiltIn type={props.triggerType} data={item} />
+                      </Form.Item>
+                    )}
                   </Col>
                 );
               })}

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

@@ -29,3 +29,39 @@ export const queryProductList = (data?: any) =>
 
 export const queryDeviceSelector = () =>
   request(`${SystemConst.API_BASE}/scene/device-selectors`, { method: 'GET' });
+
+// 内置参数
+export const queryBuiltInParams = (data: any) =>
+  request(`${SystemConst.API_BASE}/scene/parse-variables`, { method: 'POST', data });
+
+// 平台用户
+export const queryPlatformUsers = () =>
+  request(`${SystemConst.API_BASE}/user/_query/no-paging`, {
+    method: 'POST',
+    data: { paging: false, sorts: [{ name: 'name', order: 'asc' }] },
+  });
+
+// 关系用户
+export const queryRelationUsers = () =>
+  request(`${SystemConst.API_BASE}/relation/_query/no-paging`, {
+    method: 'POST',
+    data: { paging: false, sorts: [{ name: 'name', order: 'asc' }] },
+  });
+
+// 钉钉用户
+export const queryDingTalkUsers = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/users`, { method: 'GET' });
+
+// 钉钉部门
+export const queryDingTalkDepartments = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments/tree`, {
+    method: 'GET',
+  });
+
+// 微信用户
+export const queryWechatUsers = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`, { method: 'GET' });
+
+// 微信部门
+export const queryWechatDepartments = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/departments`, { method: 'GET' });

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

@@ -0,0 +1,29 @@
+import { DatePicker } from 'antd';
+import type { DatePickerProps } from 'antd';
+import moment from 'moment';
+
+interface DatePickerFormat extends Omit<DatePickerProps, 'onChange'> {
+  onChange?: (dateString: string, date: moment.Moment | null) => void;
+}
+
+export default (props: DatePickerFormat) => {
+  const { value, onChange, ...extraProps } = props;
+
+  return (
+    <>
+      {
+        // @ts-ignore
+        <DatePicker
+          {...extraProps}
+          value={typeof value === 'string' ? moment(value) : value}
+          onChange={(date, dateString) => {
+            console.log(dateString);
+            if (onChange) {
+              onChange(dateString, date);
+            }
+          }}
+        />
+      }
+    </>
+  );
+};

+ 53 - 0
src/pages/rule-engine/Scene/Save/components/InputNumber.tsx

@@ -0,0 +1,53 @@
+import { InputNumber, Select } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface InputNumberProps {
+  value?: { time: number; unit: string };
+  onChange?: (value: { time: number; unit: string }) => void;
+}
+
+export default (props: InputNumberProps) => {
+  const [time, setTime] = useState(props.value?.time || 0);
+  const [unit, setUnit] = useState(props.value?.unit || 'second');
+
+  useEffect(() => {
+    setTime(props.value?.time || 0);
+    setUnit(props.value?.unit || 'second');
+  }, [props.value]);
+
+  const TimeTypeAfter = (
+    <Select
+      value={unit}
+      options={[
+        { label: '秒', value: 'second' },
+        { label: '分', value: 'minute' },
+        { label: '小时', value: 'hour' },
+      ]}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange({
+            time: time,
+            unit: key,
+          });
+        }
+      }}
+    />
+  );
+
+  return (
+    <InputNumber
+      addonAfter={TimeTypeAfter}
+      style={{ width: 150 }}
+      min={0}
+      max={9999}
+      onChange={(value) => {
+        if (props.onChange) {
+          props.onChange({
+            time: value,
+            unit,
+          });
+        }
+      }}
+    />
+  );
+};

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

@@ -13,7 +13,7 @@ interface InputUploadProps {
 export default (props: InputUploadProps) => {
   const { onChange } = props;
 
-  const [url, setUrl] = useState('');
+  const [url, setUrl] = useState(props.value);
   const [loading, setLoading] = useState<boolean>(false);
 
   useEffect(() => {

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

@@ -5,6 +5,7 @@ import './index.less';
 interface TriggerWayProps {
   value?: string;
   onChange?: (type: string) => void;
+  onSelect?: (type: string) => void;
 }
 
 export enum TriggerWayType {
@@ -23,6 +24,10 @@ export default (props: TriggerWayProps) => {
   const onSelect = (_type: string) => {
     setType(_type);
 
+    if (props.onSelect) {
+      props.onSelect(_type);
+    }
+
     if (props.onChange) {
       props.onChange(_type);
     }

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

@@ -3,3 +3,5 @@ export { default as TimingTrigger } from './TimingTrigger';
 export { default as TriggerWay } from './TriggerWay';
 export { default as ItemGroup } from './ItemGroup';
 export { default as InputFile } from './InputUpload';
+export { default as DatePickerFormat } from './DatePickerFormat';
+export { default as InputNumber } from './InputNumber';

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

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { Button, Card, Form, Input } from 'antd';
 import { useLocation } from 'umi';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
 import { PermissionButton } from '@/components';
 import ActionItems from './action/action';
 import { PlusOutlined } from '@ant-design/icons';
@@ -13,6 +13,7 @@ export default () => {
   const [form] = Form.useForm();
 
   const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
+  const [triggerType, setTriggerType] = useState('');
 
   const getDetail = async () => {
     // TODO 回显数据
@@ -34,13 +35,19 @@ export default () => {
   return (
     <PageContainer>
       <Card>
-        <Form form={form} colon={false} layout={'vertical'} preserve={false}>
+        <Form
+          form={form}
+          colon={false}
+          layout={'vertical'}
+          preserve={false}
+          onValuesChange={() => {}}
+        >
           <Form.Item name={'name'} label={'名称'}>
             <Input placeholder={'请输入名称'} />
           </Form.Item>
           <Form.Item label={'触发方式'}>
             <Form.Item name={['trigger', 'type']}>
-              <TriggerWay />
+              <TriggerWay onSelect={setTriggerType} />
             </Form.Item>
           </Form.Item>
           <Form.Item label={'执行动作'}>
@@ -52,8 +59,9 @@ export default () => {
                       key={key}
                       form={form}
                       restField={restField}
-                      onRemove={() => remove(name)}
                       name={name}
+                      triggerType={triggerType}
+                      onRemove={() => remove(name)}
                     />
                   ))}
                   <Form.Item>

+ 1 - 1
src/pages/system/Department/Assets/product/bind.tsx

@@ -35,7 +35,7 @@ const Bind = observer((props: Props) => {
       dataIndex: 'name',
       title: intl.formatMessage({
         id: 'pages.table.name',
-        defaultMessage: '名',
+        defaultMessage: '名',
       }),
       search: {
         transform: (value) => ({ name$LIKE: value }),