xieyonghong 3 lat temu
rodzic
commit
75a3e9a7bd
31 zmienionych plików z 1971 dodań i 53 usunięć
  1. BIN
      public/images/scene/action-alarm-icon.png
  2. BIN
      public/images/scene/device-custom.png
  3. BIN
      public/images/scene/device-variable.png
  4. BIN
      public/images/scene/invoke-function.png
  5. BIN
      public/images/scene/read-property.png
  6. BIN
      public/images/scene/write-property.png
  7. 55 0
      src/components/ProTableCard/CardItems/device.tsx
  8. 61 0
      src/components/ProTableCard/CardItems/product.tsx
  9. 8 4
      src/components/ProTableCard/index.tsx
  10. 62 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/ObjModel.tsx
  11. 249 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/TypeModel.tsx
  12. 102 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/WriteProperty.tsx
  13. 123 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/functionCall.tsx
  14. 145 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/index.tsx
  15. 55 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/device/TopCard.tsx
  16. 298 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/device/index.tsx
  17. 90 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/index.less
  18. 107 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/index.tsx
  19. 25 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/model.ts
  20. 243 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/product/index.tsx
  21. 30 0
      src/pages/rule-engine/Scene/Save/action/DeviceOutput/service.ts
  22. 52 10
      src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx
  23. 27 18
      src/pages/rule-engine/Scene/Save/action/ListItem/List.tsx
  24. 21 1
      src/pages/rule-engine/Scene/Save/action/ListItem/index.less
  25. 9 2
      src/pages/rule-engine/Scene/Save/action/Modal/add.tsx
  26. 176 0
      src/pages/rule-engine/Scene/Save/action/TriggerAlarm/index.tsx
  27. 3 0
      src/pages/rule-engine/Scene/Save/action/notify/NotifyConfig.tsx
  28. 3 0
      src/pages/rule-engine/Scene/Save/action/notify/NotifyTemplate.tsx
  29. 13 0
      src/pages/rule-engine/Scene/Save/action/service.ts
  30. 1 1
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.less
  31. 13 17
      src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.tsx

BIN
public/images/scene/action-alarm-icon.png


BIN
public/images/scene/device-custom.png


BIN
public/images/scene/device-variable.png


BIN
public/images/scene/invoke-function.png


BIN
public/images/scene/read-property.png


BIN
public/images/scene/write-property.png


+ 55 - 0
src/components/ProTableCard/CardItems/device.tsx

@@ -145,3 +145,58 @@ export default (props: DeviceCardProps) => {
     </TableCard>
   );
 };
+
+export const SceneDeviceCard = (props: DeviceCardProps) => {
+  const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+
+  return (
+    <TableCard
+      showTool={props.showTool}
+      showMask={false}
+      status={props.state?.value}
+      actions={props.actions}
+      statusText={props.state?.text}
+      statusNames={{
+        online: StatusColorEnum.processing,
+        offline: StatusColorEnum.error,
+        notActive: StatusColorEnum.warning,
+      }}
+      onClick={props.onClick}
+      className={props.className}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={imgUrl}
+            alt={''}
+            onError={() => {
+              setImgUrl(defaultImage);
+            }}
+          />
+        </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-flex'}>
+            <div className={'flex-auto'}>
+              <label>设备类型</label>
+              <Ellipsis title={props.deviceType ? props.deviceType.text : ''} />
+            </div>
+            <div className={'flex-auto'}>
+              <label>产品名称</label>
+              <Ellipsis title={props.productName} />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className={'checked-icon'}>
+        <div>
+          <CheckOutlined />
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 61 - 0
src/components/ProTableCard/CardItems/product.tsx

@@ -164,6 +164,67 @@ export const ExtraProductCard = (props: ProductCardProps) => {
   );
 };
 
+export const SceneProductCard = (props: ProductCardProps) => {
+  const intl = useIntl();
+  const [imgUrl, setImgUrl] = useState<string>(props.photoUrl || defaultImage);
+
+  return (
+    <TableCard
+      showMask={false}
+      status={props.state}
+      showTool={props.showTool}
+      actions={props.actions}
+      statusText={intl.formatMessage({
+        id: `pages.device.product.status.${props.state ? 'enabled' : 'disabled'}`,
+        defaultMessage: '正常',
+      })}
+      statusNames={{
+        0: StatusColorEnum.error,
+        1: StatusColorEnum.success,
+      }}
+      className={props.className}
+      onClick={props.onClick}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={imgUrl}
+            alt={''}
+            onError={() => {
+              setImgUrl(defaultImage);
+            }}
+          />
+        </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-items'} style={{ display: 'flex', gap: 12 }}>
+            {props.content}
+          </div>
+          <div className={'card-item-content-flex'}>
+            <div className={'flex-auto'}>
+              <label>设备类型</label>
+              <Ellipsis title={props?.deviceType?.text} />
+            </div>
+            <div className={'flex-auto'}>
+              <label>接入方式</label>
+              <Ellipsis title={props.accessName || '未接入'} />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className={'checked-icon'}>
+        <div>
+          <CheckOutlined />
+        </div>
+      </div>
+    </TableCard>
+  );
+};
+
 export default (props: ProductCardProps) => {
   const intl = useIntl();
   return (

+ 8 - 4
src/components/ProTableCard/index.tsx

@@ -30,6 +30,7 @@ interface ProTableCardProps<T> {
    */
   gridColumns?: [number, number, number];
   height?: 'none';
+  onlyCard?: boolean; //只展示card
 }
 
 const ProTableCard = <
@@ -39,7 +40,7 @@ const ProTableCard = <
 >(
   props: ProTableCardProps<T> & ProTableProps<T, U, ValueType>,
 ) => {
-  const { cardRender, toolBarRender, request, ...extraProps } = props;
+  const { cardRender, toolBarRender, request, onlyCard, ...extraProps } = props;
   const [model, setModel] = useState<ModelType>(ModelEnum.CARD);
   const [total, setTotal] = useState<number | undefined>(0);
   const [current, setCurrent] = useState(1); // 当前页
@@ -64,7 +65,7 @@ const ProTableCard = <
         if (!rowSelection || (rowSelection && !rowSelection.selectedRowKeys)) {
           return dom;
         }
-        const { selectedRowKeys, onChange } = rowSelection;
+        const { selectedRowKeys, onChange, type } = rowSelection;
 
         // @ts-ignore
         const id = dom.props.id;
@@ -82,13 +83,15 @@ const ProTableCard = <
               const isSelect = selectedRowKeys.includes(id);
 
               if (isSelect) {
-                const nowRowKeys = selectedRowKeys.filter((key: string) => key !== id);
+                const nowRowKeys =
+                  type === 'radio' ? [id] : selectedRowKeys.filter((key: string) => key !== id);
                 onChange(
                   nowRowKeys,
                   dataSource!.filter((item) => nowRowKeys.includes(item.id)),
                 );
               } else {
-                const nowRowKeys = [...selectedRowKeys, id];
+                // const nowRowKeys = [...selectedRowKeys, id];
+                const nowRowKeys = rowSelection.type === 'radio' ? [id] : [...selectedRowKeys, id];
                 onChange(
                   nowRowKeys,
                   dataSource!.filter((item) => nowRowKeys.includes(item.id)),
@@ -207,6 +210,7 @@ const ProTableCard = <
           pageSizeOptions: pageSizeOptions,
         }}
         toolBarRender={(action, row) => {
+          if (onlyCard) return [];
           const oldBar = toolBarRender ? toolBarRender(action, row) : [];
           return [
             ...oldBar,

+ 62 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/ObjModel.tsx

@@ -0,0 +1,62 @@
+import { Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import MonacoEditor from 'react-monaco-editor';
+
+interface Props {
+  value: any;
+  close: () => void;
+  ok: (data: any) => void;
+}
+
+export default (props: Props) => {
+  const monacoRef = useRef<any>();
+
+  const [value, setValue] = useState<any>(props.value);
+  const [loading, setLoading] = useState(false);
+
+  const editorDidMountHandle = (editor: any) => {
+    monacoRef.current = editor;
+    editor.getAction('editor.action.formatDocument').run();
+    editor.onDidContentSizeChange?.(() => {
+      editor.getAction('editor.action.formatDocument').run();
+    });
+  };
+
+  useEffect(() => {
+    setValue(props?.value || '');
+  }, [props.value]);
+
+  return (
+    <Modal
+      visible
+      title="编辑"
+      width={700}
+      onCancel={() => props.close()}
+      onOk={() => {
+        props.ok(value);
+      }}
+    >
+      <div
+        ref={() => {
+          setTimeout(() => {
+            setLoading(true);
+          }, 100);
+        }}
+      >
+        {loading && (
+          <MonacoEditor
+            width={'100%'}
+            height={400}
+            theme="vs-dark"
+            language={'json'}
+            value={value}
+            onChange={(newValue) => {
+              setValue(newValue);
+            }}
+            editorDidMount={editorDidMountHandle}
+          />
+        )}
+      </div>
+    </Modal>
+  );
+};

+ 249 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/TypeModel.tsx

@@ -0,0 +1,249 @@
+import ParamsSelect, { ItemProps } from '@/pages/rule-engine/Scene/Save/components/ParamsSelect';
+import { useEffect, useState } from 'react';
+import { DataNode } from 'antd/es/tree';
+import { Input, InputNumber, Select, Tree } from 'antd';
+import MTimePicker from '../../../components/ParamsSelect/components/MTimePicker';
+import moment from 'moment';
+import { EditOutlined, EnvironmentOutlined } from '@ant-design/icons';
+import AMap from '@/components/GeoPoint/AMap';
+import ObjModel from './ObjModel';
+
+interface Props {
+  value: any;
+  type: string;
+  onChange: (data: any, source?: any) => void;
+  record?: any; //枚举值使用
+}
+
+export default (props: Props) => {
+  const [value, setValue] = useState<any>(props.value || '');
+  const [visible, setVisible] = useState<boolean>(false);
+  const [objVisiable, setObjVisable] = useState<boolean>(false);
+  const [source, setSource] = useState<string>('');
+  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' }],
+        },
+      ],
+    },
+  ];
+
+  // useEffect(() => {
+  //   setValue(props.value);
+  // }, [props.value]);
+
+  const onChange = (params: any) => {
+    setValue(params);
+    if (props.onChange) {
+      props.onChange(params);
+    }
+  };
+
+  useEffect(() => {
+    props.onChange(value, source);
+  }, [source, value]);
+
+  const renderNode = (type: string) => {
+    switch (type) {
+      case 'int':
+      case 'long':
+      case 'float':
+      case 'double':
+        return (
+          <InputNumber
+            value={value}
+            onChange={(e: any) => {
+              onChange(e);
+            }}
+            style={{ width: '100%' }}
+            placeholder={'请输入'}
+          />
+        );
+      case 'enum':
+        return (
+          <Select
+            value={value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={props.record.options || []}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择'}
+            mode="multiple"
+            onChange={(e) => {
+              onChange(e);
+            }}
+          />
+        );
+      case 'boolean':
+        return (
+          <Select
+            value={value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={[
+              { label: 'true', value: true },
+              { label: 'false', value: false },
+            ]}
+            placeholder={'请选择'}
+            onChange={(e) => {
+              onChange(e);
+            }}
+          />
+        );
+      case 'geoPoint':
+        return (
+          <Input
+            value={value}
+            style={{ width: '100%', textAlign: 'left' }}
+            addonAfter={
+              <EnvironmentOutlined
+                onClick={() => {
+                  setVisible(true);
+                }}
+              />
+            }
+            placeholder={'请选择'}
+            onChange={(e) => {
+              onChange(e);
+            }}
+          />
+        );
+      case 'object':
+        return (
+          <Input
+            value={value}
+            style={{ width: '100%', textAlign: 'left' }}
+            addonAfter={
+              <EditOutlined
+                onClick={() => {
+                  setObjVisable(true);
+                }}
+              />
+            }
+            placeholder={'请选择'}
+            onChange={(e) => {
+              onChange(e);
+            }}
+          />
+        );
+      case 'date':
+        return (
+          <MTimePicker
+            value={moment(value, 'HH:mm:ss')}
+            onChange={(_: any, timeString: string) => {
+              setValue(timeString);
+              if (props.onChange) {
+                props.onChange(timeString);
+              }
+            }}
+          />
+        );
+      default:
+        return (
+          <Input
+            value={value}
+            placeholder={'请输入'}
+            onChange={(e) => {
+              setValue(e.target.value);
+              if (props.onChange) {
+                props.onChange(e.target.value);
+              }
+            }}
+          />
+        );
+    }
+  };
+
+  const itemList: ItemProps[] = [
+    {
+      label: `手动输入`,
+      key: 'manual',
+      content: renderNode(props.type),
+    },
+    {
+      label: `内置参数`,
+      key: 'upper',
+      content: (
+        <Tree
+          treeData={treeData}
+          height={300}
+          defaultExpandAll
+          onSelect={(selectedKeys) => {
+            setValue(selectedKeys[0]);
+            if (props.onChange) {
+              props.onChange(selectedKeys[0]);
+            }
+          }}
+        />
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <ParamsSelect
+        style={{ width: '100%', height: '100%' }}
+        inputProps={{
+          placeholder: '请选择',
+        }}
+        tabKey={'manual'}
+        itemList={itemList}
+        value={value}
+        onChange={(val: any, tabKey: any) => {
+          setValue(val);
+          setSource(tabKey);
+        }}
+        type={props.type}
+      />
+      {visible && (
+        <AMap
+          value={value}
+          close={() => {
+            setVisible(false);
+          }}
+          ok={(param) => {
+            if (props.onChange) {
+              props.onChange(param);
+            }
+            setValue(param);
+            setVisible(false);
+          }}
+        />
+      )}
+
+      {objVisiable && (
+        <ObjModel
+          value={value}
+          close={() => {
+            setObjVisable(false);
+          }}
+          ok={(param) => {
+            if (props.onChange) {
+              props.onChange(param);
+            }
+            setValue(param);
+            setObjVisable(false);
+          }}
+        />
+      )}
+    </div>
+  );
+};

+ 102 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/WriteProperty.tsx

@@ -0,0 +1,102 @@
+import { Col, Row, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import TypeModel from './TypeModel';
+// import { useEffect, useState } from "react";
+
+interface Props {
+  properties: any[];
+  value?: any;
+  id?: string;
+  onChange?: (value?: any) => void;
+  propertiesChange?: (value?: string) => void;
+}
+
+// const item = {
+//   a1: {
+//     "value": 10,
+//     "source": "fixed",
+//   }
+// }
+
+export default (props: Props) => {
+  const [propertiesId, setPropertiesId] = useState<string | undefined>(undefined);
+  const [propertiesValue, setPropertiesValue] = useState(undefined);
+  const [propertiesType, setPropertiesType] = useState('');
+  const [source, setSource] = useState<string>('');
+
+  useEffect(() => {
+    console.log(props.value);
+    if (props.value) {
+      if (props.properties && props.properties.length) {
+        if (0 in props.value) {
+          setPropertiesValue(props.value[0]);
+        } else if ('undefined' in props.value) {
+          // setPropertiesKey(undefined);
+          setPropertiesValue(undefined);
+        } else {
+          Object.keys(props.value).forEach((key: string) => {
+            // setPropertiesKey(key);
+            setPropertiesValue(props.value[key].value);
+            const propertiesItem = props.properties.find((item: any) => item.id === key);
+            if (propertiesItem) {
+              setPropertiesType(propertiesItem.valueType.type);
+            }
+          });
+        }
+      }
+    } else {
+      // setPropertiesKey(undefined);
+      setPropertiesValue(undefined);
+    }
+  }, [props.value, props.properties]);
+
+  useEffect(() => {
+    // console.log(propertiesValue)
+    if (props.onChange && propertiesValue) {
+      const obj = {
+        [propertiesId || 0]: {
+          value: propertiesValue,
+          source: source,
+        },
+      };
+      props.onChange(obj);
+    }
+  }, [propertiesValue, source]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={12}>
+        <Select
+          id={props.id}
+          value={props.value ? props.value[0] : undefined}
+          options={props.properties.filter((item) => {
+            if (item.expands && item.expands.type) {
+              return item.expands.type.includes('write');
+            }
+            return false;
+          })}
+          fieldNames={{ label: 'name', value: 'id' }}
+          style={{ width: '100%' }}
+          placeholder={'请选择属性'}
+          onChange={(e, option) => {
+            setPropertiesId(e);
+            setPropertiesType(option.valueType.type);
+            console.log(option);
+          }}
+        ></Select>
+      </Col>
+      {propertiesId && (
+        <Col span={12}>
+          <TypeModel
+            value={propertiesValue}
+            type={propertiesType}
+            onChange={(value, sources) => {
+              setPropertiesValue(value);
+              setSource(sources);
+            }}
+          />
+        </Col>
+      )}
+    </Row>
+  );
+};

+ 123 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/functionCall.tsx

@@ -0,0 +1,123 @@
+import type { ProColumns } from '@jetlinks/pro-table';
+import { EditableProTable } from '@jetlinks/pro-table';
+import React, { useEffect, useRef, useState } from 'react';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import ProForm from '@ant-design/pro-form';
+import TypeModel from './TypeModel';
+
+type FunctionTableDataType = {
+  id: string;
+  name: string;
+  type: string;
+};
+
+interface FunctionCallProps {
+  functionData: any[];
+  value?: any;
+  onChange?: (data: any) => void;
+  name?: string;
+  productId?: string;
+}
+
+export default (props: FunctionCallProps) => {
+  const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
+  const formRef = useRef<ProFormInstance<any>>();
+
+  useEffect(() => {
+    if (props.functionData && props.functionData.length) {
+      setEditableRowKeys(props.functionData.map((d) => d.id));
+      if (props.value) {
+        // console.log(props.functionData, 11111111111);
+        const tableData = 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;
+        });
+        formRef.current?.setFieldsValue({
+          table: tableData,
+        });
+      } else {
+        formRef.current?.setFieldsValue({
+          table: props.functionData,
+        });
+      }
+    } else {
+      formRef.current?.setFieldsValue({
+        table: [],
+      });
+    }
+  }, [props.value, props.functionData]);
+
+  useEffect(() => {
+    if (props.productId && props.onChange) {
+      props.onChange([]);
+    }
+  }, [props.productId]);
+
+  const getItemNode = (record: any) => {
+    const type = record.type;
+    return <TypeModel value={record.value} type={type} record={record} />;
+  };
+
+  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) => {
+        return getItemNode(row.record);
+      },
+    },
+  ];
+
+  return (
+    <ProForm<{ table: FunctionTableDataType[] }>
+      formRef={formRef}
+      name={props.name || 'proForm'}
+      submitter={false}
+      onValuesChange={() => {
+        const values = formRef.current?.getFieldsValue();
+        console.log(values, 'values');
+        if (props.onChange) {
+          props.onChange(
+            values.table.map((item: any) => ({
+              name: item.id,
+              value: item.value,
+            })),
+          );
+        }
+      }}
+    >
+      <EditableProTable
+        rowKey="id"
+        name={'table'}
+        recordCreatorProps={false}
+        columns={columns}
+        size={'small'}
+        editable={{
+          type: 'multiple',
+          editableKeys,
+          onChange: setEditableRowKeys,
+        }}
+      />
+    </ProForm>
+  );
+};

+ 145 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/actions/index.tsx

@@ -0,0 +1,145 @@
+import { observer } from '@formily/reactive-react';
+import { Form, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import ReadProperty from '../../device/readProperty';
+import TopCard from '../device/TopCard';
+import DeviceModel from '../model';
+import FunctionCall from './functionCall';
+import WriteProperty from './WriteProperty';
+
+interface Props {
+  get: (data: any) => void;
+}
+
+export default observer((props: Props) => {
+  const [form] = Form.useForm();
+  const [deviceMessageType, setDeviceMessageType] = useState('WRITE_PROPERTY');
+  const [properties, setProperties] = useState([]); // 物模型-属性
+  const [propertiesId, setPropertiesId] = useState<string | undefined>(''); // 物模型-属性ID,用于串行
+  const [functionList, setFunctionList] = useState<any>([]); // 物模型-功能
+  const [functionId, setFunctionId] = useState('');
+  const [functions, setFunctions] = useState([]);
+
+  const TypeList = [
+    {
+      label: '功能调用',
+      value: 'INVOKE_FUNCTION',
+      image: require('/public/images/scene/invoke-function.png'),
+      tip: '-',
+    },
+    {
+      label: '读取属性',
+      value: 'READ_PROPERTY',
+      image: require('/public/images/scene/read-property.png'),
+      tip: '-',
+    },
+    {
+      label: '设置属性',
+      value: 'WRITE_PROPERTY',
+      image: require('/public/images/scene/write-property.png'),
+      tip: '-',
+    },
+  ];
+
+  useEffect(() => {
+    if (DeviceModel.productDetail) {
+      const metadata = JSON.parse(DeviceModel.productDetail?.metadata || '{}');
+      setProperties(metadata.properties);
+      setFunctions(metadata.functions);
+    }
+  }, [DeviceModel.productDetail, functionId]);
+
+  useEffect(() => {
+    if (functionId && functions.length !== 0) {
+      const functionItem: any = functions.find((item: any) => item.id === functionId);
+      if (functionItem) {
+        const item = functionItem.valueType
+          ? functionItem.valueType.properties
+          : functionItem.inputs;
+        const array = [];
+        for (const datum of item) {
+          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,
+          });
+        }
+        setFunctionList(array);
+        console.log(propertiesId, 'array');
+      }
+    }
+  }, [functions, functionId]);
+
+  useEffect(() => {
+    props.get(form);
+  }, [form]);
+
+  return (
+    <div>
+      <Form form={form} layout={'vertical'}>
+        <Form.Item name="messageType" label="动作类型" required initialValue="WRITE_PROPERTY">
+          <TopCard
+            typeList={TypeList}
+            onChange={(value: string) => {
+              setDeviceMessageType(value);
+            }}
+          />
+        </Form.Item>
+        {deviceMessageType === 'INVOKE_FUNCTION' && (
+          <>
+            <Form.Item
+              name={['device', 'message', 'functionId']}
+              label="功能调用"
+              rules={[{ required: true, message: '请选择功能' }]}
+            >
+              <Select
+                showSearch
+                allowClear
+                options={functions}
+                fieldNames={{ label: 'name', value: 'id' }}
+                style={{ width: '100%' }}
+                placeholder={'请选择功能'}
+                filterOption={(input: string, option: any) =>
+                  option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }
+                onChange={(value) => {
+                  setFunctionId(value);
+                }}
+              />
+            </Form.Item>
+            {functionId && (
+              <Form.Item
+                name={['device', 'message', 'inputs']}
+                rules={[{ required: true, message: '请输入功能值' }]}
+              >
+                <FunctionCall functionData={functionList} productId={DeviceModel.productId[0]} />
+              </Form.Item>
+            )}
+          </>
+        )}
+        {deviceMessageType === 'READ_PROPERTY' && (
+          <Form.Item
+            name={['device', 'message', 'properties']}
+            label="读取属性"
+            rules={[{ required: true, message: '请选择读取属性' }]}
+          >
+            <ReadProperty properties={properties} propertiesChange={setPropertiesId} />
+          </Form.Item>
+        )}
+        {deviceMessageType === 'WRITE_PROPERTY' && (
+          <Form.Item
+            name={['device', 'message', 'properties']}
+            label="设置属性"
+            rules={[{ required: true, message: '请选择属性' }]}
+          >
+            <WriteProperty properties={properties} />
+          </Form.Item>
+        )}
+        {/* {deviceMessageType === 'WRITE_PROPERTY' && <WriteProperty properties={properties} />} */}
+      </Form>
+    </div>
+  );
+});

+ 55 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/device/TopCard.tsx

@@ -0,0 +1,55 @@
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import '../index.less';
+
+interface Props {
+  typeList: any[];
+  value?: string;
+  className?: string;
+  onChange?: (type: string) => void;
+  onSelect?: (type: string) => void;
+  disabled?: boolean;
+}
+
+const TopCard = (props: Props) => {
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  const onSelect = (_type: string) => {
+    if (!props.disabled) {
+      setType(_type);
+
+      if (props.onChange) {
+        props.onChange(_type);
+      }
+    }
+  };
+
+  return (
+    <div className={classNames('trigger-way-warp', props.className, { disabled: props.disabled })}>
+      {props.typeList.map((item) => (
+        <div
+          key={item.value}
+          className={classNames('trigger-way-item', {
+            active: type === item.value,
+          })}
+          onClick={() => {
+            onSelect(item.value);
+          }}
+        >
+          <div className={'way-item-title'}>
+            <p>{item.label}</p>
+            <span>{item.tip}</span>
+          </div>
+          <div className={'way-item-image'}>
+            <img width={48} src={item.image} />
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+};
+export default TopCard;

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

@@ -0,0 +1,298 @@
+import { ProTableCard } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import { useRef, useState } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Instance/index';
+import { SceneDeviceCard } from '@/components/ProTableCard/CardItems/device';
+import { isNoCommunity } from '@/utils/util';
+import { useIntl } from 'umi';
+import { service as categoryService } from '@/pages/device/Category';
+import { service as deptService } from '@/pages/system/Department';
+import DeviceModel from '../model';
+import { observer } from '@formily/reactive-react';
+import { Form } from 'antd';
+import '../index.less';
+import TopCard from './TopCard';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParam, setSearchParam] = useState({});
+  const [form] = Form.useForm();
+
+  const TypeList = [
+    {
+      label: '自定义',
+      value: 'custom',
+      image: require('/public/images/scene/device-custom.png'),
+      tip: '自定义选择当前产品下的任意设备',
+    },
+    {
+      label: '按变量',
+      value: 'variable',
+      image: require('/public/images/scene/device-variable.png'),
+      tip: '选择设备ID为上游变量值的设备',
+    },
+  ];
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 300,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.deviceName',
+        defaultMessage: '设备名称',
+      }),
+      dataIndex: 'name',
+      ellipsis: true,
+      width: 200,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.productName',
+        defaultMessage: '产品名称',
+      }),
+      dataIndex: 'productId',
+      width: 200,
+      ellipsis: true,
+      valueType: 'select',
+      request: async () => {
+        const res = await service.getProductList();
+        if (res.status === 200) {
+          return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+        }
+        return [];
+      },
+      render: (_, row) => row.productName,
+      filterMultiple: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '禁用',
+          }),
+          status: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+    {
+      dataIndex: 'classifiedId',
+      title: '产品分类',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'id',
+        },
+      },
+      request: () =>
+        categoryService
+          .queryTree({
+            paging: false,
+          })
+          .then((resp: any) => resp.result),
+    },
+    {
+      title: '网关类型',
+      dataIndex: 'accessProvider',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.getProviders().then((resp: any) => {
+          return (resp?.result || [])
+            .filter((i: any) =>
+              !isNoCommunity
+                ? [
+                    'mqtt-server-gateway',
+                    'http-server-gateway',
+                    'mqtt-client-gateway',
+                    'tcp-server-gateway',
+                  ].includes(i.id)
+                : i,
+            )
+            .map((item: any) => ({
+              label: item.name,
+              value: `accessProvider is ${item.id}`,
+            }));
+        }),
+    },
+    {
+      dataIndex: 'productId$product-info',
+      title: '接入方式',
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.queryGatewayList().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: `accessId is ${item.id}`,
+          })),
+        ),
+    },
+    {
+      dataIndex: 'deviceType',
+      title: '设备类型',
+      valueType: 'select',
+      hideInTable: true,
+      valueEnum: {
+        device: {
+          text: '直连设备',
+          status: 'device',
+        },
+        childrenDevice: {
+          text: '网关子设备',
+          status: 'childrenDevice',
+        },
+        gateway: {
+          text: '网关设备',
+          status: 'gateway',
+        },
+      },
+    },
+    {
+      dataIndex: 'id$dim-assets',
+      title: '所属组织',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'value',
+        },
+      },
+      request: () =>
+        deptService
+          .queryOrgThree({
+            paging: false,
+          })
+          .then((resp) => {
+            const formatValue = (list: any[]) => {
+              const _list: any[] = [];
+              list.forEach((item) => {
+                if (item.children) {
+                  item.children = formatValue(item.children);
+                }
+                _list.push({
+                  ...item,
+                  value: JSON.stringify({
+                    assetType: 'device',
+                    targets: [
+                      {
+                        type: 'org',
+                        id: item.id,
+                      },
+                    ],
+                  }),
+                });
+              });
+              return _list;
+            };
+            return formatValue(resp.result);
+          }),
+    },
+  ];
+  return (
+    <div>
+      <div className="device-title">
+        <ExclamationCircleOutlined className="device-title-icon" />
+        <span>自定义选择当前产品下的任意设备</span>
+      </div>
+      <Form form={form} layout={'vertical'}>
+        <Form.Item name="type" label="选择方式" required>
+          <TopCard typeList={TypeList} />
+        </Form.Item>
+      </Form>
+      <SearchComponent
+        field={columns}
+        model={'simple'}
+        enableSave={false}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="device"
+        defaultParam={[
+          {
+            terms: [
+              {
+                column: 'productId',
+                value: DeviceModel.productId[0],
+              },
+            ],
+          },
+        ]}
+      />
+      <div>
+        <ProTableCard<DeviceInstance>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          onlyCard={true}
+          tableAlertRender={false}
+          rowSelection={{
+            type: 'radio',
+            selectedRowKeys: DeviceModel.deviceId,
+            onChange: (selectedRowKeys, selectedRows) => {
+              DeviceModel.deviceId = selectedRows.map((item) => item.id);
+            },
+          }}
+          request={(params) =>
+            service.query({
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            })
+          }
+          params={searchParam}
+          cardRender={(record) => (
+            <SceneDeviceCard showBindBtn={false} showTool={false} {...record} />
+          )}
+          height={'none'}
+        />
+      </div>
+    </div>
+  );
+});

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

@@ -0,0 +1,90 @@
+@import '../../../../../../../node_modules/antd/es/style/themes/default.less';
+
+.steps-steps {
+  width: 100%;
+  margin-bottom: 17px;
+  padding-bottom: 17px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.steps-content {
+  width: 100%;
+}
+
+.trigger-way-warp {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  width: 100%;
+
+  .trigger-way-item {
+    display: flex;
+    justify-content: space-between;
+    width: 237px;
+    //width: 100%;
+    padding: 16px;
+    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;
+      }
+    }
+
+    .way-item-image {
+      margin: 0 !important;
+    }
+
+    &:hover {
+      color: @primary-color-hover;
+      opacity: 0.8;
+    }
+
+    &.active {
+      border-color: @primary-color-active;
+      opacity: 1;
+    }
+  }
+
+  &.disabled {
+    .trigger-way-item {
+      cursor: not-allowed;
+
+      &:hover {
+        color: initial;
+        opacity: 0.6;
+      }
+
+      &.active {
+        opacity: 1;
+      }
+    }
+  }
+}
+
+.device-title {
+  top: 5px;
+  left: 0;
+  width: 760px;
+  height: 38px;
+  padding: 8px 16px 8px 16px;
+  background: rgba(250, 178, 71, 0.1);
+  border: 1px solid rgba(250, 178, 71, 0.4);
+  border-radius: 4px;
+
+  .device-title-icon {
+    margin-right: 5px;
+    color: #fab247;
+  }
+}

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

@@ -0,0 +1,107 @@
+import { Modal, Button, Steps } from 'antd';
+import { useRef, useState } from 'react';
+import { observer } from '@formily/react';
+import Device from './device';
+import Product from './product';
+import Action from './actions';
+import Service from './service';
+// import { model } from '@formily/reactive';
+import './index.less';
+import DeviceModel from './model';
+import { onlyMessage } from '@/utils/util';
+
+export const service = new Service<any>('');
+
+export default observer(() => {
+  const [open, setOpen] = useState<boolean>(true);
+  // const [data, setData] = useState<any>({})
+  const formRef = useRef<any>();
+
+  DeviceModel.steps = [
+    {
+      key: 'product',
+      title: '选择产品',
+      content: <Product />,
+    },
+    {
+      key: 'device',
+      title: '选择设备',
+      content: <Device />,
+    },
+    {
+      key: 'action',
+      title: '执行动作',
+      content: (
+        <Action
+          get={(item: any) => {
+            formRef.current = item;
+          }}
+        />
+      ),
+    },
+  ];
+
+  const next = () => {
+    if (
+      (DeviceModel.current === 0 && DeviceModel.productId.length !== 0) ||
+      (DeviceModel.current === 1 && DeviceModel.deviceId.length !== 0)
+    ) {
+      return (DeviceModel.current += 1);
+    } else {
+      return DeviceModel.current === 0
+        ? onlyMessage('请选择产品', 'error')
+        : onlyMessage('请选择设备', 'error');
+    }
+  };
+
+  const prev = () => {
+    DeviceModel.current -= 1;
+  };
+
+  const save = async () => {
+    const value = await formRef.current?.validateFields();
+    console.log(value);
+  };
+
+  return (
+    <Modal
+      title={'执行动作'}
+      open={open}
+      width={800}
+      onCancel={() => {
+        setOpen(false);
+      }}
+      maskClosable={false}
+      footer={
+        <div className="steps-action">
+          {DeviceModel.current === 0 && <Button onClick={() => {}}>取消</Button>}
+          {DeviceModel.current > 0 && (
+            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+              上一步
+            </Button>
+          )}
+          {DeviceModel.current < DeviceModel.steps.length - 1 && (
+            <Button type="primary" onClick={() => next()}>
+              下一步
+            </Button>
+          )}
+          {DeviceModel.current === DeviceModel.steps.length - 1 && (
+            <Button
+              type="primary"
+              onClick={async () => {
+                save();
+              }}
+            >
+              确定
+            </Button>
+          )}
+        </div>
+      }
+    >
+      <div className="steps-steps">
+        <Steps current={DeviceModel.current} items={DeviceModel.steps} />
+      </div>
+      <div className="steps-content">{DeviceModel.steps[DeviceModel.current]?.content}</div>
+    </Modal>
+  );
+});

+ 25 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/model.ts

@@ -0,0 +1,25 @@
+// 模型
+import { ProductItem } from '@/pages/device/Product/typings';
+import { model } from '@formily/reactive';
+
+type ModelType = {
+  steps: {
+    key: string;
+    title: string;
+    content: React.ReactNode;
+  }[];
+  current: number;
+  productId: string[];
+  deviceId: string[];
+  productDetail: ProductItem | any;
+};
+
+const DeviceModel = model<ModelType>({
+  steps: [],
+  current: 0,
+  productId: [],
+  deviceId: [],
+  productDetail: {},
+});
+
+export default DeviceModel;

+ 243 - 0
src/pages/rule-engine/Scene/Save/action/DeviceOutput/product/index.tsx

@@ -0,0 +1,243 @@
+import { ProTableCard } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import { ProductItem } from '@/pages/device/Product/typings';
+import { useRef, useState } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Product/index';
+import { SceneProductCard } from '@/components/ProTableCard/CardItems/product';
+import { isNoCommunity } from '@/utils/util';
+import { useIntl } from 'umi';
+import { service as categoryService } from '@/pages/device/Category';
+import { service as deptService } from '@/pages/system/Department';
+import DeviceModel from '../model';
+import { observer } from '@formily/reactive-react';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParam, setSearchParam] = useState({});
+
+  const columns: ProColumns<ProductItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 300,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      width: 200,
+      ellipsis: true,
+    },
+    {
+      title: '网关类型',
+      dataIndex: 'accessProvider',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.getProviders().then((resp: any) => {
+          if (isNoCommunity) {
+            return (resp?.result || []).map((item: any) => ({
+              label: item.name,
+              value: item.id,
+            }));
+          } else {
+            return (resp?.result || [])
+              .filter((i: any) =>
+                [
+                  'mqtt-server-gateway',
+                  'http-server-gateway',
+                  'mqtt-client-gateway',
+                  'tcp-server-gateway',
+                ].includes(i.id),
+              )
+              .map((item: any) => ({
+                label: item.name,
+                value: item.id,
+              }));
+          }
+        }),
+    },
+    {
+      title: '接入方式',
+      dataIndex: 'accessName',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      request: () =>
+        service.queryGatewayList().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: item.name,
+          })),
+        ),
+    },
+    {
+      title: '设备类型',
+      dataIndex: 'deviceType',
+      valueType: 'select',
+      valueEnum: {
+        device: {
+          text: '直连设备',
+          status: 'device',
+        },
+        childrenDevice: {
+          text: '网关子设备',
+          status: 'childrenDevice',
+        },
+        gateway: {
+          text: '网关设备',
+          status: 'gateway',
+        },
+      },
+      width: 150,
+      render: (_, row) => <>{row.deviceType ? row.deviceType.text : undefined}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      width: '90px',
+      valueEnum: {
+        0: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.disabled',
+            defaultMessage: '禁用',
+          }),
+          status: 0,
+        },
+        1: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.enabled',
+            defaultMessage: '正常',
+          }),
+          status: 1,
+        },
+      },
+    },
+    {
+      dataIndex: 'describe',
+      title: intl.formatMessage({
+        id: 'pages.system.description',
+        defaultMessage: '说明',
+      }),
+      ellipsis: true,
+      width: 300,
+      // hideInSearch: true,
+    },
+    {
+      dataIndex: 'classifiedId',
+      title: '分类',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'id',
+        },
+      },
+      request: () =>
+        categoryService
+          .queryTree({
+            paging: false,
+          })
+          .then((resp: any) => resp.result),
+    },
+    {
+      dataIndex: 'id$dim-assets',
+      title: '所属组织',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'value',
+        },
+      },
+      request: () =>
+        deptService
+          .queryOrgThree({
+            paging: false,
+          })
+          .then((resp) => {
+            const formatValue = (list: any[]) => {
+              const _list: any[] = [];
+              list.forEach((item) => {
+                if (item.children) {
+                  item.children = formatValue(item.children);
+                }
+                _list.push({
+                  ...item,
+                  value: JSON.stringify({
+                    assetType: 'product',
+                    targets: [
+                      {
+                        type: 'org',
+                        id: item.id,
+                      },
+                    ],
+                  }),
+                });
+              });
+              return _list;
+            };
+            return formatValue(resp.result);
+          }),
+    },
+  ];
+  return (
+    <div>
+      <SearchComponent
+        field={columns}
+        model={'simple'}
+        enableSave={false}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-assets-product"
+      />
+      <div
+        style={{
+          height: 'calc(100vh - 440px)',
+          overflowY: 'auto',
+        }}
+      >
+        <ProTableCard<ProductItem>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          onlyCard={true}
+          tableAlertRender={false}
+          rowSelection={{
+            type: 'radio',
+            selectedRowKeys: DeviceModel.productId,
+            onChange: (selectedRowKeys, selectedRows) => {
+              // console.log(selectedRowKeys,selectedRows)
+              DeviceModel.productId = selectedRows.map((item) => item.id);
+              DeviceModel.productDetail = selectedRows?.[0];
+            },
+          }}
+          request={(params) =>
+            service.query({
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            })
+          }
+          params={searchParam}
+          cardRender={(record) => (
+            <SceneProductCard showBindBtn={false} showTool={false} {...record} />
+          )}
+          height={'none'}
+        />
+      </div>
+    </div>
+  );
+});

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

@@ -0,0 +1,30 @@
+import BaseService from '@/utils/BaseService';
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+class Service<T> extends BaseService<T> {
+  // 设备
+  queryDeviceList = (params: any) => {
+    return request<T>(`${SystemConst.API_BASE}/device/instance/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  };
+
+  // 查询产品列表
+  getProductList = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
+
+  // 产品
+  queryProductList = (params: any) => {
+    return request<T>(`${SystemConst.API_BASE}/device-product/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  };
+}
+
+export default Service;

+ 52 - 10
src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx

@@ -3,7 +3,8 @@ 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';
-
+import './index.less';
+import TriggerAlarm from '../TriggerAlarm';
 export enum ParallelEnum {
   'parallel' = 'parallel',
   'serial' = 'serial',
@@ -16,22 +17,52 @@ interface ItemProps {
   type: ParallelType;
 }
 
+const iconMap = new Map();
+iconMap.set('alarm', require('/public/images/scene/action-alarm-icon.png'));
+
 export default (props: ItemProps) => {
   const [visible, setVisible] = useState<boolean>(false);
+  const [triggerVisible, setTriggerVisible] = useState<boolean>(false);
+
+  const contentRender = () => {
+    if (props?.data?.alarm?.mode === 'trigger') {
+      return (
+        <div>
+          满足条件后将触发关联
+          <a
+            onClick={() => {
+              setTriggerVisible(true);
+            }}
+          >
+            关联此场景的告警
+          </a>
+        </div>
+      );
+    } else if (props?.data?.alarm?.mode === 'relieve') {
+      return (
+        <div>
+          满足条件后将解除关联
+          <a
+            onClick={() => {
+              setTriggerVisible(true);
+            }}
+          >
+            关联此场景的告警
+          </a>
+        </div>
+      );
+    }
+    return '';
+  };
+
   return (
     <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 className="item-options-type">
+            <img style={{ width: 48 }} src={iconMap.get(props?.data.executor)} />
           </div>
+          <div className={'item-options-content'}>{contentRender()}</div>
         </div>
         <div className="item-number">{props.name + 1}</div>
         <div
@@ -60,6 +91,17 @@ export default (props: ItemProps) => {
           close={() => {
             setVisible(false);
           }}
+          save={(data: ActionsType) => {
+            console.log(data);
+            setVisible(false);
+          }}
+        />
+      )}
+      {triggerVisible && (
+        <TriggerAlarm
+          close={() => {
+            setTriggerVisible(false);
+          }}
         />
       )}
     </div>

+ 27 - 18
src/pages/rule-engine/Scene/Save/action/ListItem/List.tsx

@@ -1,10 +1,11 @@
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
 import Modal from '../Modal/add';
 import './index.less';
 import type { ActionsType } from '@/pages/rule-engine/Scene/typings';
 import Item from './Item';
 import type { ParallelType } from './Item';
+import { FormModel } from '../..';
 interface ListProps {
   type: ParallelType;
   actions: ActionsType[];
@@ -12,30 +13,20 @@ interface ListProps {
 
 export default (props: ListProps) => {
   const [visible, setVisible] = useState<boolean>(false);
+  const [actions, setActions] = useState<ActionsType[]>(props.actions);
+
+  useEffect(() => {
+    setActions(props.actions);
+  }, [props.actions]);
 
   return (
     <div className="action-list-content">
-      {props.actions.map((item, index) => (
+      {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)
         }}
       >
         点击配置执行动作
@@ -43,10 +34,28 @@ export default (props: ListProps) => {
       {visible && (
         <Modal
           name={props.actions.length + 1}
-          data={{}}
+          data={{
+            key: `${props.type}_${props.actions.length}`,
+          }}
           close={() => {
             setVisible(false);
           }}
+          save={(data: any) => {
+            const { type, ...extra } = data;
+            const item: ActionsType = {
+              ...extra,
+              executor: data.type === 'trigger' || data.type === 'relieve' ? 'alarm' : data.type,
+              key: data.key,
+              alarm: {
+                mode: data.type,
+              },
+            };
+            const index = FormModel?.actions.findIndex((i) => {
+              return i.key === item.key ? item : i;
+            });
+            FormModel.actions[index] = { ...item };
+            setVisible(false);
+          }}
         />
       )}
     </div>

+ 21 - 1
src/pages/rule-engine/Scene/Save/action/ListItem/index.less

@@ -11,7 +11,6 @@
     }
   }
 }
-
 .actions-item {
   position: relative;
   margin-bottom: 24px;
@@ -19,6 +18,27 @@
   border: 1px dashed #999;
   border-radius: 2px;
 
+  .item-options-warp {
+    display: flex;
+    height: 48px;
+
+    .item-options-type {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 48px;
+      margin-right: 8px;
+      background-color: #fafafa;
+    }
+
+    .item-options-content {
+      display: flex;
+      align-items: center;
+      padding: 0 8px;
+      background-color: #fafafa;
+    }
+  }
+
   .item-number {
     position: absolute;
     top: 0;

+ 9 - 2
src/pages/rule-engine/Scene/Save/action/Modal/add.tsx

@@ -3,12 +3,15 @@ import ActionsTypeComponent from '@/pages/rule-engine/Scene/Save/components/Trig
 import { useEffect, useState } from 'react';
 import Notify from '../notify';
 import type { ActionsType } from '@/pages/rule-engine/Scene/typings';
+import Device from '../DeviceOutput';
 
 interface Props {
   close: () => void;
+  save: (data: any) => void;
   data: Partial<ActionsType>;
   name: number;
 }
+
 export default (props: Props) => {
   const [form] = Form.useForm();
   const [actionType, setActionType] = useState<string>('');
@@ -22,15 +25,17 @@ export default (props: Props) => {
   }, [props.data]);
 
   const actionTypeComponent = (type: string) => {
+    console.log(type, '111');
     switch (type) {
+      case 'device':
+        return <Device />;
       case 'notify':
         return (
           <Notify
             value={props.data?.notify || {}}
             save={(data: any) => {
-              console.log(data); // value
               setActionType('');
-              props.close();
+              props.save(data);
             }}
             name={props.name}
             cancel={() => {
@@ -53,7 +58,9 @@ export default (props: Props) => {
       }}
       onOk={async () => {
         const values = await form.validateFields();
+        console.log(values.type);
         setActionType(values.type);
+        // props.save({ ...props.data, type: values.type });
       }}
     >
       <Form form={form} layout={'vertical'}>

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

@@ -0,0 +1,176 @@
+import { Badge, Modal, Tooltip } from 'antd';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useEffect, useRef, useState } from 'react';
+import { Store } from 'jetlinks-store';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { queryDefaultLevel, queryAlarmList, queryAlarmCount } from '../service';
+import encodeQuery from '@/utils/encodeQuery';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+
+interface Props {
+  close: () => void;
+}
+
+export default (props: Props) => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [count, setCount] = useState<number>(0);
+
+  useEffect(() => {
+    queryAlarmCount(
+      encodeQuery({
+        terms: {
+          sceneId: FormModel.id,
+        },
+      }),
+    ).then((resp) => {
+      if (resp.status === 200) {
+        setCount(resp.result);
+      }
+    });
+  }, []);
+
+  const columns: ProColumns<ConfigurationItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '名称',
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '类型',
+      dataIndex: 'targetType',
+      renderText: (text: string) => {
+        const map = {
+          product: '产品',
+          device: '设备',
+          org: '组织',
+          other: '其他',
+        };
+        return map[text];
+      },
+      valueType: 'select',
+      valueEnum: {
+        product: {
+          text: '产品',
+          status: 'product',
+        },
+        device: {
+          text: '设备',
+          status: 'device',
+        },
+        org: {
+          text: '组织',
+          status: 'org',
+        },
+        other: {
+          text: '其他',
+          status: 'other',
+        },
+      },
+    },
+    {
+      title: '告警级别',
+      dataIndex: 'level',
+      render: (text: any) => (
+        <Tooltip
+          placement="topLeft"
+          title={
+            (Store.get('default-level') || []).find((item: any) => item?.level === text)?.title ||
+            text
+          }
+        >
+          <div className="ellipsis">
+            {(Store.get('default-level') || []).find((item: any) => item?.level === text)?.title ||
+              text}
+          </div>
+        </Tooltip>
+      ),
+      valueType: 'select',
+      request: async () => {
+        const res = await queryDefaultLevel();
+        if (res.status === 200) {
+          return (res?.result?.levels || [])
+            .filter((i: any) => i?.level && i?.title)
+            .map((item: any) => ({
+              label: item.title,
+              value: item.level,
+            }));
+        }
+        return [];
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+      valueEnum: {
+        disabled: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.disabled',
+            defaultMessage: '禁用',
+          }),
+          status: 'disabled',
+        },
+        enabled: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.enabled',
+            defaultMessage: '正常',
+          }),
+          status: 'enabled',
+        },
+      },
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      ellipsis: true,
+    },
+  ];
+
+  return (
+    <Modal
+      title={'关联此场景的告警'}
+      open
+      width={1000}
+      onCancel={() => {
+        props.close();
+      }}
+      onOk={() => {
+        props.close();
+      }}
+    >
+      <div>关联告警数量:{count}</div>
+      <ProTable<ConfigurationItem>
+        actionRef={actionRef}
+        params={{}}
+        columns={columns}
+        search={false}
+        rowKey={'id'}
+        columnEmptyText={''}
+        tableAlertRender={false}
+        request={(params) =>
+          queryAlarmList({
+            ...params,
+            terms: [
+              ...(params?.terms || []),
+              {
+                terms: [
+                  {
+                    column: 'sceneId',
+                    value: FormModel.id,
+                  },
+                ],
+                type: 'and',
+              },
+            ],
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          })
+        }
+      />
+    </Modal>
+  );
+};

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

@@ -49,6 +49,7 @@ export default observer(() => {
           columns={columns}
           rowKey="id"
           search={false}
+          onlyCard={true}
           gridColumn={2}
           columnEmptyText={''}
           cardRender={(record) => (
@@ -59,7 +60,9 @@ export default observer(() => {
               {...record}
             />
           )}
+          tableAlertRender={false}
           rowSelection={{
+            type: 'radio',
             selectedRowKeys: [NotifyModel.notify?.notifierId || ''],
             onChange: (selectedRowKeys) => {
               if (selectedRowKeys.length) {

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

@@ -48,6 +48,7 @@ export default observer(() => {
           actionRef={actionRef}
           columns={columns}
           rowKey="id"
+          onlyCard={true}
           search={false}
           gridColumn={2}
           columnEmptyText={''}
@@ -59,7 +60,9 @@ export default observer(() => {
               {...record}
             />
           )}
+          tableAlertRender={false}
           rowSelection={{
+            type: 'radio',
             selectedRowKeys: [NotifyModel.notify?.templateId || ''],
             onChange: (selectedRowKeys) => {
               if (selectedRowKeys.length) {

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

@@ -97,3 +97,16 @@ export const getRelations = () =>
       terms: [{ termType: 'eq', column: 'objectTypeName', value: '设备' }],
     },
   });
+
+export const queryDefaultLevel = () =>
+  request(`/${SystemConst.API_BASE}/alarm/config/default/level`, {
+    method: 'GET',
+  });
+
+export const queryAlarmList = (data: any) => {
+  return request(`/${SystemConst.API_BASE}/alarm/config/_query/`, { data, method: 'POST' });
+};
+
+export const queryAlarmCount = (data: any) => {
+  return request(`/${SystemConst.API_BASE}/alarm/config/_count`, { params: data, method: 'GET' });
+};

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

@@ -1,7 +1,7 @@
 .select-wrapper {
   position: relative;
   width: 100%;
-  margin-bottom: 20px;
+  // margin-bottom: 20px;
   .select-container {
     position: absolute;
     top: 32px;

+ 13 - 17
src/pages/rule-engine/Scene/Save/components/ParamsSelect/index.tsx

@@ -14,11 +14,12 @@ export interface ItemProps {
 interface Props {
   placeholder?: string;
   value: any;
-  onChange: (dt: any) => void;
+  onChange: (valeu: any, sources?: any) => void;
   inputProps?: InputProps;
   itemList: ItemProps[];
   style?: object;
   tabKey: string;
+  type?: string;
 }
 
 export default (props: Props) => {
@@ -27,7 +28,6 @@ export default (props: Props) => {
   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);
@@ -52,20 +52,9 @@ export default (props: Props) => {
     };
   });
 
-  // 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;
-  //   }
-  // }
+  useEffect(() => {
+    props.onChange(value, tabKey);
+  }, [value, tabKey]);
 
   return (
     <div className={'select-wrapper'} ref={wrapperRef} style={props.style}>
@@ -73,12 +62,19 @@ export default (props: Props) => {
         suffix={<DownOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
         {...props.inputProps}
         value={value}
+        onChange={(e) => {
+          setValue(e.target.value);
+        }}
         onFocus={() => {
           setVisible(true);
         }}
       />
       {visible && (
-        <div className={'select-container'} ref={nodeRef}>
+        <div
+          className={'select-container'}
+          ref={nodeRef}
+          style={props.type !== 'date' ? { minHeight: 100 } : undefined}
+        >
           <div className={'select-box'}>
             <div className={'select-box-header-top'}>
               <div className={'select-box-header'}>