wzyyy il y a 3 ans
Parent
commit
33ee227db7
98 fichiers modifiés avec 3543 ajouts et 616 suppressions
  1. 3 1
      package.json
  2. BIN
      public/images/network/CTWing.jpg
  3. BIN
      public/images/network/OneNet.jpg
  4. BIN
      public/images/network/doeros.jpg
  5. BIN
      public/images/notice/webhook-backgroud.png
  6. BIN
      public/images/notice/webhook.png
  7. BIN
      public/images/running/doc.png
  8. BIN
      public/images/running/docx.png
  9. BIN
      public/images/running/flv.png
  10. BIN
      public/images/running/jpg.png
  11. BIN
      public/images/running/mp3.png
  12. BIN
      public/images/running/mp4.png
  13. BIN
      public/images/running/mvb.png
  14. BIN
      public/images/running/other.png
  15. BIN
      public/images/running/pdf.png
  16. BIN
      public/images/running/png.png
  17. BIN
      public/images/running/ppt.png
  18. BIN
      public/images/running/pptx.png
  19. BIN
      public/images/running/rmvb.png
  20. BIN
      public/images/running/swf.png
  21. BIN
      public/images/running/tiff.png
  22. BIN
      public/images/running/txt.png
  23. BIN
      public/images/running/wma.png
  24. BIN
      public/images/running/xls.png
  25. BIN
      public/images/running/xlsx.png
  26. 14 0
      src/app.tsx
  27. 26 0
      src/components/AMapComponent/APILoader.ts
  28. 94 0
      src/components/AMapComponent/PathSimplifier/PathNavigator.tsx
  29. 84 0
      src/components/AMapComponent/PathSimplifier/index.tsx
  30. 130 0
      src/components/AMapComponent/PathSimplifier/types.d.ts
  31. 55 0
      src/components/AMapComponent/amap.tsx
  32. 30 9
      src/components/FTermArrayCards/index.tsx
  33. 18 3
      src/components/FTermTypeSelect/index.tsx
  34. 2 0
      src/components/index.ts
  35. 5 0
      src/pages/Northbound/AliCloud/index.tsx
  36. 133 0
      src/pages/Northbound/DuerOS/index.tsx
  37. 4 0
      src/pages/Northbound/DuerOS/types.d.ts
  38. 52 0
      src/pages/demo/AMap/index.tsx
  39. 4 0
      src/pages/device/Instance/Detail/Config/Edit.tsx
  40. 5 8
      src/pages/device/Instance/Detail/Config/index.tsx
  41. 47 0
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  42. 319 34
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  43. 11 5
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  44. 1 1
      src/pages/device/Instance/Detail/Reation/index.tsx
  45. 40 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx
  46. 25 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  47. 87 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  48. 2 3
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  49. 2 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  50. 1 1
      src/pages/device/Instance/index.tsx
  51. 19 2
      src/pages/device/Instance/service.ts
  52. 25 29
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  53. 129 0
      src/pages/link/AccessConfig/Detail/Channel/index.tsx
  54. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less
  55. 97 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx
  56. 134 0
      src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx
  57. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less
  58. 130 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx
  59. 18 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less
  60. 157 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx
  61. 81 0
      src/pages/link/AccessConfig/Detail/Cloud/index.less
  62. 117 0
      src/pages/link/AccessConfig/Detail/Cloud/index.tsx
  63. 0 15
      src/pages/link/AccessConfig/Detail/Provider/index.less
  64. 70 83
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  65. 36 2
      src/pages/link/AccessConfig/Detail/index.tsx
  66. 90 1
      src/pages/notice/Config/Detail/index.tsx
  67. 43 9
      src/pages/notice/Template/Debug/index.tsx
  68. 85 15
      src/pages/notice/Template/Detail/index.tsx
  69. 5 0
      src/pages/notice/Template/service.ts
  70. 1 0
      src/pages/notice/Template/typings.d.ts
  71. 14 0
      src/pages/notice/index.tsx
  72. 28 5
      src/pages/rule-engine/Alarm/Config/index.tsx
  73. 22 16
      src/pages/rule-engine/Alarm/Configuration/Save/index.tsx
  74. 1 1
      src/pages/rule-engine/Alarm/Log/SolveLog/index.tsx
  75. 1 1
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  76. 1 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx
  77. 152 52
      src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx
  78. 45 7
      src/pages/rule-engine/Scene/Save/action/action.tsx
  79. 0 2
      src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx
  80. 2 1
      src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx
  81. 24 5
      src/pages/rule-engine/Scene/Save/action/device/index.tsx
  82. 84 16
      src/pages/rule-engine/Scene/Save/action/messageContent.tsx
  83. 1 1
      src/pages/rule-engine/Scene/Save/components/InputNumber.tsx
  84. 6 1
      src/pages/rule-engine/Scene/Save/index.less
  85. 74 67
      src/pages/rule-engine/Scene/Save/index.tsx
  86. 29 0
      src/pages/rule-engine/Scene/Save/trigger/OrgTreeSelect.tsx
  87. 65 28
      src/pages/rule-engine/Scene/Save/trigger/device.tsx
  88. 164 75
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  89. 46 29
      src/pages/rule-engine/Scene/Save/trigger/operation.tsx
  90. 56 17
      src/pages/rule-engine/Scene/TriggerTerm/index.tsx
  91. 19 4
      src/pages/rule-engine/Scene/index.tsx
  92. 1 0
      src/pages/rule-engine/Scene/typings.d.ts
  93. 4 0
      src/pages/system/Config/index.tsx
  94. 6 0
      src/pages/system/Config/service.ts
  95. 2 0
      src/utils/const.ts
  96. 3 0
      src/utils/menu/index.ts
  97. 2 0
      src/utils/menu/router.ts
  98. 214 66
      yarn.lock

+ 3 - 1
package.json

@@ -75,7 +75,8 @@
     "@types/react-syntax-highlighter": "^13.5.2",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",
-    "antd": "^4.18.8",
+    "antd": "4.19.5",
+    "bizcharts": "^4.1.16",
     "braft-editor": "^2.3.9",
     "classnames": "^2.3.1",
     "dexie": "^3.0.3",
@@ -88,6 +89,7 @@
     "monaco-editor-webpack-plugin": "^6.0.0",
     "omit.js": "^2.0.2",
     "react": "^17.0.0",
+    "react-amap": "^1.2.8",
     "react-dev-inspector": "^1.1.1",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",

BIN
public/images/network/CTWing.jpg


BIN
public/images/network/OneNet.jpg


BIN
public/images/network/doeros.jpg


BIN
public/images/notice/webhook-backgroud.png


BIN
public/images/notice/webhook.png


BIN
public/images/running/doc.png


BIN
public/images/running/docx.png


BIN
public/images/running/flv.png


BIN
public/images/running/jpg.png


BIN
public/images/running/mp3.png


BIN
public/images/running/mp4.png


BIN
public/images/running/mvb.png


BIN
public/images/running/other.png


BIN
public/images/running/pdf.png


BIN
public/images/running/png.png


BIN
public/images/running/ppt.png


BIN
public/images/running/pptx.png


BIN
public/images/running/rmvb.png


BIN
public/images/running/swf.png


BIN
public/images/running/tiff.png


BIN
public/images/running/txt.png


BIN
public/images/running/wma.png


BIN
public/images/running/xls.png


BIN
public/images/running/xlsx.png


+ 14 - 0
src/app.tsx

@@ -7,6 +7,7 @@ import RightContent from '@/components/RightContent';
 import Footer from '@/components/Footer';
 import { BookOutlined, LinkOutlined } from '@ant-design/icons';
 import Service from '@/pages/user/Login/service';
+import { service as SystemConfigService } from '@/pages/system/Config';
 import Token from '@/utils/token';
 import type { RequestOptionsInit } from 'umi-request';
 import ReconnectingWebSocket from 'reconnecting-websocket';
@@ -248,8 +249,21 @@ export function patchRoutes(routes: any) {
 
 export function render(oldRender: any) {
   if (history.location.pathname !== loginPath) {
+    SystemConfigService.getAMapKey().then((res) => {
+      if (res && res.status === 200 && res.result) {
+        localStorage.setItem(SystemConst.AMAP_KEY, res.result.apiKey);
+      }
+    });
     MenuService.queryOwnThree({ paging: false }).then((res) => {
       if (res && res.status === 200) {
+        if (isDev) {
+          res.result.push({
+            code: 'demo',
+            id: 'demo',
+            name: '例子',
+            url: '/demo',
+          });
+        }
         extraRoutes = handleRoutes([...extraRouteArr, ...res.result]);
         saveMenusCache(extraRoutes);
       }

+ 26 - 0
src/components/AMapComponent/APILoader.ts

@@ -0,0 +1,26 @@
+const protocol = window.location.protocol;
+
+const buildScriptTag = (src: string): HTMLScriptElement => {
+  const script = document.createElement('script');
+  script.type = 'text/javascript';
+  script.async = true;
+  script.defer = true;
+  script.src = src;
+  return script;
+};
+
+export const getAMapUiPromise = (version: string = '1.1'): Promise<any> => {
+  if ((window as any).AMapUI) {
+    return Promise.resolve();
+  }
+  const script = buildScriptTag(`${protocol}//webapi.amap.com/ui/${version}/main-async.js`);
+  const pro = new Promise((resolve) => {
+    script.onload = () => {
+      (window as any).initAMapUI();
+      resolve(true);
+    };
+  });
+
+  document.body.append(script);
+  return pro;
+};

+ 94 - 0
src/components/AMapComponent/PathSimplifier/PathNavigator.tsx

@@ -0,0 +1,94 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { omit } from 'lodash';
+
+interface PathNavigatorProps extends PathNavigatorOptions {
+  __pathSimplifier__?: PathSimplifier;
+  navKey?: number;
+  onCreate?: (nav: PathNavigator) => void;
+  /**
+   * 是否自动播放
+   * @default true
+   */
+  isAuto?: boolean;
+  onStart?: (e: any) => void;
+  onPause?: (e: any) => void;
+  onMove?: (e: any) => void;
+  onStop?: (e: any) => void;
+}
+
+const EventMap = {
+  start: 'onStart',
+  pause: 'onPause',
+  move: 'onMove',
+  stop: 'onStop',
+};
+
+export default (props: PathNavigatorProps) => {
+  const { __pathSimplifier__, navKey, isAuto, onCreate, ...extraProps } = props;
+
+  const PathNavigatorRef = useRef<PathNavigator | null>(null);
+
+  const createEvent = () => {
+    Object.keys(EventMap).forEach((event) => {
+      if (props[EventMap[event]]) {
+        PathNavigatorRef.current?.on(event, props[EventMap[event]]);
+      }
+    });
+  };
+
+  const removeEvent = () => {
+    Object.keys(EventMap).forEach((event) => {
+      if (props[EventMap[event]]) {
+        PathNavigatorRef.current?.off(event, props[EventMap[event]]);
+      }
+    });
+  };
+
+  const createPathNavigator = useCallback(
+    (path?: PathSimplifier) => {
+      if (path) {
+        PathNavigatorRef.current = path.createPathNavigator(navKey!, {
+          speed: props.speed || 10000,
+          ...omit(extraProps, Object.values(EventMap)),
+        });
+
+        if (PathNavigatorRef.current) {
+          createEvent();
+        }
+
+        if (onCreate && PathNavigatorRef.current) {
+          onCreate(PathNavigatorRef.current);
+        }
+
+        if (props.isAuto !== false) {
+          PathNavigatorRef.current?.start();
+        }
+      }
+    },
+    [props],
+  );
+
+  useEffect(() => {
+    if (PathNavigatorRef.current && props.speed !== undefined) {
+      PathNavigatorRef.current?.setSpeed(props.speed);
+    }
+  }, [props.speed]);
+
+  useEffect(() => {
+    if (PathNavigatorRef.current && props.range !== undefined) {
+      PathNavigatorRef.current?.setRange(props.range[0], props.range[1]);
+    }
+  }, [props.range]);
+
+  useEffect(() => {
+    createPathNavigator(props.__pathSimplifier__);
+
+    return () => {
+      if (PathNavigatorRef.current) {
+        removeEvent();
+      }
+    };
+  }, []);
+
+  return null;
+};

+ 84 - 0
src/components/AMapComponent/PathSimplifier/index.tsx

@@ -0,0 +1,84 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import PathNavigator from './PathNavigator';
+
+interface PathSimplifierProps {
+  __map__?: any;
+  options?: Omit<PathSimplifierOptions, 'map'>;
+  pathData?: PathDataItemType[];
+  children?: React.ReactNode;
+  onCreated?: (nav: PathSimplifier) => void;
+}
+
+const PathSimplifier = (props: PathSimplifierProps) => {
+  const { pathData, __map__, onCreated, options } = props;
+
+  const pathSimplifierRef = useRef<PathSimplifier | null>(null);
+  const [loading, setLoading] = useState(false);
+
+  const pathSimplifier = useCallback(
+    (PathObj: PathSimplifier) => {
+      pathSimplifierRef.current = new PathObj({
+        zIndex: 100,
+        getPath: (_pathData: any) => {
+          return _pathData.path;
+        },
+        getHoverTitle: (_pathData: any) => {
+          return _pathData.name;
+        },
+        map: __map__,
+        ...options,
+      });
+      if (pathData) {
+        pathSimplifierRef.current?.setData(
+          pathData.map((item) => ({ name: item.name || '路线', path: item.path })),
+        );
+        setLoading(true);
+      }
+
+      if (onCreated) {
+        onCreated(pathSimplifierRef.current!);
+      }
+    },
+    [props],
+  );
+
+  const loadUI = () => {
+    if ((window as any).AMapUI) {
+      (window as any).AMapUI.load(['ui/misc/PathSimplifier', 'lib/$'], (path: PathSimplifier) => {
+        if (!path.supportCanvas) {
+          console.warn('当前环境不支持 Canvas!');
+          return;
+        }
+        pathSimplifier(path);
+      });
+    }
+  };
+
+  const renderChildren = () => {
+    return React.Children.map(props.children, (child, index) => {
+      if (child) {
+        if (typeof child === 'string') {
+          return child;
+        } else {
+          return React.cloneElement(child as any, {
+            __pathSimplifier__: pathSimplifierRef.current,
+            navKey: index,
+          });
+        }
+      }
+      return child;
+    });
+  };
+
+  useEffect(() => {
+    if (__map__) {
+      loadUI();
+    }
+  }, [__map__]);
+
+  return <>{loading && renderChildren()}</>;
+};
+
+PathSimplifier.PathNavigator = PathNavigator;
+
+export default PathSimplifier;

+ 130 - 0
src/components/AMapComponent/PathSimplifier/types.d.ts

@@ -0,0 +1,130 @@
+type PathDataType = number[][];
+
+type PathSimplifierOptions = {
+  map?: any;
+  zIndex?: number;
+  data?: number[][];
+  getPath?: (pathData: {}, pathIndex: number) => PathDataType;
+  getZIndex?: (pathData: any, pathIndex: number) => number;
+  getHoverTitle?: (pathData: any, pathIndex: number, pointIndex: number) => string;
+  autoSetFitView?: boolean;
+  clickToSelectPath?: boolean;
+  onTopWhenSelected?: boolean;
+  renderConstructor?: Function;
+  renderOptions?: {};
+};
+
+type PathDataItemType = {
+  name?: string;
+  path: PathDataType;
+};
+
+interface PathSimplifier {
+  new (options: PathSimplifierOptions);
+
+  readonly supportCanvas: boolean;
+
+  getZIndexOfPath: (pathIndex: number) => number;
+
+  setZIndexOfPath: (pathIndex: number, zIndex: number) => void;
+
+  /**
+   * 是否置顶显示pathIndex对应的轨迹
+   * @param pathIndex
+   * @param isTop isTop为真,设置 zIndex 为 现存最大zIndex+1; isTop为假,设置 zIndex 为 构造参数中 getZIndex 的返回值
+   */
+  toggleTopOfPath: (pathIndex: number, isTop: boolean) => void;
+
+  getPathData: (pathIndex: number) => any;
+
+  createPathNavigator: (pathIndex: number, options: {}) => PathNavigator;
+
+  getPathNavigators: () => any[];
+
+  clearPathNavigators: () => void;
+
+  getSelectedPathData: () => any;
+
+  getSelectedPathIndex: () => number;
+
+  isSelectedPathIndex: (pathIndex: number) => boolean;
+
+  setSelectedPathIndex: (pathIndex: number) => void;
+
+  render: () => void;
+
+  renderLater: (delay: number[]) => void;
+
+  setData: (data: any[]) => void;
+
+  setFitView: (pathIndex: number) => void;
+
+  on: (eventName: string, handler: Function) => void;
+
+  off: (eventName: string, handler: Function) => void;
+
+  hide: () => void;
+
+  show: () => void;
+
+  isHidden: () => boolean;
+
+  getRender: () => boolean;
+
+  getRenderOptions: () => any;
+}
+
+interface PathNavigatorOptions {
+  loop?: boolean;
+  speed?: number;
+  pathNavigatorStyle?: {};
+  animInterval?: number;
+  dirToPosInMillsecs?: number;
+  range?: [number, number];
+}
+
+interface PathNavigator {
+  new (options: PathNavigatorOptions);
+
+  start: (pointIndex?: number) => void;
+
+  pause: () => void;
+
+  resume: () => void;
+
+  stop: () => void;
+
+  destroy: () => void;
+
+  getCursor: () => any;
+
+  getNaviStatus: () => string;
+
+  getPathIndex: () => number;
+
+  getPosition: () => [number, number];
+
+  getSpeed: () => number;
+
+  getMovedDistance: () => number;
+
+  getPathStartIdx: () => number;
+
+  getPathEndIdx: () => number;
+
+  moveByDistance: (distance: number) => void;
+
+  moveToPoint: (idx: number, tail: number) => void;
+
+  isCursorAtPathEnd: () => boolean;
+
+  isCursorAtPathStart: () => boolean;
+
+  setSpeed: (speed: number) => void;
+
+  setRange: (startIndex: number, endIndex: number) => void;
+
+  on: (eventName: string, handler: Function) => void;
+
+  off: (eventName: string, handler: Function) => void;
+}

+ 55 - 0
src/components/AMapComponent/amap.tsx

@@ -0,0 +1,55 @@
+import React, { useState } from 'react';
+import type { MapProps } from 'react-amap';
+import { Map } from 'react-amap';
+import { getAMapUiPromise } from './APILoader';
+import SystemConst from '@/utils/const';
+import { Empty } from 'antd';
+
+interface AMapProps extends Omit<MapProps, 'amapkey' | 'useAMapUI'> {
+  style?: React.CSSProperties;
+  className?: string;
+  AMapUI?: string | boolean;
+}
+
+export default (props: AMapProps) => {
+  const { style, className, onInstanceCreated, ...extraProps } = props;
+
+  const [uiLoading, setUiLoading] = useState(false);
+
+  const isOpenUi = 'AMapUI' in props || props.AMapUI;
+
+  const amapKey = localStorage.getItem(SystemConst.AMAP_KEY);
+
+  const getAMapUI = () => {
+    const version = typeof props.AMapUI === 'string' ? props.AMapUI : '1.1';
+    getAMapUiPromise(version).then(() => {
+      setUiLoading(true);
+    });
+  };
+
+  return (
+    <div style={style || { width: '100%', height: '100%' }} className={className}>
+      {amapKey ? (
+        // @ts-ignore
+        <Map
+          version={'2.0'}
+          amapkey={amapKey}
+          zooms={[3, 20]}
+          onInstanceCreated={(map: any) => {
+            if (onInstanceCreated) {
+              onInstanceCreated(map);
+            }
+            if (isOpenUi) {
+              getAMapUI();
+            }
+          }}
+          {...extraProps}
+        >
+          {isOpenUi ? (uiLoading ? props.children : null) : props.children}
+        </Map>
+      ) : (
+        <Empty description={'请配置高德地图key'} />
+      )}
+    </div>
+  );
+};

+ 30 - 9
src/components/FTermArrayCards/index.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Card, Empty, Select } from 'antd';
+import { Card, Empty } from 'antd';
 import { CardProps } from 'antd/lib/card';
 import { ArrayField } from '@formily/core';
 import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
@@ -95,14 +95,35 @@ export const FTermArrayCards: ComposedArrayCards = observer((props) => {
         ArrayBase.Item && (
           <ArrayBase.Item key={index} index={index} record={item}>
             {index > 0 && (
-              <div style={{ margin: 10, display: 'flex', justifyContent: 'center' }}>
-                <Select
-                  value="or"
-                  style={{ width: '200px' }}
-                  options={[
-                    { label: '或者', value: 'or' },
-                    { label: '并且', value: 'and' },
-                  ]}
+              <div style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }}>
+                <RecursionField
+                  schema={
+                    {
+                      type: 'object',
+                      properties: {
+                        termType: {
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Select',
+                          'x-component-props': {
+                            style: {
+                              width: 100,
+                            },
+                          },
+                          default: 'and',
+                          enum: [
+                            { label: '并且', value: 'and' },
+                            { label: '或者', value: 'or' },
+                          ],
+                        },
+                      },
+                    } as any
+                  }
+                  name={index}
+                  filterProperties={(schema2) => {
+                    if (isIndexComponent(schema2)) return false;
+                    if (isOperationComponent(schema2)) return false;
+                    return true;
+                  }}
                 />
               </div>
             )}

+ 18 - 3
src/components/FTermTypeSelect/index.tsx

@@ -1,12 +1,27 @@
 import { ArrayItems, Select } from '@formily/antd';
 
-const FTermTypeSelect = () => {
+interface Props {
+  value: any;
+  onChange: (value: string) => void;
+}
+
+const FTermTypeSelect = (props: Props) => {
   const index = ArrayItems.useIndex!();
   return index > 0 ? (
-    <div style={{ width: '100%', marginBottom: 15, display: 'flex', justifyContent: 'center' }}>
+    <div
+      style={{
+        width: '100%',
+        marginTop: -20,
+        marginBottom: 15,
+        display: 'flex',
+        justifyContent: 'center',
+      }}
+    >
       <Select
+        onChange={(value) => props.onChange(value)}
+        value={props.value}
         style={{ width: '200px' }}
-        value="or"
+        defaultValue={'or'}
         options={[
           { label: '并且', value: 'and' },
           { label: '或者', value: 'or' },

+ 2 - 0
src/components/index.ts

@@ -9,3 +9,5 @@ export { default as Modal } from './Modal';
 export { default as AIcon } from './AIcon';
 export { default as PermissionButton } from './PermissionButton';
 export { default as TitleComponent } from './TitleComponent';
+export { default as AMap } from './AMapComponent/amap';
+export { default as PathSimplifier } from './AMapComponent/PathSimplifier';

+ 5 - 0
src/pages/Northbound/AliCloud/index.tsx

@@ -0,0 +1,5 @@
+import { PageContainer } from '@ant-design/pro-layout';
+
+export default () => {
+  return <PageContainer>AliCloud</PageContainer>;
+};

+ 133 - 0
src/pages/Northbound/DuerOS/index.tsx

@@ -0,0 +1,133 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import SearchComponent from '@/components/SearchComponent';
+import { useRef, useState } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { PermissionButton, ProTableCard } from '@/components';
+import { DeleteOutlined, EditOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+
+export default () => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const { permission } = PermissionButton.usePermission('Northbound/DuerOS');
+
+  const Tools = (record: any, type: 'card' | 'table') => {
+    return [
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.update}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                }),
+              }
+            : undefined
+        }
+        onClick={() => {}}
+      >
+        <EditOutlined />
+        {type !== 'table' &&
+          intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+      </PermissionButton>,
+      <PermissionButton
+        key={'started'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.action}
+        popConfirm={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${
+              record.state.value === 'started' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          }),
+          onConfirm: async () => {},
+        }}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: `pages.data.option.${
+                    record.state.value === 'started' ? 'disabled' : 'enabled'
+                  }`,
+                  defaultMessage: '启用',
+                }),
+              }
+            : undefined
+        }
+      >
+        {record.state.value === 'started' ? <StopOutlined /> : <PlayCircleOutlined />}
+        {type !== 'table' &&
+          intl.formatMessage({
+            id: `pages.data.option.${record.state.value === 'started' ? 'disabled' : 'enabled'}`,
+            defaultMessage: record.state.value === 'started' ? '禁用' : '启用',
+          })}
+      </PermissionButton>,
+      <PermissionButton
+        key={'delete'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.delete}
+        disabled={record.state.value === 'started'}
+        popConfirm={{
+          title: '确认删除?',
+          disabled: record.state.value === 'started',
+          onConfirm: () => {},
+        }}
+        tooltip={{
+          title:
+            record.state.value === 'started' ? <span>请先禁用,再删除</span> : <span>删除</span>,
+        }}
+      >
+        <DeleteOutlined />
+      </PermissionButton>,
+    ];
+  };
+
+  const columns: ProColumns<DuerOSType>[] = [
+    {
+      dataIndex: 'name',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => Tools(record, 'table'),
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<DuerOSType>
+        field={columns}
+        target="device-instance"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard<DuerOSType>
+        rowKey="id"
+        search={false}
+        columns={columns}
+        actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+      />
+    </PageContainer>
+  );
+};

+ 4 - 0
src/pages/Northbound/DuerOS/types.d.ts

@@ -0,0 +1,4 @@
+type DuerOSType = {
+  id: string;
+  name: string;
+};

+ 52 - 0
src/pages/demo/AMap/index.tsx

@@ -0,0 +1,52 @@
+import { AMap, PathSimplifier } from '@/components';
+import { useState } from 'react';
+
+export default () => {
+  const [speed] = useState(100000);
+  return (
+    <AMap
+      AMapUI
+      style={{
+        height: 500,
+        width: '100%',
+      }}
+    >
+      <PathSimplifier
+        pathData={[
+          {
+            name: '线路1',
+            path: [
+              [116.405289, 39.904987],
+              [113.964458, 40.54664],
+              [111.47836, 41.135964],
+              [108.949297, 41.670904],
+              [106.380111, 42.149509],
+              [103.774185, 42.56996],
+              [101.135432, 42.930601],
+              [98.46826, 43.229964],
+              [95.777529, 43.466798],
+              [93.068486, 43.64009],
+              [90.34669, 43.749086],
+              [87.61792, 43.793308],
+            ],
+          },
+        ]}
+      >
+        <PathSimplifier.PathNavigator
+          speed={speed}
+          onStart={() => {
+            console.log('start');
+          }}
+          onCreate={(nav) => {
+            setTimeout(() => {
+              nav.pause();
+            }, 5000);
+            setTimeout(() => {
+              nav.resume(); // 恢复
+            }, 7000);
+          }}
+        />
+      </PathSimplifier>
+    </AMap>
+  );
+};

+ 4 - 0
src/pages/device/Instance/Detail/Config/Edit.tsx

@@ -14,6 +14,7 @@ const componentMap = {
 interface Props {
   close: () => void;
   metadata: any[];
+  reload: () => void;
 }
 
 const Edit = (props: Props) => {
@@ -106,6 +107,9 @@ const Edit = (props: Props) => {
                     setTimeout(() => window.close(), 300);
                   }
                 } else {
+                  if (props.reload) {
+                    props.reload();
+                  }
                   props.close();
                 }
               }

+ 5 - 8
src/pages/device/Instance/Detail/Config/index.tsx

@@ -16,11 +16,7 @@ const Config = () => {
   const params = useParams<{ id: string }>();
   useEffect(() => {
     const id = InstanceModel.current?.id || params.id;
-    if (id) {
-      service.getConfigMetadata(id).then((response) => {
-        InstanceModel.config = response?.result;
-      });
-    } else {
+    if (!id) {
       history.goBack();
     }
   }, []);
@@ -29,7 +25,7 @@ const Config = () => {
   const [visible, setVisible] = useState<boolean>(false);
   const { permission } = PermissionButton.usePermission('device/Instance');
 
-  const id = InstanceModel.detail?.id || params?.id;
+  const id = params?.id;
 
   const getDetail = () => {
     service.detail(id || '').then((resp) => {
@@ -42,7 +38,8 @@ const Config = () => {
   useEffect(() => {
     if (id) {
       service.getConfigMetadata(id).then((config) => {
-        setMetadata(config?.result);
+        InstanceModel.config = config?.result || [];
+        setMetadata(config?.result || []);
       });
     }
   }, [id]);
@@ -180,8 +177,8 @@ const Config = () => {
           metadata={metadata || []}
           close={() => {
             setVisible(false);
-            getDetail();
           }}
+          reload={getDetail}
         />
       )}
     </div>

+ 47 - 0
src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx

@@ -0,0 +1,47 @@
+import { Modal, Input } from 'antd';
+// import ReactMarkdown from "react-markdown";
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (type === 'object') {
+      return (
+        <div>
+          <div>自定义属性</div>
+          {JSON.stringify(value)}
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <div>自定义属性</div>
+          <Input value={value} disabled />
+        </div>
+      );
+    }
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 319 - 34
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,11 +1,14 @@
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { DatePicker, Modal, Radio, Space, Table } from 'antd';
+import { DatePicker, Modal, Radio, Select, Space, Table, Tabs } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import encodeQuery from '@/utils/encodeQuery';
 import { useEffect, useState } from 'react';
 import moment from 'moment';
-
+import { Axis, Chart, Geom, Legend, Tooltip, Slider } from 'bizcharts';
+import FileComponent from '../../Running/Property/FileComponent';
+import { DownloadOutlined, SearchOutlined } from '@ant-design/icons';
+import Detail from './Detail';
 interface Props {
   visible: boolean;
   close: () => void;
@@ -15,11 +18,20 @@ interface Props {
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
   const { visible, close, data } = props;
+  const list = ['int', 'float', 'double', 'long'];
   const [dataSource, setDataSource] = useState<any>({});
   const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
   const [end, setEnd] = useState<number>(new Date().getTime());
   const [radioValue, setRadioValue] = useState<undefined | 'today' | 'week' | 'month'>('today');
   const [dateValue, setDateValue] = useState<any>(undefined);
+  const [chartsList, setChartsList] = useState<any>([]);
+  const [cycle, setCycle] = useState<string>(
+    list.includes(data.valueType?.type || '') ? '*' : '1m',
+  );
+  const [agg, setAgg] = useState<string>('AVG');
+  const [tab, setTab] = useState<string>('table');
+  const [detailVisible, setDetailVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>('');
 
   const columns = [
     {
@@ -29,9 +41,42 @@ const PropertyLog = (props: Props) => {
       render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
     },
     {
-      title: '自定义属性',
-      dataIndex: 'formatValue',
-      key: 'formatValue',
+      title: <span>{data.valueType?.type !== 'file' ? '自定义属性' : '文件内容'}</span>,
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <FileComponent type="table" value={{ formatValue: record.value }} data={data} />
+      ),
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (text: any, record: any) => (
+        <a>
+          {data.valueType?.type !== 'file' ? (
+            <SearchOutlined
+              onClick={() => {
+                setDetailVisible(true);
+                setCurrent(record.value);
+              }}
+            />
+          ) : (
+            <DownloadOutlined />
+          )}
+        </a>
+      ),
+    },
+  ];
+
+  const tabList = [
+    {
+      tab: '列表',
+      key: 'table',
+    },
+    {
+      tab: '图表',
+      key: 'charts',
     },
   ];
 
@@ -55,7 +100,47 @@ const PropertyLog = (props: Props) => {
       });
   };
 
+  const queryChartsList = async (startTime?: number, endTime?: number) => {
+    const resp = await service.queryPropertieList(params.id, data.id || '', {
+      paging: false,
+      terms: [
+        {
+          column: 'timestamp$BTW',
+          value: startTime && endTime ? [startTime, endTime] : [],
+          type: 'and',
+        },
+      ],
+    });
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.data.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.timestamp).format('YYYY-MM-DD HH:mm:ss'),
+          value: i.value,
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
+  const queryChartsAggList = async (datas: any) => {
+    const resp = await service.queryPropertieInfo(params.id, datas);
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.time).format('YYYY-MM-DD HH:mm:ss'),
+          value: Number(i[data.id || '']),
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
   useEffect(() => {
+    console.log(data);
     if (visible) {
       handleSearch(
         {
@@ -68,6 +153,162 @@ const PropertyLog = (props: Props) => {
     }
   }, [visible]);
 
+  const scale = {
+    value: { min: 0 },
+    year: {
+      range: [0, 0.96],
+      type: 'timeCat',
+    },
+  };
+
+  const renderComponent = (type: string) => {
+    switch (type) {
+      case 'table':
+        return (
+          <Table
+            size="small"
+            rowKey={'id'}
+            onChange={(page) => {
+              handleSearch(
+                {
+                  pageSize: page.pageSize,
+                  pageIndex: Number(page.current) - 1 || 0,
+                },
+                start,
+                end,
+              );
+            }}
+            dataSource={dataSource?.data || []}
+            columns={columns}
+            pagination={{
+              pageSize: dataSource?.pageSize || 10,
+              showSizeChanger: true,
+              total: dataSource?.total || 0,
+            }}
+          />
+        );
+      case 'charts':
+        return (
+          <div>
+            <div style={{ margin: '10 0', display: 'flex' }}>
+              <div style={{ marginRight: 20 }}>
+                统计周期:
+                <Select
+                  value={cycle}
+                  style={{ width: 120 }}
+                  onChange={(value: string) => {
+                    setCycle(value);
+                    if (cycle === '*') {
+                      queryChartsList(start, end);
+                    } else {
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: agg,
+                          },
+                        ],
+                        query: {
+                          interval: value,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }
+                  }}
+                >
+                  {list.includes(data.valueType?.type || '') && (
+                    <Select.Option value="*">实际值</Select.Option>
+                  )}
+                  <Select.Option value="1m">按分钟统计</Select.Option>
+                  <Select.Option value="1h">按小时统计</Select.Option>
+                  <Select.Option value="1d">按天统计</Select.Option>
+                  <Select.Option value="1w">按周统计</Select.Option>
+                  <Select.Option value="1M">按月统计</Select.Option>
+                </Select>
+              </div>
+              {cycle !== '*' && list.includes(data.valueType?.type || '') && (
+                <div>
+                  统计规则:
+                  <Select
+                    defaultValue="AVG"
+                    style={{ width: 120 }}
+                    onChange={(value: string) => {
+                      setAgg(value);
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: value,
+                          },
+                        ],
+                        query: {
+                          interval: cycle,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }}
+                  >
+                    <Select.Option value="AVG">平均值</Select.Option>
+                    <Select.Option value="MAX">最大值</Select.Option>
+                    <Select.Option value="MIN">最小值</Select.Option>
+                    <Select.Option value="COUNT">总数</Select.Option>
+                  </Select>
+                </div>
+              )}
+            </div>
+            <div style={{ paddingTop: 15 }}>
+              <Chart height={400} data={chartsList} scale={scale} autoFit>
+                <Legend />
+                <Axis name="year" />
+                <Axis
+                  name="value"
+                  label={{
+                    formatter: (val) => parseFloat(val).toLocaleString(),
+                  }}
+                />
+                <Tooltip showCrosshairs shared />
+                <Geom
+                  type="line"
+                  tooltip={[
+                    'value*type',
+                    (value, name) => {
+                      return {
+                        value: value,
+                        name,
+                      };
+                    },
+                  ]}
+                  position="year*value"
+                  size={2}
+                />
+                <Geom
+                  type="point"
+                  tooltip={false}
+                  position="year*value"
+                  size={4}
+                  shape={'circle'}
+                  style={{
+                    stroke: '#fff',
+                    lineWidth: 1,
+                  }}
+                />
+                <Geom type="area" position="year*value" shape={'circle'} tooltip={false} />
+                <Slider />
+              </Chart>
+            </div>
+          </div>
+        );
+      default:
+        return null;
+    }
+  };
+
   // @ts-ignore
   return (
     <Modal
@@ -98,14 +339,36 @@ const PropertyLog = (props: Props) => {
               setDateValue(undefined);
               setStart(st);
               setEnd(et);
-              handleSearch(
-                {
-                  pageSize: 10,
-                  pageIndex: 0,
-                },
-                st,
-                et,
-              );
+              if (tab === 'charts') {
+                if (list.includes(data.valueType?.type || '')) {
+                  queryChartsList(st, et);
+                } else {
+                  queryChartsAggList({
+                    columns: [
+                      {
+                        property: data.id,
+                        alias: data.id,
+                        agg,
+                      },
+                    ],
+                    query: {
+                      interval: cycle,
+                      format: 'yyyy-MM-dd HH:mm:ss',
+                      from: st,
+                      to: et,
+                    },
+                  });
+                }
+              } else {
+                handleSearch(
+                  {
+                    pageSize: 10,
+                    pageIndex: 0,
+                  },
+                  st,
+                  et,
+                );
+              }
             }}
             style={{ minWidth: 220 }}
           >
@@ -140,28 +403,50 @@ const PropertyLog = (props: Props) => {
           }
         </Space>
       </div>
-
-      <Table
-        size="small"
-        rowKey={'id'}
-        onChange={(page) => {
-          handleSearch(
-            {
-              pageSize: page.pageSize,
-              pageIndex: Number(page.current) - 1 || 0,
-            },
-            start,
-            end,
-          );
-        }}
-        dataSource={dataSource?.data || []}
-        columns={columns}
-        pagination={{
-          pageSize: dataSource?.pageSize || 10,
-          showSizeChanger: true,
-          total: dataSource?.total || 0,
+      <Tabs
+        activeKey={tab}
+        onChange={(key: string) => {
+          setTab(key);
+          if (key === 'charts' && !!data.valueType?.type) {
+            if (list.includes(data.valueType?.type)) {
+              queryChartsList(start, end);
+            } else {
+              setCycle('1m');
+              setAgg('COUNT');
+              queryChartsAggList({
+                columns: [
+                  {
+                    property: data.id,
+                    alias: data.id,
+                    agg: 'COUNT',
+                  },
+                ],
+                query: {
+                  interval: '1m',
+                  format: 'yyyy-MM-dd HH:mm:ss',
+                  from: start,
+                  to: end,
+                },
+              });
+            }
+          }
         }}
-      />
+      >
+        {tabList.map((item) => (
+          <Tabs.TabPane tab={item.tab} key={item.key}>
+            {renderComponent(item.key)}
+          </Tabs.TabPane>
+        ))}
+      </Tabs>
+      {detailVisible && (
+        <Detail
+          close={() => {
+            setDetailVisible(false);
+          }}
+          value={current}
+          type={data.valueType?.type || ''}
+        />
+      )}
     </Modal>
   );
 };

+ 11 - 5
src/pages/device/Instance/Detail/Reation/Edit.tsx

@@ -8,11 +8,12 @@ import { useParams } from 'umi';
 import { Button, Drawer, message, Space } from 'antd';
 import { action } from '@formily/reactive';
 import type { Response } from '@/utils/typings';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
 
 interface Props {
   close: () => void;
   data: any[];
+  reload: () => void;
 }
 
 const Edit = (props: Props) => {
@@ -40,10 +41,12 @@ const Edit = (props: Props) => {
     );
   };
 
-  const form = createForm({
-    validateFirst: true,
-    initialValues: initData,
-  });
+  const form = useMemo(() => {
+    return createForm({
+      validateFirst: true,
+      initialValues: initData,
+    });
+  }, []);
 
   const SchemaField = createSchemaField({
     components: {
@@ -143,6 +146,9 @@ const Edit = (props: Props) => {
                 if (resp.status === 200) {
                   message.success('操作成功!');
                   props.close();
+                  if (props.reload) {
+                    props.reload();
+                  }
                 }
               }
             }}

+ 1 - 1
src/pages/device/Instance/Detail/Reation/index.tsx

@@ -66,8 +66,8 @@ const Reation = () => {
           data={data || []}
           close={() => {
             setVisible(false);
-            getDetail();
           }}
+          reload={getDetail}
         />
       )}
     </div>

+ 40 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx

@@ -0,0 +1,40 @@
+import LivePlayer from '@/components/Player';
+import { Modal, Image } from 'antd';
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (['jpg', 'png', 'tiff'].includes(type)) {
+      return <Image src={value?.formatValue} />;
+    } else if (value?.formatValue.indexOf('https') !== -1) {
+      return <p>域名为https时,不支持访问http地址</p>;
+    } else if (['flv', 'm3u8', 'mp4'].includes(type)) {
+      return <LivePlayer live={false} url={value?.formatValue} />;
+    }
+    return <p>当前仅支持播放.mp4,.flv,.m3u8格式的视频</p>;
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 25 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less

@@ -0,0 +1,25 @@
+.value {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 60px;
+
+  .other {
+    width: 100%;
+    overflow: hidden;
+    color: #323130;
+    font-weight: 700;
+    font-size: 24px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .img {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 60px;
+    height: 100%;
+    border: 1px solid rgba(0, 0, 0, 0.08);
+  }
+}

+ 87 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -0,0 +1,87 @@
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import styles from './index.less';
+import Detail from './Detail';
+import { useState } from 'react';
+
+interface Props {
+  data: Partial<PropertyMetadata>;
+  value: any;
+  type: 'card' | 'table';
+}
+
+const imgMap = new Map<any, any>();
+imgMap.set('txt', require('/public/images/running/txt.png'));
+imgMap.set('doc', require('/public/images/running/doc.png'));
+imgMap.set('xls', require('/public/images/running/xls.png'));
+imgMap.set('ppt', require('/public/images/running/ppt.png'));
+imgMap.set('docx', require('/public/images/running/docx.png'));
+imgMap.set('xlsx', require('/public/images/running/xlsx.png'));
+imgMap.set('pptx', require('/public/images/running/pptx.png'));
+imgMap.set('jpg', require('/public/images/running/jpg.png'));
+imgMap.set('png', require('/public/images/running/png.png'));
+imgMap.set('pdf', require('/public/images/running/pdf.png'));
+imgMap.set('tiff', require('/public/images/running/tiff.png'));
+imgMap.set('swf', require('/public/images/running/swf.png'));
+imgMap.set('flv', require('/public/images/running/flv.png'));
+imgMap.set('rmvb', require('/public/images/running/rmvb.png'));
+imgMap.set('mp4', require('/public/images/running/mp4.png'));
+imgMap.set('mvb', require('/public/images/running/mvb.png'));
+imgMap.set('wma', require('/public/images/running/wma.png'));
+imgMap.set('mp3', require('/public/images/running/mp3.png'));
+imgMap.set('other', require('/public/images/running/other.png'));
+
+const FileComponent = (props: Props) => {
+  const { data, value } = props;
+  const [type, setType] = useState<string>('other');
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const renderValue = () => {
+    if (!value?.formatValue) {
+      return <div className={props.type === 'card' ? styles.other : {}}>--</div>;
+    } else if (data?.valueType?.type === 'file') {
+      const flag: string = value?.formatValue.split('.').pop() || 'other';
+      return (
+        <div
+          className={styles.img}
+          onClick={() => {
+            if (['jpg', 'png', 'tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
+              setType(flag);
+              setVisible(true);
+            }
+          }}
+        >
+          <img src={imgMap.get(flag) || imgMap.get('other')} />
+        </div>
+      );
+    } else if (data?.valueType?.type === 'object') {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {JSON.stringify(value?.formatValue)}
+        </div>
+      );
+    } else {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {String(value?.formatValue)}
+        </div>
+      );
+    }
+  };
+
+  return (
+    <div className={styles.value}>
+      {renderValue()}
+      {visible && (
+        <Detail
+          type={type}
+          value={value}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+export default FileComponent;

+ 2 - 3
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -14,6 +14,7 @@ import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditPr
 import moment from 'moment';
 import Indicators from './Indicators';
 import './PropertyCard.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -88,9 +89,7 @@ const Property = (props: Props) => {
       <Spin spinning={loading}>
         <div>
           <div>{renderTitle(data?.name || '')}</div>
-          <div className="value" style={{ fontWeight: 700, fontSize: '24px', color: '#323130' }}>
-            {value?.formatValue || '--'}
-          </div>
+          <FileComponent type="card" value={value} data={data} />
           <div style={{ marginTop: 10 }}>
             <div style={{ color: 'rgba(0, 0, 0, .65)', fontSize: 12 }}>更新时间</div>
             <div style={{ marginTop: 5, fontSize: 16, color: 'black' }} className="value">

+ 2 - 1
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -12,6 +12,7 @@ import { useParams } from 'umi';
 import PropertyLog from '../../MetadataLog/Property';
 import moment from 'moment';
 import styles from './index.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>[];
@@ -64,7 +65,7 @@ const Property = (props: Props) => {
       dataIndex: 'value',
       key: 'value',
       render: (text: any, record: any) => (
-        <span>{propertyValue[record.id]?.formatValue || '--'}</span>
+        <FileComponent type="table" value={propertyValue[record.id]} data={record} />
       ),
     },
     {

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

@@ -290,7 +290,7 @@ const Instance = () => {
       valueType: 'select',
       hideInTable: true,
       request: () =>
-        service.queryGatewayList().then((resp) =>
+        service.queryGatewayList().then((resp: any) =>
           resp.result.map((item: any) => ({
             label: item.name,
             value: `accessId is ${item.id}`,

+ 19 - 2
src/pages/device/Instance/service.ts

@@ -259,8 +259,14 @@ class Service extends BaseService<DeviceInstance> {
       method: 'GET',
     });
 
-  //接入方式
-  public queryGatewayList = () => request(`/${SystemConst.API_BASE}/gateway/device/providers`);
+  //接入网关
+  public queryGatewayList = () =>
+    request(`/${SystemConst.API_BASE}/gateway/device/_query/no-paging`, {
+      method: 'POST',
+      data: {
+        paging: false,
+      },
+    });
   // 保存设备关系
   public saveRelations = (id: string, data: any) =>
     request(`/${SystemConst.API_BASE}/device/instance/${id}/relations`, {
@@ -286,6 +292,17 @@ class Service extends BaseService<DeviceInstance> {
     request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/metric/property/${propertyId}`, {
       method: 'GET',
     });
+  //聚合查询设备属性
+  public queryPropertieInfo = (deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/agg/_query`, {
+      method: 'POST',
+      data,
+    });
+  public queryPropertieList = (deviceId: string, property: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/property/${property}/_query`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

+ 25 - 29
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -2,7 +2,13 @@ import { Button, Drawer, Dropdown, Menu, message } from 'antd';
 import { createSchemaField, observer } from '@formily/react';
 import MetadataModel from '../model';
 import type { Field, IFieldState } from '@formily/core';
-import { createForm, onFieldInit, onFieldReact, registerValidateRules } from '@formily/core';
+import {
+  createForm,
+  onFieldInit,
+  onFieldReact,
+  onFieldValueChange,
+  registerValidateRules,
+} from '@formily/core';
 import {
   ArrayItems,
   Checkbox,
@@ -57,8 +63,15 @@ const Edit = observer((props: Props) => {
   const form = useMemo(
     () =>
       createForm({
-        initialValues: MetadataModel.item as Record<string, unknown>,
+        initialValues: _.cloneDeep(MetadataModel.item as Record<string, unknown>),
         effects: () => {
+          onFieldValueChange('valueType.type', (field, form1) => {
+            if (field.modified) {
+              form1.setFieldState('expands.metrics', (state) => {
+                state.value = [];
+              });
+            }
+          });
           onFieldInit('expands.metrics.*.id', (field) => {
             const id = field as Field;
             if (id.value && !id.modified) {
@@ -96,7 +109,7 @@ const Edit = observer((props: Props) => {
           });
         },
       }),
-    [],
+    [MetadataModel.edit],
   );
 
   const schemaTitleMapping = {
@@ -838,6 +851,7 @@ const Edit = observer((props: Props) => {
                   visible:
                     props.type === 'product' &&
                     "{{['int','float','double','long','date','string','boolean'].includes($deps[0])}}",
+                  // value: []
                 },
               },
             },
@@ -969,31 +983,24 @@ const Edit = observer((props: Props) => {
     );
   };
 
-  // const param = useParams<{ id: string }>();
   const typeMap = new Map<string, any>();
 
   typeMap.set('product', productModel.current);
   typeMap.set('device', InstanceModel.detail);
-  // const saveMap = new Map<string, Promise<any>>();
   const { type } = MetadataModel;
 
   const saveMetadata = async (deploy?: boolean) => {
     setLoading(true);
-    const params = (await form.submit()) as MetadataItem;
+    let params;
+    try {
+      params = (await form.submit()) as MetadataItem;
+    } catch (e) {
+      setLoading(false);
+      return;
+    }
 
     if (!typeMap.get(props.type)) return;
 
-    // const metadata = JSON.parse(typeMap.get(props.type).metadata || '{}') as DeviceMetadata;
-    // const config = (metadata[type] || []) as MetadataItem[];
-    // const index = config.findIndex((item) => item.id === params.id);
-    // if (index > -1) {
-    //   config[index] = params;
-    //   DB.getDB().table(`${type}`).update(params.id, params);
-    // } else {
-    //   config.push(params);
-    //   DB.getDB().table(`${type}`).add(params, params.id);
-    // }
-
     const updateDB = (t: 'add' | 'update', item: MetadataItem) => {
       switch (t) {
         case 'add':
@@ -1006,18 +1013,7 @@ const Edit = observer((props: Props) => {
     };
 
     const _data = updateMetadata(type, [params], typeMap.get(props.type), updateDB);
-    // console.log(params, JSON.parse(_data.metadata));
-    // if (props.type === 'product') {
-    //   // const product = typeMap.get('product');
-    //   // @ts-ignore
-    //   // metadata[type] = config;
-    //   // product.metadata = JSON.stringify(metadata);
-    //   saveMap.set('product', service.saveProductMetadata(_data));
-    // } else {
-    //   saveMap.set('device', service.saveDeviceMetadata(param.id, { metadata: _data.metadata }));
-    // }
-    //
-    // const result = await saveMap.get(props.type);
+
     const result = await asyncUpdateMedata(props.type, _data);
     if (result.status === 200) {
       if ((window as any).onTabSaveSuccess) {

+ 129 - 0
src/pages/link/AccessConfig/Detail/Channel/index.tsx

@@ -0,0 +1,129 @@
+import { Button, Card, Col, Form, Input, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import { ProcotoleMapping } from '../Cloud/Protocol';
+import TitleComponent from '@/components/TitleComponent';
+import { getButtonPermission } from '@/utils/menu';
+import ReactMarkdown from 'react-markdown';
+import { useHistory } from 'umi';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+const Media = (props: Props) => {
+  const [form] = Form.useForm();
+  const [config, setConfig] = useState<any>({});
+  const history = useHistory();
+
+  const procotol = props.provider.id === 'modbus-tcp' ? 'modbus-tcp' : 'opc-ua';
+  const name = props.provider.id === 'modbus-tcp' ? 'Modbus' : 'OPCUA';
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    console.log(ProcotoleMapping);
+    service.getConfigView(procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  }, [props.provider]);
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div style={{ margin: '20px 30px' }}>
+        <Row gutter={24}>
+          <Col span={12}>
+            <div>
+              <TitleComponent data={'基本信息'} />
+              <Form name="basic" layout="vertical" form={form}>
+                <Form.Item
+                  label="名称"
+                  name="name"
+                  rules={[{ required: true, message: '请输入名称' }]}
+                >
+                  <Input placeholder="请输入名称" />
+                </Form.Item>
+                <Form.Item name="description" label="说明">
+                  <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+                </Form.Item>
+              </Form>
+              <div style={{ marginTop: 50 }}>
+                <Button
+                  type="primary"
+                  disabled={
+                    !!props.data.id
+                      ? getButtonPermission('link/AccessConfig', ['update'])
+                      : getButtonPermission('link/AccessConfig', ['add'])
+                  }
+                  onClick={async () => {
+                    try {
+                      const values = await form.validateFields();
+                      const param = {
+                        ...props.data,
+                        ...values,
+                        provider: props.provider?.id,
+                        protocol: procotol,
+                        transport: props.provider.id === 'modbus-tcp' ? 'MODBUS_TCP' : 'OPC_UA',
+                        channel: props.provider.id === 'modbus-tcp' ? 'modbus' : 'opc-ua',
+                      };
+                      const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                      if (resp.status === 200) {
+                        message.success('操作成功!');
+                        history.goBack();
+                      }
+                    } catch (errorInfo) {
+                      console.error('Failed:', errorInfo);
+                    }
+                  }}
+                >
+                  保存
+                </Button>
+              </div>
+            </div>
+          </Col>
+          <Col span={12}>
+            <div style={{ marginLeft: 10 }}>
+              <TitleComponent data={'配置概览'} />
+              <div>
+                <p>接入方式:{props.provider?.name || '--'}</p>
+                {props.provider?.description && <p>{props.provider?.description || '--'}</p>}
+                <p>消息协议:{procotol}</p>
+                {config?.document && (
+                  <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}</div>
+                )}
+              </div>
+              <TitleComponent data={'设备接入指引'} />
+              <div>
+                <p>1、配置{name}通道</p>
+                <p>2、创建{name}设备接入网关</p>
+                <p>3、创建产品,并选中接入方式为{name}</p>
+                <p>4、添加设备,单独为每一个设备进行数据点绑定</p>
+              </div>
+            </div>
+          </Col>
+        </Row>
+      </div>
+    </Card>
+  );
+};
+
+export default Media;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 97 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx

@@ -0,0 +1,97 @@
+import { Button, Col, Form, Input, Row, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const CTWing = (props: Props) => {
+  const [form] = Form.useForm();
+  const img = require('/public/images/network/CTWing.jpg');
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="CTWing"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://ag-api.ctwing.cn/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={12}>
+              <Form.Item
+                label="接口地址"
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appKey"
+                name="appKey"
+                rules={[{ required: true, message: '请输入appKey' }]}
+              >
+                <Input placeholder="请输入appKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appSecret"
+                name="appSecret"
+                rules={[{ required: true, message: '请输入appSecret' }]}
+              >
+                <Input placeholder="请输入appSecret" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为CTWing的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为CTWing</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通过CTWing平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.请将CTWing的AEP平台-应用管理中的App Key和App Secret复制到当前页面</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default CTWing;

+ 134 - 0
src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx

@@ -0,0 +1,134 @@
+import { TitleComponent } from '@/components';
+import { getButtonPermission } from '@/utils/menu';
+import { Button, Col, Form, Input, message, Row } from 'antd';
+import { service } from '@/pages/link/AccessConfig';
+import { useHistory } from 'umi';
+import { useEffect, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import { ProcotoleMapping } from '../Protocol';
+
+interface Props {
+  prev: () => void;
+  data: any;
+  config: any;
+  provider: any;
+  procotol: string;
+}
+
+const Finish = (props: Props) => {
+  const [form] = Form.useForm();
+  const history = useHistory();
+  const [config, setConfig] = useState<any>({});
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    service.getConfigView(props.procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  }, [props.procotol, props.provider]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={12}>
+        <div>
+          <TitleComponent data={'基本信息'} />
+          <Form name="basic" layout="vertical" form={form}>
+            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
+              <Input placeholder="请输入名称" />
+            </Form.Item>
+            <Form.Item name="description" label="说明">
+              <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+            </Form.Item>
+          </Form>
+          <div style={{ marginTop: 50 }}>
+            <Button
+              style={{ margin: '0 8px' }}
+              onClick={() => {
+                props.prev();
+              }}
+            >
+              上一步
+            </Button>
+            <Button
+              type="primary"
+              disabled={
+                !!props.data.id
+                  ? getButtonPermission('link/AccessConfig', ['update'])
+                  : getButtonPermission('link/AccessConfig', ['add'])
+              }
+              onClick={async () => {
+                try {
+                  const values = await form.validateFields();
+                  const param = {
+                    ...props.data,
+                    ...values,
+                    provider: props.provider.id,
+                    protocol: props.procotol,
+                    transport: 'HTTP_SERVER',
+                    configuration: {
+                      ...props.config,
+                    },
+                  };
+                  const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                  if (resp.status === 200) {
+                    message.success('操作成功!');
+                    history.goBack();
+                    if ((window as any).onTabSaveSuccess) {
+                      (window as any).onTabSaveSuccess(resp);
+                      setTimeout(() => window.close(), 300);
+                    }
+                  }
+                } catch (errorInfo) {
+                  console.error('Failed:', errorInfo);
+                }
+              }}
+            >
+              保存
+            </Button>
+          </div>
+        </div>
+      </Col>
+      <Col span={12}>
+        <div style={{ marginLeft: 10 }}>
+          <TitleComponent data={'配置概览'} />
+          <div>
+            <p>接入方式:{props.provider?.name || '--'}</p>
+            {props.provider?.description && <p>{props.provider?.description || '--'}</p>}
+            <p>消息协议:{props.procotol}</p>
+            {config?.document && (
+              <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}</div>
+            )}
+          </div>
+          <TitleComponent data={'设备接入指引'} />
+          <div>
+            <p>
+              1、创建类型为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}的设备接入网关
+            </p>
+            <p>
+              2、创建产品,并选中接入方式为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}
+            </p>
+            {props?.provider?.id === 'OneNet' ? (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+              </p>
+            ) : (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通
+              </p>
+            )}
+          </div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default Finish;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 130 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx

@@ -0,0 +1,130 @@
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { Button, Col, Form, Input, Row, Tooltip, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const OneNet = (props: Props) => {
+  const img = require('/public/images/network/OneNet.jpg');
+
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="onenet"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://api.heclouds.com/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={24}>
+              <Form.Item
+                label={
+                  <span>
+                    接口地址
+                    <Tooltip title={`同步物联网平台设备数据到OneNet`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item
+                label={<span>apiKey</span>}
+                name="apiKey"
+                rules={[{ required: true, message: '请输入apiKey' }]}
+              >
+                <Input placeholder="请输入apiKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    通知Token
+                    <Tooltip title={`接收OneNet推送的Token地址`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="validateToken"
+                rules={[{ required: true, message: '请输入通知Token' }]}
+              >
+                <Input placeholder="请输入通知Token" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    aesKey
+                    <Tooltip title={`OneNet 端生成的消息加密key`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="aesKey"
+              >
+                <Input placeholder="请输入aesKey" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为OneNet的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为OneNet</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.接口地址需要与OneNet数据推送配置中地址一致</div>
+          <div>2.通知Token需要与OneNet数据推送配置中Token一致</div>
+          <div>3.aesKey需要与OneNet数据推送配置中aesKey一致</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default OneNet;

+ 18 - 0
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less

@@ -0,0 +1,18 @@
+.search {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}
+
+.cardRender {
+  width: 100%;
+  background: url('/images/access.png') no-repeat;
+  background-size: 100% 100%;
+}

+ 157 - 0
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx

@@ -0,0 +1,157 @@
+import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { Button, Card, Col, Empty, Input, message, Row, Space } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import styles from './index.less';
+import PermissionButton from '@/components/PermissionButton';
+import encodeQuery from '@/utils/encodeQuery';
+
+export const ProcotoleMapping = new Map();
+ProcotoleMapping.set('websocket-server', 'WebSocket');
+ProcotoleMapping.set('http-server-gateway', 'HTTP');
+ProcotoleMapping.set('udp-device-gateway', 'UDP');
+ProcotoleMapping.set('coap-server-gateway', 'COAP');
+ProcotoleMapping.set('mqtt-client-gateway', 'MQTT');
+ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
+ProcotoleMapping.set('tcp-server-gateway', 'TCP');
+ProcotoleMapping.set('child-device', '');
+ProcotoleMapping.set('OneNet', 'OneNet');
+ProcotoleMapping.set('Ctwing', 'Ctwing');
+ProcotoleMapping.set('modbus-tcp', 'MODBUS_TCP');
+ProcotoleMapping.set('opc-ua', 'OPC_UA');
+
+interface Props {
+  provider: any;
+  data: string;
+  prev: () => void;
+  next: (data: string) => void;
+}
+
+const Protocol = (props: Props) => {
+  const [procotolList, setProcotolList] = useState<any[]>([]);
+  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+  const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+
+  const queryProcotolList = (id?: string, params?: any) => {
+    service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
+      if (resp.status === 200) {
+        setProcotolList(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    queryProcotolList(props.provider?.id);
+  }, [props.provider]);
+
+  useEffect(() => {
+    setProcotolCurrent(props.data);
+  }, [props.data]);
+
+  return (
+    <div>
+      <div className={styles.search}>
+        <Input.Search
+          key={'protocol'}
+          placeholder="请输入名称"
+          onSearch={(value: string) => {
+            queryProcotolList(
+              props.provider?.id,
+              encodeQuery({
+                terms: {
+                  name$LIKE: `%${value}%`,
+                },
+              }),
+            );
+          }}
+          style={{ width: 500, margin: '20px 0' }}
+        />
+        <PermissionButton
+          isPermission={protocolPermission.add}
+          onClick={() => {
+            const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+            const tab: any = window.open(`${origin}/#${url}?save=true`);
+            tab!.onTabSaveSuccess = (resp: any) => {
+              if (resp.status === 200) {
+                queryProcotolList(props.provider?.id);
+              }
+            };
+          }}
+          key="button"
+          type="primary"
+        >
+          新增
+        </PermissionButton>
+      </div>
+      {procotolList.length > 0 ? (
+        <Row gutter={[16, 16]}>
+          {procotolList.map((item) => (
+            <Col key={item.id} span={8}>
+              <Card
+                className={styles.cardRender}
+                style={{
+                  width: '100%',
+                  borderColor: procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
+                }}
+                hoverable
+                onClick={() => {
+                  setProcotolCurrent(item.id);
+                }}
+              >
+                <div style={{ height: '45px' }}>
+                  <div className={styles.title}>{item.name || '--'}</div>
+                  <div className={styles.desc}>{item.description || '--'}</div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      ) : (
+        <Empty
+          description={
+            <span>
+              暂无数据
+              {getButtonPermission('link/Protocol', ['add']) ? (
+                '请联系管理员进行配置'
+              ) : (
+                <Button
+                  type="link"
+                  onClick={() => {
+                    const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+                    const tab: any = window.open(`${origin}/#${url}?save=true`);
+                    tab!.onTabSaveSuccess = (resp: any) => {
+                      if (resp.status === 200) {
+                        queryProcotolList(props.provider?.id);
+                      }
+                    };
+                  }}
+                >
+                  去新增
+                </Button>
+              )}
+            </span>
+          }
+        />
+      )}
+      <Space style={{ marginTop: 20 }}>
+        <Button style={{ margin: '0 8px' }} onClick={() => props.prev()}>
+          上一步
+        </Button>
+        <Button
+          type="primary"
+          onClick={() => {
+            if (!procotolCurrent) {
+              message.error('请选择消息协议!');
+            } else {
+              props.next(procotolCurrent);
+            }
+          }}
+        >
+          下一步
+        </Button>
+      </Space>
+    </div>
+  );
+};
+
+export default Protocol;

+ 81 - 0
src/pages/link/AccessConfig/Detail/Cloud/index.less

@@ -0,0 +1,81 @@
+.box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 20px 30px;
+}
+
+.steps {
+  width: 100%;
+}
+
+.content {
+  width: 100%;
+  margin: 20px 0;
+}
+
+.action {
+  width: 100%;
+}
+
+.title {
+  width: '100%';
+  overflow: hidden;
+  font-weight: 800;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.desc {
+  width: 100%;
+  margin-top: 10px;
+  overflow: hidden;
+  color: rgba(0, 0, 0, 0.55);
+  font-weight: 400;
+  font-size: 13px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.cardContent {
+  display: flex;
+  flex-direction: column;
+  margin-top: 5px;
+  color: rgba(0, 0, 0, 0.55);
+
+  .item {
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.config {
+  padding: 10px 20px 20px 20px;
+  color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.04);
+
+  .title {
+    width: 100%;
+    margin: 10px 0;
+    font-weight: 600;
+  }
+
+  .item {
+    margin-bottom: 10px;
+
+    .context {
+      margin: 5px 0;
+      color: rgba(0, 0, 0, 0.8);
+    }
+  }
+}
+
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}

+ 117 - 0
src/pages/link/AccessConfig/Detail/Cloud/index.tsx

@@ -0,0 +1,117 @@
+import { Button, Card, Steps } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { ExclamationCircleFilled } from '@ant-design/icons';
+import OneNet from './OneNet';
+import CTWing from './CTWing';
+import Protocol from './Protocol';
+import Finish from './Finish';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+const Cloud = (props: Props) => {
+  const [current, setCurrent] = useState<number>(0);
+  const [steps] = useState<string[]>(['接入配置', '消息协议', '完成']);
+  const [config, setConfig] = useState<any>({});
+  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+
+  const prev = () => {
+    setCurrent(current - 1);
+  };
+
+  const next = (param: any) => {
+    setConfig(param);
+    setCurrent(current + 1);
+  };
+
+  useEffect(() => {
+    setCurrent(0);
+    setConfig(props.data?.configuration || {});
+    setProcotolCurrent(props.data?.protocol);
+  }, [props.data]);
+
+  const renderSteps = (cur: number) => {
+    switch (cur) {
+      case 0:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              通过{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}
+              平台的HTTP推送服务进行数据接入
+            </div>
+            <div style={{ marginTop: 10 }}>
+              {props?.provider?.id === 'OneNet' ? (
+                <OneNet data={config} next={(param: any) => next(param)} />
+              ) : (
+                <CTWing data={config} next={(param: any) => next(param)} />
+              )}
+            </div>
+          </div>
+        );
+      case 1:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              只能选择HTTP通信方式的协议
+            </div>
+            <div style={{ marginTop: 10 }}>
+              <Protocol
+                data={procotolCurrent}
+                provider={props.provider}
+                next={(param: string) => {
+                  setProcotolCurrent(param);
+                  setCurrent(current + 1);
+                }}
+                prev={prev}
+              />
+            </div>
+          </div>
+        );
+      case 2:
+        return (
+          <Finish
+            procotol={procotolCurrent}
+            provider={props.provider}
+            data={props.data}
+            config={config}
+            prev={prev}
+          />
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div className={styles.box}>
+        <div className={styles.steps}>
+          <Steps size="small" current={current}>
+            {steps.map((item) => (
+              <Steps.Step key={item} title={item} />
+            ))}
+          </Steps>
+        </div>
+        <div className={styles.content}>{renderSteps(current)}</div>
+      </div>
+    </Card>
+  );
+};
+
+export default Cloud;

+ 0 - 15
src/pages/link/AccessConfig/Detail/Provider/index.less

@@ -27,18 +27,3 @@
   white-space: nowrap;
   text-overflow: ellipsis;
 }
-
-.title {
-  width: 100%;
-  margin-bottom: 10px;
-  overflow: hidden;
-  font-weight: 800;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-}
-
-.title::before {
-  margin-right: 10px;
-  background-color: #2810ff;
-  content: '|';
-}

+ 70 - 83
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -1,5 +1,6 @@
-import { Button, Card, Col, Row } from 'antd';
 import { useEffect, useState } from 'react';
+import { TitleComponent } from '@/components';
+import { Button, Card, Col, Row } from 'antd';
 import styles from './index.less';
 
 interface Props {
@@ -9,109 +10,95 @@ interface Props {
 
 const Provider = (props: Props) => {
   const [dataSource, setDataSource] = useState<any[]>([]);
-  const [mediaSource, setMediaSource] = useState<any[]>([]);
 
   useEffect(() => {
     const media: any[] = [];
-    const data: any = [];
+    const network: any[] = [];
+    const cloud: any[] = [];
+    const channel: any[] = [];
     (props?.data || []).map((item: any) => {
       if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
         media.push(item);
+      } else if (item.id === 'OneNet' || item.id === 'Ctwing') {
+        cloud.push(item);
+      } else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
+        channel.push(item);
       } else {
-        data.push(item);
+        network.push(item);
       }
     });
-    setDataSource(data);
-    setMediaSource(media);
+
+    setDataSource([
+      {
+        type: 'network',
+        list: [...network],
+        title: '自定义设备接入',
+      },
+      {
+        type: 'media',
+        list: [...media],
+        title: '视频类设备接入',
+      },
+      {
+        type: 'cloud',
+        list: [...cloud],
+        title: '云平台接入',
+      },
+      {
+        type: 'channel',
+        list: [...channel],
+        title: '通道类设备接入',
+      },
+    ]);
   }, [props.data]);
 
   return (
-    <>
-      <Card>
-        <div className={styles.title}>自定义设备接入</div>
-        <Row gutter={[16, 16]}>
-          {dataSource.map((item) => (
-            <Col key={item.name} span={12}>
-              <Card style={{ width: '100%' }} hoverable>
-                <div
-                  style={{
-                    width: '100%',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'space-between',
-                  }}
-                >
+    <div>
+      {dataSource.map((i) => (
+        <Card key={i.type} style={{ marginTop: 20 }}>
+          <TitleComponent data={i.title} />
+          <Row gutter={[16, 16]}>
+            {(i?.list || []).map((item: any) => (
+              <Col key={item.name} span={12}>
+                <Card style={{ width: '100%' }} hoverable>
                   <div
                     style={{
+                      width: '100%',
                       display: 'flex',
-                      width: 'calc(100% - 70px)',
+                      alignItems: 'center',
+                      justifyContent: 'space-between',
                     }}
                   >
-                    <div className={styles.images}>{item.name}</div>
-                    <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
-                      <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item?.description || '--'}</div>
-                    </div>
-                  </div>
-                  <div style={{ width: '70px' }}>
-                    <Button
-                      type="primary"
-                      onClick={() => {
-                        props.change(item, 'network');
+                    <div
+                      style={{
+                        display: 'flex',
+                        width: 'calc(100% - 70px)',
                       }}
                     >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-      <Card style={{ marginTop: 20 }}>
-        <div className={styles.title}>视频类设备接入</div>
-        <Row gutter={[16, 16]}>
-          {mediaSource.map((item) => (
-            <Col key={item.name} span={12}>
-              <Card style={{ width: '100%' }} hoverable>
-                <div
-                  style={{
-                    width: '100%',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'space-between',
-                  }}
-                >
-                  <div
-                    style={{
-                      display: 'flex',
-                      width: 'calc(100% - 70px)',
-                    }}
-                  >
-                    <div className={styles.images}>{item.name}</div>
-                    <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
-                      <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description || '--'}</div>
+                      <div className={styles.images}>{item.name}</div>
+                      <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
+                        <div style={{ fontWeight: 600 }}>{item.name}</div>
+                        <div className={styles.desc}>{item?.description || '--'}</div>
+                      </div>
+                    </div>
+                    <div style={{ width: '70px' }}>
+                      <Button
+                        type="primary"
+                        onClick={() => {
+                          props.change(item, i.type);
+                        }}
+                      >
+                        接入
+                      </Button>
                     </div>
                   </div>
-                  <div style={{ width: '70px' }}>
-                    <Button
-                      type="primary"
-                      onClick={() => {
-                        props.change(item, 'media');
-                      }}
-                    >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-    </>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        </Card>
+      ))}
+    </div>
   );
 };
 

+ 36 - 2
src/pages/link/AccessConfig/Detail/index.tsx

@@ -6,6 +6,8 @@ import Provider from './Provider';
 import Media from './Media';
 import { service } from '@/pages/link/AccessConfig';
 import { Spin } from 'antd';
+import Cloud from './Cloud';
+import Channel from './Channel';
 
 type LocationType = {
   id?: string;
@@ -17,7 +19,9 @@ const Detail = () => {
   const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<any>({});
   const [provider, setProvider] = useState<any>({});
-  const [type, setType] = useState<'media' | 'network' | undefined>(undefined);
+  const [type, setType] = useState<'media' | 'network' | 'cloud' | 'channel' | undefined>(
+    undefined,
+  );
 
   const [dataSource, setDataSource] = useState<any[]>([]);
 
@@ -41,6 +45,16 @@ const Detail = () => {
               response.result?.provider === 'gb28181-2016'
             ) {
               setType('media');
+            } else if (
+              response.result?.provider === 'Ctwing' ||
+              response.result?.provider === 'OneNet'
+            ) {
+              setType('cloud');
+            } else if (
+              response.result?.provider === 'modbus-tcp' ||
+              response.result?.provider === 'opc-ua'
+            ) {
+              setType('channel');
             } else {
               setType('network');
             }
@@ -80,6 +94,26 @@ const Detail = () => {
             }}
           />
         );
+      case 'cloud':
+        return (
+          <Cloud
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
+      case 'channel':
+        return (
+          <Channel
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
       default:
         return null;
     }
@@ -91,7 +125,7 @@ const Detail = () => {
         {visible ? (
           <Provider
             data={dataSource}
-            change={(param: any, typings: 'media' | 'network') => {
+            change={(param: any, typings: 'media' | 'network' | 'cloud' | 'channel') => {
               setType(typings);
               setProvider(param);
               setData({});

+ 90 - 1
src/pages/notice/Config/Detail/index.tsx

@@ -53,6 +53,9 @@ export const docMap = {
   email: {
     embedded: <Email />,
   },
+  webhook: {
+    http: <div>webhook</div>,
+  },
 };
 
 const Detail = observer(() => {
@@ -87,6 +90,9 @@ const Detail = observer(() => {
   );
 
   useEffect(() => {
+    if (id === 'webhook') {
+      setProvider('http');
+    }
     if (state.current) {
       form.setValues(state.current);
     }
@@ -152,7 +158,7 @@ const Detail = observer(() => {
         },
         required: true,
         'x-visible': typeList[id]?.length > 0,
-        'x-hidden': id === 'email',
+        'x-hidden': id === 'email' || id === 'webhook',
         'x-value': typeList[id][0]?.value,
         enum: typeList[id] || [],
       },
@@ -452,6 +458,89 @@ const Detail = observer(() => {
               },
             },
           },
+          webhook: {
+            'x-visible': id === 'webhook',
+            type: 'void',
+            properties: {
+              url: {
+                title: 'Webhook',
+                required: true,
+                'x-component-props': {
+                  placeholder: '请输入Webhook',
+                },
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              headers: {
+                title: '请求头',
+                type: 'array',
+                'x-decorator': 'FormItem',
+                'x-component': 'ArrayTable',
+                'x-component-props': {
+                  pagination: { pageSize: 9999 },
+                  // scroll: {x: '100%'},
+                },
+                items: {
+                  type: 'object',
+                  properties: {
+                    column1: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Column',
+                      'x-component-props': { width: 200, title: 'KEY' },
+                      properties: {
+                        key: {
+                          type: 'string',
+                          // 'x-decorator': 'Editable',
+                          'x-component': 'Input',
+                        },
+                      },
+                    },
+                    column2: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Column',
+                      'x-component-props': { width: 200, title: 'VALUE' },
+                      properties: {
+                        value: {
+                          type: 'string',
+                          // 'x-decorator': 'Editable',
+                          'x-component': 'Input',
+                        },
+                      },
+                    },
+                    column3: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Column',
+                      'x-component-props': {
+                        title: '操作',
+                        dataIndex: 'operations',
+                        width: 50,
+                        fixed: 'right',
+                      },
+                      properties: {
+                        item: {
+                          type: 'void',
+                          'x-component': 'FormItem',
+                          properties: {
+                            remove: {
+                              type: 'void',
+                              'x-component': 'ArrayTable.Remove',
+                            },
+                          },
+                        },
+                      },
+                    },
+                  },
+                },
+                properties: {
+                  add: {
+                    type: 'void',
+                    'x-component': 'ArrayTable.Addition',
+                    title: '添加条目',
+                  },
+                },
+              },
+            },
+          },
         },
       },
       description: {

+ 43 - 9
src/pages/notice/Template/Debug/index.tsx

@@ -1,5 +1,5 @@
 import { message, Modal } from 'antd';
-import { useEffect, useMemo } from 'react';
+import { useEffect, useMemo, useRef } from 'react';
 import { createForm, Field, onFieldReact, onFieldValueChange } from '@formily/core';
 import { createSchemaField, observer } from '@formily/react';
 import {
@@ -23,6 +23,8 @@ const Debug = observer(() => {
   const location = useLocation<{ id: string }>();
   const id = (location as any).query?.id;
 
+  const variableRef = useRef<any>([]);
+
   const form = useMemo(
     () =>
       createForm({
@@ -38,9 +40,13 @@ const Debug = observer(() => {
             }
           });
 
-          onFieldReact('variableDefinitions.*.type', (field) => {
+          onFieldReact('variableDefinitions.*.type', async (field) => {
             const value = (field as Field).value;
             const format = field.query('.value').take() as Field;
+            const _id = field.query('.id').take() as Field;
+
+            const configId = field.query('configId');
+
             if (format && value) {
               switch (value) {
                 case 'date':
@@ -64,6 +70,27 @@ const Debug = observer(() => {
                   break;
               }
             }
+            if (variableRef.current) {
+              const a = variableRef.current?.find((i: any) => i.id === _id.value);
+              const _configId = configId.value();
+              const businessType = a?.expands?.businessType;
+              if (id === 'dingTalk' && _configId) {
+                switch (businessType) {
+                  case 'org':
+                    // 获取org
+                    const orgList = await service.dingTalk.getDepartments(_configId);
+                    format.setComponent(Select);
+                    format.setDataSource(orgList);
+                    break;
+                  case 'user':
+                    // 获取user
+                    const userList = await service.dingTalk.getUser(_configId);
+                    format.setComponent(Select);
+                    format.setDataSource(userList);
+                    break;
+                }
+              }
+            }
           });
         },
       }),
@@ -71,13 +98,19 @@ const Debug = observer(() => {
   );
 
   useEffect(() => {
-    const data = state.current;
-    if (data?.variableDefinitions?.length > 0) {
-      form.setFieldState('variableDefinitions', (state1) => {
-        state1.visible = true;
-        state1.value = data?.variableDefinitions;
-      });
-    }
+    // const data = state.current;
+    // 从后端接口来获取变量参数
+    service.getVariableDefinitions(state.current?.id || '').then((resp) => {
+      const _template = resp.result;
+      console.log(resp, 'userEfffect', state.current);
+      if (_template?.variableDefinitions?.length > 0) {
+        variableRef.current = _template?.variableDefinitions;
+        form.setFieldState('variableDefinitions', (state1) => {
+          state1.visible = true;
+          state1.value = _template?.variableDefinitions;
+        });
+      }
+    });
   }, [state.current, state.debug]);
 
   const SchemaField = createSchemaField({
@@ -117,6 +150,7 @@ const Debug = observer(() => {
         title: '通知配置',
         type: 'string',
         required: true,
+        default: state?.current?.configId,
         'x-decorator': 'FormItem',
         'x-component': 'Select',
         'x-reactions': '{{useAsyncDataSource(getConfig)}}',

+ 85 - 15
src/pages/notice/Template/Detail/index.tsx

@@ -46,6 +46,7 @@ import { Store } from 'jetlinks-store';
 import FAutoComplete from '@/components/FAutoComplete';
 import { PermissionButton } from '@/components';
 import usePermissions from '@/hooks/permission';
+import FMonacoEditor from '@/components/FMonacoEditor';
 
 export const docMap = {
   weixin: {
@@ -65,6 +66,9 @@ export const docMap = {
   email: {
     embedded: <Email />,
   },
+  webhook: {
+    http: <div>webhook</div>,
+  },
 };
 
 const Detail = observer(() => {
@@ -242,7 +246,7 @@ const Detail = observer(() => {
               });
             }
           });
-          onFieldValueChange('template.*(subject,markdown.title)', (field, form1) => {
+          onFieldValueChange('template.*(subject,markdown.title,link.title)', (field, form1) => {
             const value = (field as Field).value;
             const _message = field.query('template.message').value();
 
@@ -406,6 +410,7 @@ const Detail = observer(() => {
       FormGrid,
       ArrayTable,
       FAutoComplete,
+      FMonacoEditor,
     },
   });
 
@@ -460,13 +465,15 @@ const Detail = observer(() => {
     batchCheckEmail(value) {
       const regEmail = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
       let error;
-      value.some((item: string) => {
-        if (!regEmail.test(item)) {
-          error = item;
-          return true;
-        }
-        return false;
-      });
+      if (value) {
+        value.some((item: string) => {
+          if (!regEmail.test(item)) {
+            error = item;
+            return true;
+          }
+          return false;
+        });
+      }
       return error ? `${error}邮件格式错误` : '';
     },
   });
@@ -509,7 +516,7 @@ const Detail = observer(() => {
         },
         required: true,
         'x-visible': typeList[id]?.length > 0,
-        'x-hidden': id === 'email',
+        'x-hidden': id === 'email' || id === 'webhook',
         'x-value': typeList[id][0]?.value,
         enum: typeList[id] || [],
       },
@@ -1116,6 +1123,7 @@ const Detail = observer(() => {
                         },
                         gridSpan: 23,
                       },
+                      required: true,
                       'x-component-props': {
                         type: 'file',
                         display: 'name',
@@ -1142,6 +1150,68 @@ const Detail = observer(() => {
               },
             },
           },
+          webhook: {
+            type: 'void',
+            'x-visible': id === 'webhook',
+            properties: {
+              contextAsBody: {
+                title: '请求体',
+                type: 'boolean',
+                'x-component': 'Radio.Group',
+                'x-decorator': 'FormItem',
+                default: true,
+                enum: [
+                  { label: '默认', value: true },
+                  { label: '自定义', value: false },
+                ],
+              },
+              body: {
+                'x-decorator': 'FormItem',
+                'x-component': 'FMonacoEditor',
+                required: true,
+                'x-component-props': {
+                  height: 250,
+                  theme: 'vs',
+                  language: 'json',
+                  editorDidMount: (editor1: any) => {
+                    editor1.onDidScrollChange?.(() => {
+                      editor1.getAction('editor.action.formatDocument').run();
+                    });
+                  },
+                },
+                // 'x-decorator-props': {
+                //   style: {
+                //     zIndex: 9998,
+                //   },
+                // },
+                'x-reactions': {
+                  dependencies: ['.contextAsBody'],
+                  fulfill: {
+                    state: {
+                      visible: '{{$deps[0]===false}}',
+                    },
+                  },
+                },
+              },
+              defaultBody: {
+                'x-decorator': 'FormItem',
+                'x-component': 'Input.TextArea',
+                'x-component-props': {
+                  rows: 3,
+                  placeholder: '请求体中的数据来自于发送通知时指定的所有变量',
+                },
+                'x-disabled': true,
+                'x-reactions': {
+                  dependencies: ['.contextAsBody'],
+                  fulfill: {
+                    state: {
+                      visible: '{{$deps[0]===true}}',
+                    },
+                  },
+                },
+              },
+            },
+          },
         },
       },
       'template.message': {
@@ -1156,7 +1226,7 @@ const Detail = observer(() => {
           dependencies: ['provider'],
           fulfill: {
             state: {
-              hidden: '{{$deps[0]==="aliyun"}}',
+              hidden: '{{$deps[0]==="aliyun"||$deps[0]==="http"}}',
               disabled: '{{["aliyunSms","aliyun"].includes($deps[0])}}',
             },
           },
@@ -1251,11 +1321,11 @@ const Detail = observer(() => {
         'x-component-props': {
           rows: 4,
         },
-        'x-decorator-props': {
-          style: {
-            zIndex: 998,
-          },
-        },
+        // 'x-decorator-props': {
+        //   style: {
+        //     zIndex: 998,
+        //   },
+        // },
       },
     },
   };

+ 5 - 0
src/pages/notice/Template/service.ts

@@ -42,6 +42,11 @@ class Service extends BaseService<TemplateItem> {
       method: 'POST',
     });
 
+  public getVariableDefinitions = (templateId: string) =>
+    request(`${SystemConst.API_BASE}/notifier/template/${templateId}/detail`, {
+      method: 'GET',
+    });
+
   dingTalk = {
     getDepartments: (id: string) =>
       request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments`).then(

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

@@ -8,6 +8,7 @@ type TemplateItem = {
   createTime: number;
   variableDefinitions: any;
   description: string;
+  configId?: string;
 };
 
 type LogItem = {

+ 14 - 0
src/pages/notice/index.tsx

@@ -20,6 +20,7 @@ const dingTalkMessage = require('/public/images/notice/dingTalk-message.png');
 const dingTalkRebot = require('/public/images/notice/dingTalk-rebot.png');
 const sms = require('/public/images/notice/sms.png');
 const vocie = require('/public/images/notice/voice.png');
+const webhook = require('/public/images/notice/webhook.png');
 
 export const typeList = {
   weixin: [
@@ -60,6 +61,12 @@ export const typeList = {
       label: '默认',
     },
   ],
+  webhook: [
+    {
+      label: createImageLabel(webhook, 'Webhook'),
+      value: 'http',
+    },
+  ],
 };
 
 const Type = observer(() => {
@@ -89,6 +96,11 @@ const Type = observer(() => {
       name: '短信',
       describe: '支持阿里云短信消息类型',
     },
+    {
+      type: 'webhook',
+      name: 'webhook',
+      describe: '支持websocket消息通知',
+    },
   ];
 
   const iconMap = new Map();
@@ -97,6 +109,7 @@ const Type = observer(() => {
   iconMap.set('email', require('/public/images/notice/email.png'));
   iconMap.set('voice', require('/public/images/notice/voice.png'));
   iconMap.set('sms', require('/public/images/notice/sms.png'));
+  iconMap.set('webhook', require('/public/images/notice/webhook.png'));
 
   const bGroundMap = new Map();
   bGroundMap.set('dingTalk', require('/public/images/notice/dingtalk-background.png'));
@@ -104,6 +117,7 @@ const Type = observer(() => {
   bGroundMap.set('email', require('/public/images/notice/email-background.png'));
   bGroundMap.set('voice', require('/public/images/notice/voice-background.png'));
   bGroundMap.set('sms', require('/public/images/notice/sms-background.png'));
+  bGroundMap.set('webhook', require('/public/images/notice/webhook-backgroud.png'));
 
   return (
     <PageContainer

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

@@ -1,5 +1,5 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Button, Card, Col, Divider, Image, message, Row, Table } from 'antd';
+import { Button, Card, Col, Divider, Image, message, Row, Table, Tooltip } from 'antd';
 import TitleComponent from '@/components/TitleComponent';
 import { createSchemaField } from '@formily/react';
 import { ArrayItems, Form, FormButtonGroup, FormGrid, FormItem, Input } from '@formily/antd';
@@ -11,6 +11,7 @@ import type { IOConfigItem } from '@/pages/rule-engine/Alarm/Config/typing';
 import Service from '@/pages/rule-engine/Alarm/Config/service';
 import styles from './index.less';
 import ReactMarkdown from 'react-markdown';
+import { QuestionCircleOutlined } from '@ant-design/icons';
 
 export const service = new Service('alarm/config');
 const ioImg = require('/public/images/alarm/io.png');
@@ -410,7 +411,7 @@ const Config = () => {
     <Row>
       <Col span={14}>
         <Card>
-          <TitleComponent data="告警级别配置" />
+          <TitleComponent data={'告警级别配置'} />
           <Form form={levelForm}>
             <SchemaField schema={levelSchema} />
             <FormButtonGroup.Sticky>
@@ -439,12 +440,30 @@ const Config = () => {
       <Col span={14}>
         <div>
           <Card>
-            <TitleComponent data="告警数据输出" />
+            <TitleComponent
+              data={
+                <span>
+                  告警数据输出
+                  <Tooltip title={'将告警数据输出到其他第三方系统'}>
+                    <QuestionCircleOutlined style={{ marginLeft: 5 }} />
+                  </Tooltip>
+                </span>
+              }
+            />
             <Form form={outputForm} layout="vertical">
               <SchemaField schema={ioSchema} />
             </Form>
             <Divider />
-            <TitleComponent data="告警处理结果输入" />
+            <TitleComponent
+              data={
+                <span>
+                  告警处理结果输入
+                  <Tooltip title={'接收第三方系统处理的告警结果'}>
+                    <QuestionCircleOutlined style={{ marginLeft: 5 }} />
+                  </Tooltip>
+                </span>
+              }
+            />
             <Form form={inputForm} layout="vertical">
               <SchemaField schema={ioSchema} />
               <FormButtonGroup.Sticky>
@@ -487,7 +506,11 @@ const Config = () => {
   );
 
   const list = [
-    { key: 'config', tab: '告警级别', component: level },
+    {
+      key: 'config',
+      tab: '告警级别',
+      component: level,
+    },
     { key: 'io', tab: '数据流转', component: io },
   ];
 

+ 22 - 16
src/pages/rule-engine/Alarm/Configuration/Save/index.tsx

@@ -196,6 +196,13 @@ const Save = (props: Props) => {
             message: '请选择关联触发场景',
           },
         ],
+        'x-component-props': {
+          placeholder: '请选择关联触发场景',
+          showSearch: true,
+          showArrow: true,
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
         'x-decorator-props': {
           gridSpan: 1,
           addonAfter: (
@@ -204,28 +211,27 @@ const Save = (props: Props) => {
               style={{ padding: 0 }}
               isPermission={true}
               onClick={() => {
-                // const tab: any = window.open(`${origin}/#/system/department?save=true`);
-                // tab!.onTabSaveSuccess = (value: any) => {
-                //   form.setFieldState('orgIdList', async (state) => {
-                // state.dataSource = await getOrg().then((resp) =>
-                //   resp.result?.map((item: Record<string, unknown>) => ({
-                //     ...item,
-                //     label: item.name,
-                //     value: item.id,
-                //   })),
-                // );
-                // state.value = [...(state.value || []), value.id];
-                // });
-                // };
+                const tab: any = window.open(`${origin}/#/iot/rule-engine/scene/Save`);
+                tab!.onTabSaveSuccess = (value: any) => {
+                  form.setFieldState('sceneId', async (state) => {
+                    state.dataSource = await getScene();
+                    // .then((resp) =>
+                    //   resp.result?.map((item: Record<string, unknown>) => ({
+                    //     ...item,
+                    //     label: item.name,
+                    //     value: item.id,
+                    //   })),
+                    // );
+                    console.log(value, 'value');
+                    state.value = value?.result?.id;
+                  });
+                };
               }}
             >
               <PlusOutlined />
             </PermissionButton>
           ),
         },
-        'x-component-props': {
-          placeholder: '请选择关联触发场景',
-        },
       },
       description: {
         title: '说明',

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

@@ -59,7 +59,7 @@ const SolveLog = (props: Props) => {
       },
     },
     {
-      dataIndex: 'createTime',
+      dataIndex: 'alarmTime',
       title: '告警时间',
       valueType: 'dateTime',
       // render: (text: any, record: any) => <span>{moment(record.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>,

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

@@ -43,7 +43,7 @@ const TabComponent = observer((props: Props) => {
   const columns: ProColumns<any>[] = [
     {
       title: '名称',
-      dataIndex: 'name',
+      dataIndex: 'alarmName',
     },
     {
       title: '最近告警时间',

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

@@ -15,6 +15,7 @@ interface BuiltInProps {
   value?: ChangeType;
   data?: any;
   type?: string;
+  notifyType?: string;
   onChange?: (value: ChangeType) => void;
 }
 

+ 152 - 52
src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx

@@ -1,7 +1,7 @@
 // 收信人
 import { useEffect, useState } from 'react';
 import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
-import { Select } from 'antd';
+import { Input, Select } from 'antd';
 import {
   queryDingTalkUsers,
   queryPlatformUsers,
@@ -33,7 +33,6 @@ export default (props: UserProps) => {
     setSource(props.value?.source);
     if (props.value?.source === 'relation') {
       const relation = props.value?.relation;
-      console.log(relation);
       if (relation) {
         if (relation.objectId) {
           // 平台用户
@@ -84,7 +83,7 @@ export default (props: UserProps) => {
   };
 
   useEffect(() => {
-    if (source === 'fixed') {
+    if (source === 'fixed' && ['dingTalk', 'weixin'].includes(props.notifyType)) {
       // 钉钉,微信用户
       getRelationUsers(props.notifyType, props.configId);
     } else {
@@ -97,6 +96,14 @@ export default (props: UserProps) => {
     { label: props.notifyType === 'dingTalk' ? '钉钉用户' : '微信用户', value: 'fixed' },
   ];
 
+  const otherOptions = [
+    { label: '平台用户', value: 'relation' },
+    {
+      label: props.notifyType && props.notifyType === 'email' ? '固定邮箱' : '固定号码',
+      value: 'fixed',
+    },
+  ];
+
   /**
    * 收信人-平台用户格式
    * {
@@ -168,65 +175,158 @@ export default (props: UserProps) => {
     return option.children ? option.children.toLowerCase().includes(input.toLowerCase()) : false;
   };
 
+  const userSelect =
+    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>
+    );
+
+  const emailSelect =
+    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>
+    ) : (
+      <Input
+        value={value}
+        placeholder={'请输入固定邮箱'}
+        onChange={(e) => {
+          onchange(source, e.target.value);
+        }}
+      />
+    );
+
+  const voiceSelect =
+    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>
+    ) : (
+      <Input
+        value={value}
+        placeholder={'请输入固定号码'}
+        onChange={(e) => {
+          onchange(source, e.target.value);
+        }}
+      />
+    );
+
   return (
     <ItemGroup compact>
       <Select
         value={source}
-        options={options}
+        options={
+          props.notifyType && ['dingTalk', 'weixin'].includes(props.notifyType)
+            ? options
+            : otherOptions
+        }
         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>
-      )}
+      {props.notifyType && ['dingTalk', 'weixin'].includes(props.notifyType) ? userSelect : null}
+      {props.notifyType && ['email'].includes(props.notifyType) ? emailSelect : null}
+      {props.notifyType && ['sms', 'voice'].includes(props.notifyType) ? voiceSelect : null}
     </ItemGroup>
   );
 };

+ 45 - 7
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -144,7 +144,11 @@ export default observer((props: ActionProps) => {
   const MessageNodes = (
     <>
       <Col span={7}>
-        <Form.Item {...props.restField} name={[name, 'notify', 'notifyType']}>
+        <Form.Item
+          {...props.restField}
+          name={[name, 'notify', 'notifyType']}
+          rules={[{ required: true, message: '请选择通知方式' }]}
+        >
           <Select
             options={messageType}
             fieldNames={{ value: 'id', label: 'name' }}
@@ -159,7 +163,11 @@ export default observer((props: ActionProps) => {
         </Form.Item>
       </Col>
       <Col span={7}>
-        <Form.Item {...props.restField} name={[name, 'notify', 'notifierId']}>
+        <Form.Item
+          {...props.restField}
+          name={[name, 'notify', 'notifierId']}
+          rules={[{ required: true, message: '请选择通知配置' }]}
+        >
           <Select
             options={messageConfig}
             loading={messageConfigLoading}
@@ -174,7 +182,11 @@ export default observer((props: ActionProps) => {
         </Form.Item>
       </Col>
       <Col span={6}>
-        <Form.Item {...props.restField} name={[name, 'notify', 'templateId']}>
+        <Form.Item
+          {...props.restField}
+          name={[name, 'notify', 'templateId']}
+          rules={[{ required: true, message: '请选择通知模板' }]}
+        >
           <Select
             options={messageTemplate}
             loading={messageTemplateLoading}
@@ -210,7 +222,11 @@ export default observer((props: ActionProps) => {
       </div>
       <Row gutter={24}>
         <Col span={4}>
-          <Form.Item {...props.restField} name={[name, 'executor']}>
+          <Form.Item
+            {...props.restField}
+            name={[name, 'executor']}
+            rules={[{ required: true, message: '请选择执行条件' }]}
+          >
             <Select
               options={[
                 { label: '消息通知', value: 'notify' },
@@ -257,7 +273,23 @@ export default observer((props: ActionProps) => {
       {type1 === 'device' &&
       deviceMessageType === MessageTypeEnum.WRITE_PROPERTY &&
       properties.length ? (
-        <Form.Item name={[name, 'device', 'message', 'properties']}>
+        <Form.Item
+          name={[name, 'device', 'message', 'properties']}
+          rules={[
+            {
+              validator: async (_: any, value: any) => {
+                if (value) {
+                  if (!Object.values(value)[0]) {
+                    return Promise.reject(new Error('请输入属性值'));
+                  }
+                } else {
+                  return Promise.reject(new Error('请选择属性'));
+                }
+                return Promise.resolve();
+              },
+            },
+          ]}
+        >
           <WriteProperty properties={properties} type={props.triggerType} form={props.form} />
         </Form.Item>
       ) : null}
@@ -266,7 +298,10 @@ export default observer((props: ActionProps) => {
       properties.length ? (
         <Row gutter={24}>
           <Col span={4}>
-            <Form.Item name={[name, 'device', 'message', 'properties']}>
+            <Form.Item
+              name={[name, 'device', 'message', 'properties']}
+              rules={[{ required: true, message: '请选择属性' }]}
+            >
               <ReadProperty properties={properties} />
             </Form.Item>
           </Col>
@@ -275,7 +310,10 @@ export default observer((props: ActionProps) => {
       {type1 === 'device' &&
       deviceMessageType === MessageTypeEnum.INVOKE_FUNCTION &&
       functionList.length ? (
-        <Form.Item name={[name, 'device', 'message', 'inputs']}>
+        <Form.Item
+          name={[name, 'device', 'message', 'inputs']}
+          rules={[{ required: true, message: '请输入功能值' }]}
+        >
           <FunctionCall functionData={functionList} />
         </Form.Item>
       ) : null}

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

@@ -108,8 +108,6 @@ export default (props: DeviceModelProps) => {
     },
   ];
 
-  console.log(selectKeys);
-
   return (
     <>
       {visible && (

+ 2 - 1
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -24,6 +24,7 @@ export default (props: FunctionCallProps) => {
 
   useEffect(() => {
     setEditableRowKeys(props.functionData.map((d) => d.id));
+    console.log('functionData', props.functionData);
     formRef.current?.setFieldsValue({
       table: props.functionData,
     });
@@ -121,7 +122,6 @@ export default (props: FunctionCallProps) => {
       align: 'center',
       width: 260,
       renderFormItem: (_, row: any) => {
-        console.log('functionCall', row.record);
         return getItemNode(row.record);
       },
     },
@@ -130,6 +130,7 @@ export default (props: FunctionCallProps) => {
   return (
     <ProForm<{ table: FunctionTableDataType[] }>
       formRef={formRef}
+      name={'proForm'}
       submitter={false}
       onValuesChange={() => {
         const values = formRef.current?.getFieldsValue();

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

@@ -155,7 +155,10 @@ export default (props: DeviceProps) => {
   return (
     <>
       <Col span={5}>
-        <Form.Item name={[name, 'device', 'productId']}>
+        <Form.Item
+          name={[name, 'device', 'productId']}
+          rules={[{ required: true, message: '请选择产品' }]}
+        >
           <Select
             showSearch
             options={productList}
@@ -185,17 +188,29 @@ export default (props: DeviceProps) => {
             <Select options={sourceList} style={{ width: 120 }} />
           </Form.Item>
           {selector === SourceEnum.fixed && (
-            <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+            <Form.Item
+              name={[name, 'device', 'selectorValues']}
+              {...props.restField}
+              rules={[{ required: true, message: '请选择设备' }]}
+            >
               <Device productId={productId} />
             </Form.Item>
           )}
           {selector === SourceEnum.tag && (
-            <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+            <Form.Item
+              name={[name, 'device', 'selectorValues']}
+              {...props.restField}
+              rules={[{ required: true, message: '请选择标签' }]}
+            >
               <TagModal tagData={tagList} />
             </Form.Item>
           )}
           {selector === SourceEnum.relation && (
-            <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+            <Form.Item
+              name={[name, 'device', 'selectorValues']}
+              {...props.restField}
+              rules={[{ required: true, message: '请选择关系人' }]}
+            >
               <Select style={{ width: 300 }} />
             </Form.Item>
           )}
@@ -223,7 +238,11 @@ export default (props: DeviceProps) => {
       </Col>
       <Col span={4}>
         {messageType === MessageTypeEnum.INVOKE_FUNCTION ? (
-          <Form.Item name={[name, 'device', 'message', 'functionId']} {...props.restField}>
+          <Form.Item
+            name={[name, 'device', 'message', 'functionId']}
+            {...props.restField}
+            rules={[{ required: true, message: '请选择功能' }]}
+          >
             <Select
               showSearch
               options={functionList}

+ 84 - 16
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -1,5 +1,5 @@
 import type { FormInstance } from 'antd';
-import { Col, Form, Row } from 'antd';
+import { Col, Form, Input, Row } from 'antd';
 import {
   BuiltIn,
   OrgList,
@@ -7,6 +7,7 @@ import {
   UserList,
 } from '@/pages/rule-engine/Scene/Save/action/VariableItems';
 import { InputFile } from '@/pages/rule-engine/Scene/Save/components';
+import { useCallback } from 'react';
 
 interface MessageContentProps {
   name: number;
@@ -20,6 +21,85 @@ interface MessageContentProps {
 const rowGutter = 12;
 
 export default (props: MessageContentProps) => {
+  const getRules = useCallback(
+    (item: any, type: string): any[] => {
+      const rules = [];
+      if (item.required) {
+        if (type === 'user') {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              console.log('user', value);
+              if (!value) {
+                return Promise.reject(new Error('请选择' + item.name));
+              } else {
+                if (value.source === 'fixed' && !value.value) {
+                  return Promise.reject(new Error('请输入' + item.name));
+                } else if (value.source === 'relation' && !value.value && !value.relation) {
+                  return Promise.reject(new Error('请选择' + item.name));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        } else {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (type === 'file' && !value) {
+                return Promise.reject(new Error('请输入' + item.name));
+              } else {
+                if (!value || !value.value) {
+                  if (['date', 'org'].includes(type)) {
+                    return Promise.reject(new Error('请选择' + item.name));
+                  } else {
+                    return Promise.reject(new Error('请输入' + item.name));
+                  }
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+      }
+
+      if (type === 'link') {
+        rules.push({ max: 64, message: '最多64个字符' });
+      }
+
+      if (type === 'user') {
+        if (props.notifyType === 'email') {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (value.value) {
+                const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
+                if (!reg.test(value.value)) {
+                  return Promise.reject(new Error('请输入正确的邮箱地址'));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+
+        if (['sms', 'voice'].includes(props.notifyType)) {
+          rules.push({
+            validator: async (_: any, value: any) => {
+              if (value.value) {
+                const reg = /^[1][3-9]\d{9}$/;
+                if (!reg.test(value.value)) {
+                  return Promise.reject(new Error('请输入正确的手机号码'));
+                }
+              }
+              return Promise.resolve();
+            },
+          });
+        }
+      }
+
+      return rules;
+    },
+    [props.notifyType],
+  );
+
   return (
     <>
       {props.template && (
@@ -30,7 +110,7 @@ export default (props: MessageContentProps) => {
                 const type = item.expands?.businessType || item.type;
                 const _name = [props.name, 'notify', 'variables', item.id];
                 let initialValue = undefined;
-                const rules = [];
+                const rules = getRules(item, type);
                 if (type === 'user') {
                   initialValue = {
                     source: 'relation',
@@ -42,20 +122,6 @@ export default (props: MessageContentProps) => {
                     value: undefined,
                   };
                 }
-                if (item.required) {
-                  rules.push({
-                    validator: async (_: any, value: any) => {
-                      if (!value.value) {
-                        if (['date'].includes(type)) {
-                          return Promise.reject(new Error('请选择' + item.name));
-                        } else {
-                          return Promise.reject(new Error('请输入' + item.name));
-                        }
-                      }
-                      return Promise.resolve();
-                    },
-                  });
-                }
                 return (
                   <Col span={12} key={`${item.id}_${index}`}>
                     <Form.Item
@@ -77,6 +143,8 @@ export default (props: MessageContentProps) => {
                         <InputFile />
                       ) : type === 'tag' ? (
                         <TagSelect configId={props.configId} />
+                      ) : type === 'link' ? (
+                        <Input placeholder={'请输入' + item.name} />
                       ) : (
                         <BuiltIn type={props.triggerType} data={item} />
                       )}

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

@@ -11,7 +11,6 @@ export default (props: InputNumberProps) => {
   const [unit, setUnit] = useState(props.value?.unit || 'seconds');
 
   useEffect(() => {
-    console.log('timer', props.value);
     setTime(props.value?.time || 0);
     setUnit(props.value?.unit || 'seconds');
   }, [props.value]);
@@ -41,6 +40,7 @@ export default (props: InputNumberProps) => {
       addonAfter={TimeTypeAfter}
       min={0}
       max={9999}
+      placeholder={'请输入时间'}
       onChange={(value) => {
         if (props.onChange) {
           props.onChange({

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

@@ -8,8 +8,13 @@
   }
 
   .trigger-type-content {
-    > .ant-row:not(:last-child) {
+    > .ant-row {
       margin-bottom: 24px;
+
+      &:last-child,
+      &:first-child {
+        margin-bottom: 0;
+      }
     }
   }
 }

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

@@ -12,14 +12,14 @@ import {
   Tooltip,
 } from 'antd';
 import { useIntl, useLocation } from 'umi';
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import { PermissionButton, TitleComponent } from '@/components';
 import ActionItems from './action/action';
 import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import { TimingTrigger, TriggerWay } from './components';
 import { TriggerWayType } from './components/TriggerWay';
 import TriggerTerm from '@/pages/rule-engine/Scene/TriggerTerm';
-import TriggerDevice from './trigger/device';
+import TriggerDevice from './trigger';
 import { service } from '../index';
 import './index.less';
 import { model } from '@formily/reactive';
@@ -52,31 +52,40 @@ export default () => {
 
   const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
   const [triggerType, setTriggerType] = useState('');
-  // const [triggerValue, setTriggerValue] = useState<any>();
+
   const [loading, setLoading] = useState(false);
   const [parallel, setParallel] = useState(true); // 是否并行
   const [shakeLimit, setShakeLimit] = useState<ShakeLimitType>(DefaultShakeLimit);
+
   const [requestParams, setRequestParams] = useState<any>(undefined);
+  const [triggerValue, setTriggerValue] = useState<any>([]);
+
   const [actionsData, setActionsData] = useState<any[]>([]);
   const [isEdit, setIsEdit] = useState(false);
 
-  const getDetail = async (id: string) => {
-    const resp = await service.detail(id);
-    if (resp.status === 200 && resp.result) {
-      setIsEdit(true);
-      const _data: any = resp.result;
-      FormModel = _data;
-      form.setFieldsValue(_data);
-      setParallel(_data.parallel);
-      setShakeLimit(_data.shakeLimit || DefaultShakeLimit);
-      if (_data.trigger?.device?.selectorValues) {
-        setRequestParams({ trigger: _data.trigger });
-      }
-      if (_data.actions) {
-        setActionsData(_data.actions);
+  const getDetail = useCallback(
+    async (id: string) => {
+      const resp = await service.detail(id);
+      if (resp.status === 200 && resp.result) {
+        setIsEdit(true);
+        const _data: any = resp.result;
+        FormModel = _data;
+        form.setFieldsValue(_data);
+        setParallel(_data.parallel);
+        setShakeLimit(_data.shakeLimit || DefaultShakeLimit);
+
+        setTriggerValue({ trigger: _data.terms || [] });
+
+        if (_data.trigger?.device) {
+          setRequestParams({ trigger: _data.trigger });
+        }
+        if (_data.actions) {
+          setActionsData(_data.actions);
+        }
       }
-    }
-  };
+    },
+    [triggerRef],
+  );
 
   useEffect(() => {
     const params = new URLSearchParams(location.search);
@@ -90,18 +99,25 @@ export default () => {
     const formData = await form.validateFields();
     let triggerData = undefined;
     // 获取触发条件数据
-    if (triggerRef.current) {
+    if (triggerRef.current && formData.trigger) {
       triggerData = await triggerRef.current.getTriggerForm();
-      console.log(JSON.stringify(triggerData), 'trigger');
-      if (!triggerData) {
-        return;
+      if (triggerData) {
+        formData.terms = triggerData.trigger;
       }
-      formData.terms = triggerData.trigger;
     }
-    console.log(formData);
+    console.log('save', formData);
     if (formData) {
       setLoading(true);
       const resp = formData.id ? await service.updateScene(formData) : await service.save(formData);
+
+      // 处理跳转新增
+      if ((window as any).onTabSaveSuccess) {
+        if (resp.result) {
+          (window as any).onTabSaveSuccess(resp);
+          setTimeout(() => window.close(), 300);
+        }
+      }
+
       setLoading(false);
       if (resp.status === 200) {
         message.success('操作成功');
@@ -178,12 +194,20 @@ export default () => {
         <Form
           form={form}
           colon={false}
+          name="basicForm"
           layout={'vertical'}
           preserve={false}
           className={'scene-save'}
           onValuesChange={(changeValue, allValues) => {
-            if (changeValue.trigger) {
-              setRequestParams({ trigger: allValues.trigger });
+            if (changeValue.trigger && changeValue.trigger.device) {
+              if (
+                changeValue.trigger.device.selectorValues ||
+                (changeValue.trigger.device.operation &&
+                  changeValue.trigger.device.operation.operator)
+              ) {
+                setTriggerValue([]);
+                setRequestParams({ trigger: allValues.trigger });
+              }
             }
             if (allValues.actions) {
               setActionsData(allValues.actions);
@@ -265,20 +289,33 @@ export default () => {
               </Form.Item>
             )}
             {triggerType === TriggerWayType.device && (
-              <Form.Item name={['trigger', 'device']}>
-                <TriggerDevice className={'trigger-type-content'} />
-              </Form.Item>
+              // <Form.Item
+              //   name={['trigger', 'device']}
+              //   rules={[
+              //     {
+              //       validator: async (_: any, value: any) => {
+              //         if (!value) {
+              //           return Promise.reject(new Error('请选择产品'));
+              //         }
+              //         return Promise.resolve();
+              //       },
+              //     },
+              //   ]}
+              // >
+              //   <TriggerDevice className={'trigger-type-content'} />
+              // </Form.Item>
+              <TriggerDevice
+                value={requestParams && requestParams.trigger}
+                className={'trigger-type-content'}
+                form={form}
+              />
             )}
           </Form.Item>
           {triggerType === TriggerWayType.device &&
           requestParams &&
           requestParams.trigger?.device?.productId ? (
             <Form.Item label={AntiShake}>
-              <TriggerTerm
-                ref={triggerRef}
-                params={requestParams}
-                // value={triggerValue}
-              />
+              <TriggerTerm ref={triggerRef} params={requestParams} value={triggerValue} />
             </Form.Item>
           ) : null}
           <Form.Item hidden name={'parallel'} initialValue={true}>
@@ -299,8 +336,8 @@ export default () => {
                 <Tooltip
                   title={
                     <div>
-                      <div>串行:满足所有执行条件才会触发执行动作</div>
-                      <div>并行:满足任意条件时会触发执行动作</div>
+                      <div>串行:按顺序依次执行动作</div>
+                      <div>并行:同时执行所有动作</div>
                     </div>
                   }
                 >
@@ -384,36 +421,6 @@ export default () => {
         >
           保存
         </PermissionButton>
-        {/*<Button*/}
-        {/*  onClick={() => {*/}
-        {/*    setTriggerValue({*/}
-        {/*      trigger: [*/}
-        {/*        {*/}
-        {/*          terms: [*/}
-        {/*            {*/}
-        {/*              column: '_now',*/}
-        {/*              termType: 'eq',*/}
-        {/*              source: 'manual',*/}
-        {/*              value: '2022-04-21 14:26:04',*/}
-        {/*            },*/}
-        {/*          ],*/}
-        {/*        },*/}
-        {/*        {*/}
-        {/*          terms: [*/}
-        {/*            {*/}
-        {/*              column: 'properties.test-zhibioa.recent',*/}
-        {/*              termType: 'lte',*/}
-        {/*              source: 'metrics',*/}
-        {/*              value: '123',*/}
-        {/*            },*/}
-        {/*          ],*/}
-        {/*        },*/}
-        {/*      ],*/}
-        {/*    });*/}
-        {/*  }}*/}
-        {/*>*/}
-        {/*  设置*/}
-        {/*</Button>*/}
       </Card>
     </PageContainer>
   );

+ 29 - 0
src/pages/rule-engine/Scene/Save/trigger/OrgTreeSelect.tsx

@@ -0,0 +1,29 @@
+import type { TreeSelectProps } from 'antd';
+import { TreeSelect } from 'antd';
+import React, { useEffect, useState } from 'react';
+
+interface OrgTreeSelect extends Omit<TreeSelectProps, 'onChange' | 'value'> {
+  onChange?: (value: any[]) => void;
+  value?: any;
+}
+
+export default (props: OrgTreeSelect) => {
+  const [myValue, setMyValue] = useState(props.value ? props.value[0].id : undefined);
+  const { value, onChange, ...extraProps } = props;
+
+  const onchange = (key: string, label: React.ReactNode[]) => {
+    if (props.onChange) {
+      props.onChange([{ id: key, name: label[0] }]);
+    }
+  };
+
+  useEffect(() => {
+    if (props.value) {
+      setMyValue(props.value[0].id);
+    } else {
+      setMyValue(undefined);
+    }
+  }, [props.value]);
+
+  return <TreeSelect<string> value={myValue} onChange={onchange} {...extraProps} />;
+};

+ 65 - 28
src/pages/rule-engine/Scene/Save/trigger/device.tsx

@@ -1,5 +1,5 @@
 import { useCallback, useEffect, useState } from 'react';
-import { Col, Row, Select, TreeSelect } from 'antd';
+import { Col, Form, Row, Select, TreeSelect } from 'antd';
 import { ItemGroup, TimingTrigger } from '@/pages/rule-engine/Scene/Save/components';
 import { getProductList } from '@/pages/rule-engine/Scene/Save/action/device/service';
 import { queryOrgTree } from '@/pages/rule-engine/Scene/Save/trigger/service';
@@ -153,29 +153,41 @@ export default (props: TriggerProps) => {
     <div className={classNames(props.className)}>
       <Row gutter={24}>
         <Col span={6}>
-          <Select
-            showSearch
-            value={props.value?.productId}
-            options={productList}
-            placeholder={'请选择产品'}
-            style={{ width: '100%' }}
-            listHeight={220}
-            onChange={(key: any, node: any) => {
-              productIdChange(key, node.metadata);
-              onChange({
-                productId: key,
-                selectorValues: undefined,
-                selector: 'fixed',
-                operation: {
-                  operator: undefined,
+          <Form.Item
+            noStyle
+            rules={[
+              {
+                validator: async (_: any, value: any) => {
+                  console.log('productId', value);
+                  return Promise.resolve();
                 },
-              });
-            }}
-            fieldNames={{ label: 'name', value: 'id' }}
-            filterOption={(input: string, option: any) =>
-              option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
-            }
-          />
+              },
+            ]}
+          >
+            <Select
+              showSearch
+              value={props.value?.productId}
+              options={productList}
+              placeholder={'请选择产品'}
+              style={{ width: '100%' }}
+              listHeight={220}
+              onChange={(key: any, node: any) => {
+                productIdChange(key, node.metadata);
+                onChange({
+                  productId: key,
+                  selectorValues: undefined,
+                  selector: 'fixed',
+                  operation: {
+                    operator: undefined,
+                  },
+                });
+              }}
+              fieldNames={{ label: 'name', value: 'id' }}
+              filterOption={(input: string, option: any) =>
+                option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
+              }
+            />
+          </Form.Item>
         </Col>
         {props.value?.productId ? (
           <Col span={12}>
@@ -219,14 +231,18 @@ export default (props: TriggerProps) => {
               )}
               {props.value?.selector === 'org' && (
                 <TreeSelect
-                  value={props.value?.selectorValues}
+                  value={
+                    props.value?.selectorValues && props.value?.selectorValues.length
+                      ? props.value?.selectorValues[0].id
+                      : undefined
+                  }
                   treeData={orgTree}
                   fieldNames={{ label: 'name', value: 'id' }}
                   placeholder={'请选择部门'}
                   style={{ width: '100%' }}
-                  onChange={(value) => {
+                  onChange={(value, label) => {
                     const _value = { ...props.value };
-                    _value.selectorValues = value;
+                    _value.selectorValues = [{ id: value, name: label[0] }];
                     onChange(_value);
                   }}
                 />
@@ -251,6 +267,7 @@ export default (props: TriggerProps) => {
         </Col>
       </Row>
       {props.value?.operation?.operator === OperatorEnum.invokeFunction ||
+      props.value?.operation?.operator === OperatorEnum.readProperty ||
       props.value?.operation?.operator === OperatorEnum.writeProperty ? (
         <TimingTrigger
           value={props.value?.operation?.timer}
@@ -295,7 +312,7 @@ export default (props: TriggerProps) => {
               />
             </Col>
             <Col span={18}>
-              <span style={{ lineHeight: '32px' }}>定时调用所选功能,功能返回值用于条件配置</span>
+              <span style={{ lineHeight: '32px' }}>定时调用所选功能</span>
             </Col>
           </Row>
           <Row>
@@ -366,7 +383,27 @@ export default (props: TriggerProps) => {
             />
           </Col>
           <Col span={18}>
-            <span style={{ lineHeight: '32px' }}>定时读取所选属性值,用于条件配置</span>
+            <span style={{ lineHeight: '32px' }}>定时读取所选属性值</span>
+          </Col>
+        </Row>
+      )}
+      {props.value?.operation?.operator === OperatorEnum.reportEvent && (
+        <Row gutter={24}>
+          <Col span={6}>
+            <Select
+              value={props.value?.operation?.eventId}
+              options={events}
+              placeholder={'请选择事件'}
+              style={{ width: '100%' }}
+              fieldNames={{ label: 'name', value: 'id' }}
+              onChange={(value) => {
+                if (props.value) {
+                  const _value = { ...props.value };
+                  _value!.operation!.eventId = value;
+                  onChange(_value);
+                }
+              }}
+            />
           </Col>
         </Row>
       )}

Fichier diff supprimé car celui-ci est trop grand
+ 164 - 75
src/pages/rule-engine/Scene/Save/trigger/index.tsx


+ 46 - 29
src/pages/rule-engine/Scene/Save/trigger/operation.tsx

@@ -1,6 +1,7 @@
 import { Col, Row, Select } from 'antd';
 import { useCallback, useEffect, useState } from 'react';
 import FunctionCall from '@/pages/rule-engine/Scene/Save/action/device/functionCall';
+import { debounce } from 'lodash';
 
 interface OperatorProps {
   propertiesList?: any[];
@@ -10,7 +11,7 @@ interface OperatorProps {
 
 export default (props: OperatorProps) => {
   const [data, setData] = useState<any>({});
-  const [key, setKey] = useState<string | undefined>(undefined);
+  const [key, setKey] = useState<string[] | undefined>(undefined);
   const [propertiesItem, setPropertiesItem] = useState<any[]>([]);
 
   const objToArray = (_data: any) => {
@@ -20,34 +21,44 @@ export default (props: OperatorProps) => {
   };
 
   const findProperties = useCallback(
-    (_key: string, value: any) => {
+    (_key: string[], value: any) => {
       if (props.propertiesList) {
-        const proItem = props.propertiesList.find((item: any) => item.id === _key);
-        if (proItem) {
-          return [
-            {
-              id: proItem.id,
-              name: proItem.name,
-              type: proItem.valueType ? proItem.valueType.type : '-',
-              format: proItem.valueType ? proItem.valueType.format : undefined,
-              options: proItem.valueType ? proItem.valueType.elements : undefined,
-              value: value,
-            },
-          ];
-        }
-        return [];
+        return _key.map((item) => {
+          const proItem = props.propertiesList!.find((a: any) => a.id === item);
+          return {
+            id: proItem.id,
+            name: proItem.name,
+            type: proItem.valueType ? proItem.valueType.type : '-',
+            format: proItem.valueType ? proItem.valueType.format : undefined,
+            options: proItem.valueType ? proItem.valueType.elements : undefined,
+            value: value[item],
+          };
+        });
       }
       return [];
     },
     [props.propertiesList],
   );
 
+  const functionDataChange = useCallback(
+    (value: any[]) => {
+      if (props.onChange) {
+        const _value = { ...props.value };
+        value.forEach((item: any) => {
+          _value[item.name] = item.value;
+        });
+        props.onChange(_value);
+      }
+    },
+    [props],
+  );
+
   useEffect(() => {
     if (props.value && props.propertiesList?.length) {
-      const _key = Object.keys(props.value)[0];
+      const _key = Object.keys(props.value);
       setKey(_key);
       setData(objToArray(props.value));
-      setPropertiesItem(findProperties(_key, props.value[_key]));
+      setPropertiesItem(findProperties(_key, props.value));
     } else {
       setData({});
       setKey(undefined);
@@ -58,6 +69,7 @@ export default (props: OperatorProps) => {
     <Row gutter={24}>
       <Col span={6}>
         <Select
+          mode="multiple"
           options={props.propertiesList || []}
           value={key}
           fieldNames={{
@@ -67,28 +79,33 @@ export default (props: OperatorProps) => {
           style={{ width: '100%' }}
           placeholder={'请选择属性'}
           onSelect={(id: any) => {
-            // TODO 多选
-            if (props.onChange) {
-              props.onChange({ [id]: {} });
+            if (props.value) {
+              const _value: any = { ...props.value };
+              if (id in props.value) {
+                delete _value[id];
+              } else {
+                _value[id] = undefined;
+              }
+              if (props.onChange) {
+                props.onChange(_value!);
+              }
+            } else {
+              if (props.onChange) {
+                props.onChange({ [id]: undefined });
+              }
             }
           }}
         />
       </Col>
       <Col span={18}>
-        <span style={{ lineHeight: '32px' }}>定时调用所选属性,修改后的属性值用于条件配置</span>
+        <span style={{ lineHeight: '32px' }}>定时调用所选属性</span>
       </Col>
       {key && (
         <Col span={24}>
           <FunctionCall
             value={data}
             functionData={propertiesItem}
-            onChange={(value) => {
-              if (props.onChange) {
-                props.onChange({
-                  [value[0].name]: value[0].value,
-                });
-              }
-            }}
+            onChange={debounce(functionDataChange, 300)}
           />
         </Col>
       )}

+ 56 - 17
src/pages/rule-engine/Scene/TriggerTerm/index.tsx

@@ -30,7 +30,6 @@ import { useAsyncDataSource } from '@/utils/util';
 import { Store } from 'jetlinks-store';
 import { treeFilter } from '@/utils/tree';
 import FInputGroup from '@/components/FInputGroup';
-import { Button } from 'antd';
 
 const service = new Service('scene');
 
@@ -50,7 +49,10 @@ const TriggerTerm = (props: Props, ref: any) => {
       const handleName = (_data: any): any => (
         <Space>
           {_data.name}
-          <div style={{ color: 'grey', marginLeft: '5px' }}>{_data.description}</div>
+          <div style={{ color: 'grey', marginLeft: '5px' }}>{_data.fullName}</div>
+          {_data.description && (
+            <div style={{ color: 'grey', marginLeft: '5px' }}>({_data.description})</div>
+          )}
         </Space>
       );
       const handleChildrenName = (_data: any[]): any[] => {
@@ -71,7 +73,7 @@ const TriggerTerm = (props: Props, ref: any) => {
           return [];
         }
       };
-      return data.map((item: any) => {
+      return data?.map((item: any) => {
         const disabled = item.children?.length > 0;
         return {
           column: item.column,
@@ -122,8 +124,11 @@ const TriggerTerm = (props: Props, ref: any) => {
                 label: item.name,
                 value: item.id,
               }));
+              if (target?.termTypes?.length > 0 && !state.value) {
+                state.value = 'eq';
+              }
             });
-            form1.setFieldState(field.query('.source'), (state) => {
+            form1.setFieldState(field.query('.value.source'), (state) => {
               state.dataSource =
                 target && target.metrics && target.metrics.length > 0
                   ? [
@@ -131,11 +136,12 @@ const TriggerTerm = (props: Props, ref: any) => {
                       { label: '指标', value: 'metrics' },
                     ]
                   : [{ label: '手动输入', value: 'manual' }];
+              state.value = 'manual';
             });
           });
-          onFieldReact('trigger.*.terms.*.source', (field, form1) => {
-            const params = field.query('.column').value();
-            const value = field.query('.value');
+          onFieldReact('trigger.*.terms.*.value.source', (field, form1) => {
+            const params = field.query('..column').value();
+
             // 找到选中的
             const _data = Store.get('trigger-parse-term');
             if (!_data) return;
@@ -148,6 +154,7 @@ const TriggerTerm = (props: Props, ref: any) => {
                 : treeValue[0];
 
             const source = (field as Field).value;
+            const value = field.query(source === 'manual' ? '.value' : '.metric');
             if (target) {
               if (source === 'manual') {
                 // 手动输入
@@ -189,7 +196,10 @@ const TriggerTerm = (props: Props, ref: any) => {
   );
 
   useImperativeHandle(ref, () => ({
-    getTriggerForm: () => form.submit(),
+    getTriggerForm: async () => {
+      await form.validate();
+      return form.submit();
+    },
   }));
   const SchemaField = createSchemaField({
     components: {
@@ -254,6 +264,7 @@ const TriggerTerm = (props: Props, ref: any) => {
                         'x-decorator-props': {
                           gridSpan: 6,
                         },
+                        required: true,
                         'x-component-props': {
                           placeholder: '请选择参数',
                           fieldNames: { value: 'column', label: 'name', options: 'children' },
@@ -272,9 +283,10 @@ const TriggerTerm = (props: Props, ref: any) => {
                         'x-component-props': {
                           placeholder: '操作符',
                         },
+                        required: true,
                       },
-                      inputGroup: {
-                        type: 'void',
+                      value: {
+                        type: 'object',
                         'x-component': 'FInputGroup',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
@@ -294,6 +306,7 @@ const TriggerTerm = (props: Props, ref: any) => {
                             type: 'string',
                             'x-component': 'Select',
                             'x-decorator': 'FormItem',
+                            required: true,
                             'x-component-props': {
                               style: {
                                 minWidth: '110px',
@@ -309,6 +322,39 @@ const TriggerTerm = (props: Props, ref: any) => {
                                 width: 'calc(100% - 110px)',
                               },
                             },
+                            required: true,
+                            'x-reactions': {
+                              dependencies: ['.source'],
+                              fulfill: {
+                                state: {
+                                  visible: '{{$deps[0]==="manual"}}',
+                                },
+                              },
+                            },
+                          },
+                          metric: {
+                            type: 'string',
+                            'x-component': 'Select',
+                            'x-decorator': 'FormItem',
+                            'x-component-props': {
+                              style: {
+                                width: '100%',
+                              },
+                            },
+                            required: true,
+                            'x-decorator-props': {
+                              style: {
+                                width: 'calc(100% - 110px)',
+                              },
+                            },
+                            'x-reactions': {
+                              dependencies: ['.source'],
+                              fulfill: {
+                                state: {
+                                  visible: '{{$deps[0]==="metrics"}}',
+                                },
+                              },
+                            },
                           },
                         },
                       },
@@ -351,13 +397,6 @@ const TriggerTerm = (props: Props, ref: any) => {
   return (
     <Form form={form} layout="vertical" className={styles.form}>
       <SchemaField schema={schema} scope={{ useAsyncDataSource, getParseTerm }} />
-      <Button
-        onClick={async () => {
-          console.log(await form.submit(), '保存');
-        }}
-      >
-        保存
-      </Button>
     </Form>
   );
 };

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

@@ -174,6 +174,21 @@ const Scene = () => {
         defaultMessage: '触发方式',
       }),
       width: 120,
+      valueType: 'select',
+      valueEnum: {
+        manual: {
+          text: '手动触发',
+          status: 'manual',
+        },
+        timer: {
+          text: '定时触发',
+          status: 'timer',
+        },
+        device: {
+          text: '设备触发',
+          status: 'device',
+        },
+      },
       renderText: (record) => TriggerWayType[record],
     },
     {
@@ -206,14 +221,14 @@ const Scene = () => {
           ''
         ),
       valueEnum: {
-        disable: {
-          text: '禁用',
-          status: 'offline',
-        },
         started: {
           text: '正常',
           status: 'started',
         },
+        disable: {
+          text: '禁用',
+          status: 'disable',
+        },
       },
     },
     {

+ 1 - 0
src/pages/rule-engine/Scene/typings.d.ts

@@ -24,6 +24,7 @@ interface SceneItem {
 type TriggerType = {
   type?: string;
   shakeLimit?: any;
+  operation?: any;
   device?: any;
   timer?: any;
 };

+ 4 - 0
src/pages/system/Config/index.tsx

@@ -0,0 +1,4 @@
+import Service from '@/pages/system/Config/service';
+
+export const service = new Service('system');
+export default () => {};

+ 6 - 0
src/pages/system/Config/service.ts

@@ -0,0 +1,6 @@
+import BaseService from '@/utils/BaseService';
+import { request } from '@@/plugin-request/request';
+
+export default class Service extends BaseService<any> {
+  getAMapKey = () => request(`${this.uri}/config/amap`, { method: 'GET' });
+}

+ 2 - 0
src/utils/const.ts

@@ -26,6 +26,8 @@ class SystemConst {
   static GET_METADATA = 'get_metadata';
 
   static REFRESH_DEVICE = 'refresh_device';
+
+  static AMAP_KEY = 'amap_key';
 }
 
 export default SystemConst;

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

@@ -36,6 +36,9 @@ const extraRouteObj = {
       { code: 'Save2', name: '测试详情' },
     ],
   },
+  demo: {
+    children: [{ code: 'AMap', name: '地图' }],
+  },
 };
 //额外路由
 export const extraRouteArr = [

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

@@ -112,6 +112,8 @@ export enum MENUS_CODE {
   'link/Type/Detail' = 'link/Type/Detail',
   'account/Center' = 'account/Center',
   'account/Center/bind' = 'account/Center/bind',
+  'Northbound/DuerOS' = 'Northbound/DuerOS',
+  'Northbound/AliCloud' = 'Northbound/AliCloud',
 }
 
 export type MENUS_CODE_TYPE = keyof typeof MENUS_CODE | string;

+ 214 - 66
yarn.lock

@@ -51,7 +51,7 @@
 
 "@ant-design/colors@^6.0.0":
   version "6.0.0"
-  resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
+  resolved "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
   integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==
   dependencies:
     "@ctrl/tinycolor" "^3.4.0"
@@ -80,7 +80,7 @@
 
 "@ant-design/icons-svg@^4.2.1":
   version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz#8630da8eb4471a4aabdaed7d1ff6a97dcb2cf05a"
+  resolved "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz#8630da8eb4471a4aabdaed7d1ff6a97dcb2cf05a"
   integrity sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==
 
 "@ant-design/icons@^4.0.0", "@ant-design/icons@^4.1.0", "@ant-design/icons@^4.2.1", "@ant-design/icons@^4.3.0", "@ant-design/icons@^4.5.0", "@ant-design/icons@^4.7.0":
@@ -385,7 +385,7 @@
 
 "@ant-design/react-slick@~0.28.1":
   version "0.28.4"
-  resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.28.4.tgz#8b296b87ad7c7ae877f2a527b81b7eebd9dd29a9"
+  resolved "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-0.28.4.tgz#8b296b87ad7c7ae877f2a527b81b7eebd9dd29a9"
   integrity sha512-j9eAHTn7GxbXUFNknJoHS2ceAsqrQi2j8XykjZE1IXCD8kJF+t28EvhBLniDpbOsBk/3kjalnhriTfZcjBHNqg==
   dependencies:
     "@babel/runtime" "^7.10.4"
@@ -434,6 +434,21 @@
     "@antv/util" "^2.0.9"
     tslib "^2.0.3"
 
+"@antv/component@*", "@antv/component@^0.8.7":
+  version "0.8.27"
+  resolved "https://registry.yarnpkg.com/@antv/component/-/component-0.8.27.tgz#eac6326ba1b5db123f88635f93b4450976249d48"
+  integrity sha512-FY9fgUBjEuWxQ4w7VbcMSwFr7pqnRf1/F1ja1weoEpNndKBlStNYWhXTx4p5uMJVLvMpXoFPqan7RzyP8rel6Q==
+  dependencies:
+    "@antv/color-util" "^2.0.3"
+    "@antv/dom-util" "~2.0.1"
+    "@antv/g-base" "^0.5.9"
+    "@antv/matrix-util" "^3.1.0-beta.1"
+    "@antv/path-util" "~2.0.7"
+    "@antv/scale" "~0.3.1"
+    "@antv/util" "~2.0.0"
+    fecha "~4.2.0"
+    tslib "^2.0.3"
+
 "@antv/component@^0.8.19":
   version "0.8.26"
   resolved "https://registry.yarnpkg.com/@antv/component/-/component-0.8.26.tgz#f0fe01ebc20f1f33deacc83a07ad3f76c611b561"
@@ -565,6 +580,27 @@
     probe.gl "^3.1.1"
     reflect-metadata "^0.1.13"
 
+"@antv/g2@4.1.32":
+  version "4.1.32"
+  resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-4.1.32.tgz#ac0646d7cbb279f8100f4bd2a3a25ba6bacb98f2"
+  integrity sha512-vJC0LgFyCjN3RdPA6JOi59qC8O4Z70TqFh/th+kzdWlt9KXDJc3MBBYcJI97m1IlrT9XqTGKqkZyGduZw4HCoA==
+  dependencies:
+    "@antv/adjust" "^0.2.1"
+    "@antv/attr" "^0.3.1"
+    "@antv/color-util" "^2.0.2"
+    "@antv/component" "^0.8.7"
+    "@antv/coord" "^0.3.0"
+    "@antv/dom-util" "^2.0.2"
+    "@antv/event-emitter" "~0.1.0"
+    "@antv/g-base" "~0.5.6"
+    "@antv/g-canvas" "~0.5.10"
+    "@antv/g-svg" "~0.5.6"
+    "@antv/matrix-util" "^3.1.0-beta.1"
+    "@antv/path-util" "^2.0.3"
+    "@antv/scale" "^0.3.7"
+    "@antv/util" "~2.0.5"
+    tslib "^2.0.0"
+
 "@antv/g2@^4.1.26":
   version "4.1.49"
   resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-4.1.49.tgz#ab74f1db2b74ecf9bd771deaa2549bb2c94121c5"
@@ -586,6 +622,20 @@
     "@antv/util" "~2.0.5"
     tslib "^2.0.0"
 
+"@antv/g2plot@2.3.39":
+  version "2.3.39"
+  resolved "https://registry.yarnpkg.com/@antv/g2plot/-/g2plot-2.3.39.tgz#46b74bbd7ec12258ca762683adcd327848af4cee"
+  integrity sha512-B6/b+MiUOuO3vlvY19Qt0v+3B7ds72pxESI714hzuH2niXQ35AW8GaQ7+1U6Y7Kk7btoaB2AaCyWcvZuloWoPw==
+  dependencies:
+    "@antv/event-emitter" "^0.1.2"
+    "@antv/g2" "^4.1.26"
+    d3-hierarchy "^2.0.0"
+    d3-regression "^1.3.5"
+    fmin "^0.0.2"
+    pdfast "^0.2.0"
+    size-sensor "^1.0.1"
+    tslib "^2.0.3"
+
 "@antv/g2plot@^2.2.11":
   version "2.4.15"
   resolved "https://registry.yarnpkg.com/@antv/g2plot/-/g2plot-2.4.15.tgz#6777cd6c5e2c921ea98012cde4bbaf9ba3ace279"
@@ -937,7 +987,16 @@
     fecha "~4.2.0"
     tslib "^2.0.0"
 
-"@antv/util@^2.0.13", "@antv/util@^2.0.14", "@antv/util@^2.0.7", "@antv/util@^2.0.9", "@antv/util@~2.0.0", "@antv/util@~2.0.1", "@antv/util@~2.0.12", "@antv/util@~2.0.13", "@antv/util@~2.0.3", "@antv/util@~2.0.5":
+"@antv/scale@^0.3.7":
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/@antv/scale/-/scale-0.3.17.tgz#f19cc4a7a667edbceac1df35b87267bbbca71ae0"
+  integrity sha512-YjPYG2Lbhou2cnle4MTlsq45dUVjP5tiGG/pYNIerE1sSBqFnC0/7tf9ZWp5OaHZH/qHNX8IfKeQdWHZDR4kDw==
+  dependencies:
+    "@antv/util" "~2.0.3"
+    fecha "~4.2.0"
+    tslib "^2.0.0"
+
+"@antv/util@2.0.17", "@antv/util@^2.0.13", "@antv/util@^2.0.14", "@antv/util@^2.0.7", "@antv/util@^2.0.9", "@antv/util@~2.0.0", "@antv/util@~2.0.1", "@antv/util@~2.0.12", "@antv/util@~2.0.13", "@antv/util@~2.0.3", "@antv/util@~2.0.5":
   version "2.0.17"
   resolved "https://registry.yarnpkg.com/@antv/util/-/util-2.0.17.tgz#e8ef42aca7892815b229269f3dd10c6b3c7597a9"
   integrity sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==
@@ -2151,6 +2210,18 @@
     resolve "^1.8.1"
     semver "^5.5.1"
 
+"@babel/plugin-transform-runtime@^7.12.10":
+  version "7.17.10"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1"
+  integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==
+  dependencies:
+    "@babel/helper-module-imports" "^7.16.7"
+    "@babel/helper-plugin-utils" "^7.16.7"
+    babel-plugin-polyfill-corejs2 "^0.3.0"
+    babel-plugin-polyfill-corejs3 "^0.5.0"
+    babel-plugin-polyfill-regenerator "^0.3.0"
+    semver "^6.3.0"
+
 "@babel/plugin-transform-shorthand-properties@^7.12.1", "@babel/plugin-transform-shorthand-properties@^7.16.7", "@babel/plugin-transform-shorthand-properties@^7.2.0":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a"
@@ -2668,9 +2739,9 @@
   integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
 
 "@ctrl/tinycolor@^3.4.0":
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f"
-  integrity sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
+  integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
 
 "@dagrejs/graphlib@2.1.4":
   version "2.1.4"
@@ -5682,7 +5753,7 @@ antd-mobile@^2.3.1:
     rmc-tabs "~1.2.0"
     rmc-tooltip "~1.0.0"
 
-antd@^4.1.2, antd@^4.1.3, antd@^4.18.8:
+antd@4.19.5, antd@^4.1.2, antd@^4.1.3:
   version "4.19.5"
   resolved "https://registry.yarnpkg.com/antd/-/antd-4.19.5.tgz#38d08f3e1391a7a69c2ca76f50968bb12ec2ac93"
   integrity sha512-C4H/VJqlVO5iMvHZyiV27R8SbPs4jsOKCGPhDXIHUry/RnUCbMmVeQaPRfUIxSI1NbqDflsuQfevPtz1svyIlg==
@@ -6137,6 +6208,13 @@ babel-plugin-transform-react-remove-prop-types@0.4.24:
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a"
   integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==
 
+babel-plugin-transform-replace-object-assign@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-replace-object-assign/-/babel-plugin-transform-replace-object-assign-2.0.0.tgz#a5d538d0e5d1c78ef2058a2518ff96720a43b355"
+  integrity sha512-PMT+dRz6JAHbXIsJB4XjcIstmKK9SFj9MYZGcEWW/1jISiemGz9w6TVLrj4hgpR89X0J9mFuHq61zPvP8lgZZQ==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+
 babel-plugin-transform-typescript-metadata@0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-typescript-metadata/-/babel-plugin-transform-typescript-metadata-0.3.1.tgz#d86599b7139131ba5e917f5f568d0c824a5cdfc3"
@@ -6283,6 +6361,25 @@ bindings@^1.5.0:
   dependencies:
     file-uri-to-path "1.0.0"
 
+bizcharts@^4.1.16:
+  version "4.1.16"
+  resolved "https://registry.yarnpkg.com/bizcharts/-/bizcharts-4.1.16.tgz#0dac99a5875314e4328121e0ad23cfbbf3e9b0d0"
+  integrity sha512-AMwbNd2UF1/iPKFSA/T1o2PvtcGlNQWnxGToNdBwHNDmoUgMvxa7GuBNz5NNEt32ftt+Ra4m2fdgqK5mrj8C3Q==
+  dependencies:
+    "@antv/component" "*"
+    "@antv/g2" "4.1.32"
+    "@antv/g2plot" "2.3.39"
+    "@antv/util" "2.0.17"
+    "@babel/plugin-transform-modules-commonjs" "^7.12.1"
+    "@babel/plugin-transform-runtime" "^7.12.10"
+    "@juggle/resize-observer" "^3.3.1"
+    babel-plugin-transform-replace-object-assign "^2.0.0"
+    d3-color "^1.4.1"
+    react-error-boundary "3.0.2"
+    react-reconciler "^0.25.1"
+    simple-statistics "^7.1.0"
+    warning "^4.0.3"
+
 bl@^4.0.3, bl@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -7294,7 +7391,7 @@ compression@1.7.4, compression@^1.7.4:
 
 compute-scroll-into-view@^1.0.17:
   version "1.0.17"
-  resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
+  resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
   integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
 
 concat-map@0.0.1:
@@ -7765,7 +7862,7 @@ d3-collection@1:
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
   integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
 
-d3-color@1, d3-color@^1.4.0:
+d3-color@1, d3-color@^1.4.0, d3-color@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
   integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
@@ -7912,7 +8009,7 @@ data-urls@^2.0.0:
 
 date-fns@2.x:
   version "2.28.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
+  resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
   integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
 
 date-format@^0.0.0:
@@ -7920,7 +8017,12 @@ date-format@^0.0.0:
   resolved "https://registry.yarnpkg.com/date-format/-/date-format-0.0.0.tgz#09206863ab070eb459acea5542cbd856b11966b3"
   integrity sha1-CSBoY6sHDrRZrOpVQsvYVrEZZrM=
 
-dayjs@1.x, dayjs@^1.10.3, dayjs@^1.9.1:
+dayjs@1.x:
+  version "1.11.2"
+  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
+  integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
+
+dayjs@^1.10.3, dayjs@^1.9.1:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805"
   integrity sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==
@@ -8212,11 +8314,16 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
-dom-align@1.x, dom-align@^1.7.0:
+dom-align@1.x:
   version "1.12.2"
   resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.2.tgz#0f8164ebd0c9c21b0c790310493cd855892acd4b"
   integrity sha512-pHuazgqrsTFrGU2WLDdXxCFabkdQDx72ddkraZNih1KsMcN5qsRSTR9O4VJRlwTPCPb5COYg3LOfiMHHcPInHg==
 
+dom-align@^1.7.0:
+  version "1.12.3"
+  resolved "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.3.tgz#a36d02531dae0eefa2abb0c4db6595250526f103"
+  integrity sha512-Gj9hZN3a07cbR6zviMUBOMPdWxYhbMI+x+WS0NAIu2zFZmbK8ys9R79g+iG9qLnlCwpFoaB+fKy8Pdv470GsPA==
+
 dom-serializer@0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -12289,8 +12396,8 @@ json2module@^0.0.3:
 
 json2mq@^0.2.0:
   version "0.2.0"
-  resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a"
-  integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=
+  resolved "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a"
+  integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==
   dependencies:
     string-convert "^0.2.0"
 
@@ -13269,7 +13376,7 @@ memoize-one@^5.1.1:
 
 memoize-one@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
+  resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
   integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
 
 memoizee@^0.4.15:
@@ -13926,7 +14033,12 @@ mockjs@^1.0.1-beta3, mockjs@^1.1.0:
   dependencies:
     commander "*"
 
-moment@^2.24.0, moment@^2.25.3, moment@^2.27.0, moment@^2.29.1:
+moment@^2.24.0:
+  version "2.29.3"
+  resolved "https://registry.npmmirror.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
+  integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
+
+moment@^2.25.3, moment@^2.27.0, moment@^2.29.1:
   version "2.29.2"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
   integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
@@ -15953,9 +16065,9 @@ rc-align@^2.4.0:
     rc-util "^4.0.4"
 
 rc-align@^4.0.0:
-  version "4.0.11"
-  resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.11.tgz#8198c62db266bc1b8ef05e56c13275bf72628a5e"
-  integrity sha512-n9mQfIYQbbNTbefyQnRHZPWuTEwG1rY4a9yKlIWHSTbgwI+XUMGRYd0uJ5pE2UbrNX0WvnMBA1zJ3Lrecpra/A==
+  version "4.0.12"
+  resolved "https://registry.npmmirror.com/rc-align/-/rc-align-4.0.12.tgz#065b5c68a1cc92a00800c9239320d9fdf5f16207"
+  integrity sha512-3DuwSJp8iC/dgHzwreOQl52soj40LchlfUHtgACOUtwGuoFIOVh6n/sCpfqCU8kO5+iz6qR0YKvjgB8iPdE3aQ==
   dependencies:
     "@babel/runtime" "^7.10.1"
     classnames "2.x"
@@ -16001,7 +16113,7 @@ rc-checkbox@~2.0.0:
 
 rc-checkbox@~2.3.0:
   version "2.3.2"
-  resolved "https://registry.yarnpkg.com/rc-checkbox/-/rc-checkbox-2.3.2.tgz#f91b3678c7edb2baa8121c9483c664fa6f0aefc1"
+  resolved "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-2.3.2.tgz#f91b3678c7edb2baa8121c9483c664fa6f0aefc1"
   integrity sha512-afVi1FYiGv1U0JlpNH/UaEXdh6WUJjcWokj/nUN2TgG80bfG+MDdbfHKlLcNNba94mbjy2/SXJ1HDgrOkXGAjg==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16019,7 +16131,7 @@ rc-collapse@~1.9.1:
 
 rc-collapse@~3.1.0:
   version "3.1.4"
-  resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.1.4.tgz#063e33fcc427a378e63da757898cd1fba6269679"
+  resolved "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.1.4.tgz#063e33fcc427a378e63da757898cd1fba6269679"
   integrity sha512-WayrhswKMwuJab9xbqFxXTgV0m6X8uOPEO6zm/GJ5YJiJ/wIh/Dd2VtWeI06HYUEnTFv0HNcYv+zWbB+p6OD2A==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16040,7 +16152,7 @@ rc-dialog@~8.6.0:
 
 rc-drawer@~4.4.2:
   version "4.4.3"
-  resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.4.3.tgz#2094937a844e55dc9644236a2d9fba79c344e321"
+  resolved "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-4.4.3.tgz#2094937a844e55dc9644236a2d9fba79c344e321"
   integrity sha512-FYztwRs3uXnFOIf1hLvFxIQP9MiZJA+0w+Os8dfDh/90X7z/HqP/Yg+noLCIeHEbKln1Tqelv8ymCAN24zPcfQ==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16095,7 +16207,7 @@ rc-image@~5.2.5:
 
 rc-input-number@~7.3.0:
   version "7.3.4"
-  resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.3.4.tgz#674aea98260250287d36e330a7e065b174486e9d"
+  resolved "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-7.3.4.tgz#674aea98260250287d36e330a7e065b174486e9d"
   integrity sha512-W9uqSzuvJUnz8H8vsVY4kx+yK51SsAxNTwr8SNH4G3XqQNocLVmKIibKFRjocnYX1RDHMND9FFbgj2h7E7nvGA==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16103,9 +16215,9 @@ rc-input-number@~7.3.0:
     rc-util "^5.9.8"
 
 rc-input@~0.0.1-alpha.5:
-  version "0.0.1-alpha.6"
-  resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.0.1-alpha.6.tgz#b9bcfb41251ca07aa183c03a3574fbc14fa2e426"
-  integrity sha512-kgpmbxa9vp6kPLW7IP5/Lf6wuaMq+pUq+dPz98vIM58h4wkEKgBQlkMIg9OCEVQIiR8rEPEoe4dO2fc9R0aypQ==
+  version "0.0.1-alpha.7"
+  resolved "https://registry.npmmirror.com/rc-input/-/rc-input-0.0.1-alpha.7.tgz#53e3f13871275c21d92b51f80b698f389ad45dd3"
+  integrity sha512-eozaqpCYWSY5LBMwlHgC01GArkVEP+XlJ84OMvdkwUnJBSv83Yxa15pZpn7vACAj84uDC4xOA2CoFdbLuqB08Q==
   dependencies:
     "@babel/runtime" "^7.11.1"
     classnames "^2.2.1"
@@ -16137,9 +16249,9 @@ rc-menu@~9.3.2:
     shallowequal "^1.1.0"
 
 rc-menu@~9.5.1:
-  version "9.5.4"
-  resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-9.5.4.tgz#6aade123b215026d2305a94de61ace3609d95525"
-  integrity sha512-SS/JLvOhGBvBMLb13k8SEcKX0VxyjbQ2d6YYCBD64F1Smkz1BtD5zfhcei/2GCKHvkh+SVr3TP0ZOSh50Cg09Q==
+  version "9.5.5"
+  resolved "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.5.5.tgz#aa2f151d4191ed089dc1a8141fe365c9b77d61a9"
+  integrity sha512-wj2y2BAKwSMyWXO3RBf9sNN5V+DFWxFl45Ma6qQEHA5nwwh7p07bNgc6AAJc+L1+LAz+rWz3AU8PYyT17hMHCw==
   dependencies:
     "@babel/runtime" "^7.10.1"
     classnames "2.x"
@@ -16150,13 +16262,13 @@ rc-menu@~9.5.1:
     shallowequal "^1.1.0"
 
 rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.2.0, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4:
-  version "2.4.9"
-  resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.4.9.tgz#fa6e4b044b971845ffb83696e1c4d11b81bf132f"
-  integrity sha512-lrIpBQQ5gIDVedaubnhXuTjC3zpW7HvC/34KyvcHlf6fBjuBlwv45PbonFhmk4Rgu7gLQYrKoMGgFVXqxxyLCw==
+  version "2.6.0"
+  resolved "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.6.0.tgz#c60c3e7f15257f55a8cd7794a539f0e2cc751399"
+  integrity sha512-1MDWA9+i174CZ0SIDenSYm2Wb9YbRkrexjZWR0CUFu7D6f23E8Y0KsTgk9NGOLJsGak5ELZK/Y5lOlf5wQdzbw==
   dependencies:
     "@babel/runtime" "^7.11.1"
     classnames "^2.2.1"
-    rc-util "^5.19.2"
+    rc-util "^5.21.0"
 
 rc-notification@~4.5.7:
   version "4.5.7"
@@ -16169,9 +16281,9 @@ rc-notification@~4.5.7:
     rc-util "^5.0.1"
 
 rc-overflow@^1.0.0, rc-overflow@^1.2.0:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.2.4.tgz#e25cd3c4a98b4f8233a8dab7172ab2dbcc83b45e"
-  integrity sha512-nIeelyYfdS+mQBK1++FisLZEvZ8xVAzC+duG+TC4TmqNN+kTHraiGntV9/zxDGA1ruyQ91YRJ549JjFodCBnsw==
+  version "1.2.5"
+  resolved "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.2.5.tgz#d0fe3f9fa99edec70f4fe20e38119e8c1c5ae3ca"
+  integrity sha512-5HJKZ4nPe9e7AFdCkflgpRydvH6lJ4i2iFF06q/T1G9lL/XBeuoPLRrTBU8ao/Vo/yARW6WfEHnC2951lVgX5Q==
   dependencies:
     "@babel/runtime" "^7.11.1"
     classnames "^2.2.1"
@@ -16179,17 +16291,17 @@ rc-overflow@^1.0.0, rc-overflow@^1.2.0:
     rc-util "^5.19.2"
 
 rc-pagination@~3.1.9:
-  version "3.1.15"
-  resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.15.tgz#e05eddf4c15717a5858290bed0857e27e2f957ff"
-  integrity sha512-4L3fot8g4E+PjWEgoVGX0noFCg+8ZFZmeLH4vsnZpB3O2T2zThtakjNxG+YvSaYtyMVT4B+GLayjKrKbXQpdAg==
+  version "3.1.16"
+  resolved "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-3.1.16.tgz#b0082108cf027eded18ed61d818d31897c343e81"
+  integrity sha512-GFcHXJ7XxeJDf9B+ndP4PRDt46maSSgYhiwofBMiIGKIlBhJ0wfu8DMCEvaWJJLpI2u4Gb6zF1dHpiqPFrosPg==
   dependencies:
     "@babel/runtime" "^7.10.1"
     classnames "^2.2.1"
 
 rc-picker@~2.6.4:
-  version "2.6.5"
-  resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.6.5.tgz#a7cf8eb0723ec81e379c784c4b798b7fe076dd8c"
-  integrity sha512-4pcg0PgEz4YXBfdwMuHIKaRWaADm3k3g0NtoPIgeGM+VVeOBdUowTx0YSXnT8mQEXcE9lWXX+ZX3biAzQwDM1w==
+  version "2.6.8"
+  resolved "https://registry.npmmirror.com/rc-picker/-/rc-picker-2.6.8.tgz#eff71e13d836953a4c7439c958228b5108f92c22"
+  integrity sha512-j14N2nxcx4PAw7LviwLKIJG4cEAlCFhcHI/7pz+Ps43Df7UrSIWt/QGJgPAWz38Z6jrjsgMcyVHVccpL09gDDA==
   dependencies:
     "@babel/runtime" "^7.10.1"
     classnames "^2.2.1"
@@ -16202,7 +16314,7 @@ rc-picker@~2.6.4:
 
 rc-progress@~3.2.1:
   version "3.2.4"
-  resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.2.4.tgz#4036acdae2566438545bc4df2203248babaf7549"
+  resolved "https://registry.npmmirror.com/rc-progress/-/rc-progress-3.2.4.tgz#4036acdae2566438545bc4df2203248babaf7549"
   integrity sha512-M9WWutRaoVkPUPIrTpRIDpX0SPSrVHzxHdCRCbeoBFrd9UFWTYNWRlHsruJM5FH1AZI+BwB4wOJUNNylg/uFSw==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16211,7 +16323,7 @@ rc-progress@~3.2.1:
 
 rc-rate@~2.9.0:
   version "2.9.1"
-  resolved "https://registry.yarnpkg.com/rc-rate/-/rc-rate-2.9.1.tgz#e43cb95c4eb90a2c1e0b16ec6614d8c43530a731"
+  resolved "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.9.1.tgz#e43cb95c4eb90a2c1e0b16ec6614d8c43530a731"
   integrity sha512-MmIU7FT8W4LYRRHJD1sgG366qKtSaKb67D0/vVvJYR0lrCuRrCiVQ5qhfT5ghVO4wuVIORGpZs7ZKaYu+KMUzA==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16239,7 +16351,7 @@ rc-resize-observer@^0.2.3:
 
 rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.2.0:
   version "1.2.0"
-  resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.2.0.tgz#9f46052f81cdf03498be35144cb7c53fd282c4c7"
+  resolved "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.2.0.tgz#9f46052f81cdf03498be35144cb7c53fd282c4c7"
   integrity sha512-6W+UzT3PyDM0wVCEHfoW3qTHPTvbdSgiA43buiy8PzmeMnfgnDeb9NjdimMXMl3/TcrvvWl5RRVdp+NqcR47pQ==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16286,7 +16398,7 @@ rc-slider@~8.2.0:
 
 rc-steps@~4.1.0:
   version "4.1.4"
-  resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-4.1.4.tgz#0ba82db202d59ca52d0693dc9880dd145b19dc23"
+  resolved "https://registry.npmmirror.com/rc-steps/-/rc-steps-4.1.4.tgz#0ba82db202d59ca52d0693dc9880dd145b19dc23"
   integrity sha512-qoCqKZWSpkh/b03ASGx1WhpKnuZcRWmvuW+ZUu4mvMdfvFzVxblTwUM+9aBd0mlEUFmt6GW8FXhMpHkK3Uzp3w==
   dependencies:
     "@babel/runtime" "^7.10.2"
@@ -16305,7 +16417,7 @@ rc-swipeout@~2.0.0:
 
 rc-switch@~3.2.0:
   version "3.2.2"
-  resolved "https://registry.yarnpkg.com/rc-switch/-/rc-switch-3.2.2.tgz#d001f77f12664d52595b4f6fb425dd9e66fba8e8"
+  resolved "https://registry.npmmirror.com/rc-switch/-/rc-switch-3.2.2.tgz#d001f77f12664d52595b4f6fb425dd9e66fba8e8"
   integrity sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16349,7 +16461,7 @@ rc-tabs@~11.10.0:
 
 rc-textarea@^0.3.0, rc-textarea@~0.3.0:
   version "0.3.7"
-  resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.3.7.tgz#987142891efdedb774883c07e2f51b318fde5a11"
+  resolved "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-0.3.7.tgz#987142891efdedb774883c07e2f51b318fde5a11"
   integrity sha512-yCdZ6binKmAQB13hc/oehh0E/QRwoPP1pjF21aHBxlgXO3RzPF6dUu4LG2R4FZ1zx/fQd2L1faktulrXOM/2rw==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16369,7 +16481,7 @@ rc-tooltip@^3.4.2:
 
 rc-tooltip@^5.0.1, rc-tooltip@~5.1.1:
   version "5.1.1"
-  resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.1.1.tgz#94178ed162d0252bc4993b725f5dc2ac0fccf154"
+  resolved "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-5.1.1.tgz#94178ed162d0252bc4993b725f5dc2ac0fccf154"
   integrity sha512-alt8eGMJulio6+4/uDm7nvV+rJq9bsfxFDCI0ljPdbuoygUscbsMYb6EQgwib/uqsXQUvzk+S7A59uYHmEgmDA==
   dependencies:
     "@babel/runtime" "^7.11.2"
@@ -16411,9 +16523,9 @@ rc-trigger@^2.2.2:
     react-lifecycles-compat "^3.0.4"
 
 rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.10:
-  version "5.2.12"
-  resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.12.tgz#023728bf2e31a3bdd07bdca016e0991aae98e504"
-  integrity sha512-+rymZRe23f2imQwuelWqWSKj3tAnlDbjEU8yjKzW5zh8AlChJ/brda7Qg3FQ4f2jcAquL7phVOwq5BZp2PdhAg==
+  version "5.2.18"
+  resolved "https://registry.npmmirror.com/rc-trigger/-/rc-trigger-5.2.18.tgz#adab51918e4569b174d4fc5044186200d97a542c"
+  integrity sha512-hi2yZ7umtbAGLxgSph1az9BR9i4Pb4fiQa4pdvFQuKN7U//3nwwygHQKHfexnM+0APBnzZwVlEHA5I8BpWrygw==
   dependencies:
     "@babel/runtime" "^7.11.2"
     classnames "^2.2.6"
@@ -16423,7 +16535,7 @@ rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.10:
 
 rc-upload@~4.3.0:
   version "4.3.3"
-  resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.3.3.tgz#e237aa525e5313fa16f4d04d27f53c2f0e157bb8"
+  resolved "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.3.3.tgz#e237aa525e5313fa16f4d04d27f53c2f0e157bb8"
   integrity sha512-YoJ0phCRenMj1nzwalXzciKZ9/FAaCrFu84dS5pphwucTC8GUWClcDID/WWNGsLFcM97NqIboDqrV82rVRhW/w==
   dependencies:
     "@babel/runtime" "^7.10.1"
@@ -16441,7 +16553,7 @@ rc-util@4.x, rc-util@^4.0.4, rc-util@^4.13.0, rc-util@^4.15.3, rc-util@^4.15.7,
     react-lifecycles-compat "^3.0.4"
     shallowequal "^1.1.0"
 
-rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.14.0, rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.19.3, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4, rc-util@^5.9.8:
+rc-util@^5.0.0, rc-util@^5.19.3:
   version "5.19.6"
   resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.19.6.tgz#76d714fd116d22bc5fd7421939589e27c1dfad19"
   integrity sha512-/xJ8UtpbuJj7+2ftxVQM6gUzLY+Towq4iB6sP6/2hhn6mwWNLij2I+1qOkLv75I1jqWKpS+gU8A2EmbfLtGxNg==
@@ -16450,10 +16562,19 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.14.0
     react-is "^16.12.0"
     shallowequal "^1.1.0"
 
+rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.14.0, rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.21.0, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4, rc-util@^5.9.8:
+  version "5.21.2"
+  resolved "https://registry.npmmirror.com/rc-util/-/rc-util-5.21.2.tgz#fa23277ba84e5561af2febdca64de3fc2b3e1528"
+  integrity sha512-QuuZ2tKMScGtxSx3rLzgPGGDZm/np7phMqA7OcDidSf44abvSk+AdtdD7ZvQPvCEtdC6nCSI5tEVnUaYjjD9/w==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    react-is "^16.12.0"
+    shallowequal "^1.1.0"
+
 rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.2:
-  version "3.4.6"
-  resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.6.tgz#af34235915221173dd42d9f25b32e95d4c0f5698"
-  integrity sha512-wMJ7Bl+AxgIDojp0VxuQxjpNulKodwxGXSsTyxA9Mwzwemj5vKAgTbkPT64ZW5ORf8FOQAaPRlMiTADrPEo3sQ==
+  version "3.4.7"
+  resolved "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.4.7.tgz#ca0ba5ecddff686cd3833562d07c2678d1c9cb2e"
+  integrity sha512-PhV8a8g/L9sCmWcmXizzwW7QdqsxK4ebHU6fA9OsUIR7isFdx2bTGU2iAUdRV4teiIF1ZHF3gSQh8NtAxrXh6A==
   dependencies:
     classnames "^2.2.6"
     rc-resize-observer "^1.0.0"
@@ -16469,6 +16590,11 @@ rc@^1.2.8:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-amap@^1.2.8:
+  version "1.2.8"
+  resolved "https://registry.npmmirror.com/react-amap/-/react-amap-1.2.8.tgz#cb5a6441c5e83acb0d7b2e8a1b20dc539b759d9b"
+  integrity sha512-uHPEUXti+CcwFyCeqGGqR0ACnXJA9D8S/lQYal9AG3XEOrwkaOFbWUavrvXxjcfAclROIWg8uKxzlRpMQnkHFg==
+
 react-attr-converter@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/react-attr-converter/-/react-attr-converter-0.3.1.tgz#4a2abf6d907b7ddae4d862dfec80e489ce41ad6e"
@@ -16610,6 +16736,13 @@ react-dom@^17.0.0:
     object-assign "^4.1.1"
     scheduler "^0.20.2"
 
+react-error-boundary@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.0.2.tgz#74399a4d9a68bfede1f3f4261ea0aabfc65d9868"
+  integrity sha512-KVzCusRTFpUYG0OFJbzbdRuxNQOBiGXVCqyNpBXM9z5NFsFLzMjUXMjx8gTja6M6WH+D2PvP3yKz4d8gD1PRaA==
+  dependencies:
+    "@babel/runtime" "^7.11.2"
+
 react-error-overlay@^6.0.9:
   version "6.0.10"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
@@ -16756,6 +16889,16 @@ react-native-swipeout@^2.2.2:
     prop-types "^15.5.10"
     react-tween-state "^0.1.5"
 
+react-reconciler@^0.25.1:
+  version "0.25.1"
+  resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.25.1.tgz#f9814d59d115e1210762287ce987801529363aaa"
+  integrity sha512-R5UwsIvRcSs3w8n9k3tBoTtUHdVhu9u84EG7E5M0Jk9F5i6DA1pQzPfUZd6opYWGy56MJOtV3VADzy6DRwYDjw==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.19.1"
+
 react-redux@=4.4.10:
   version "4.4.10"
   resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-4.4.10.tgz#ad57bd1db00c2d0aa7db992b360ce63dd0b80ec5"
@@ -17162,7 +17305,7 @@ regenerator-runtime@^0.11.0:
 
 regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
   version "0.13.9"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
   integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
 
 regenerator-transform@^0.15.0:
@@ -17468,7 +17611,7 @@ reserved-words@^0.1.2:
 
 resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
   version "1.5.1"
-  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
   integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
 
 resolve-cwd@^3.0.0:
@@ -17932,7 +18075,7 @@ screenfull@^5.0.0, screenfull@^5.1.0:
 
 scroll-into-view-if-needed@^2.2.25:
   version "2.2.29"
-  resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
+  resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
   integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==
   dependencies:
     compute-scroll-into-view "^1.0.17"
@@ -18156,7 +18299,7 @@ shallow-equal@^1.2.1:
 
 shallowequal@^1.0.1, shallowequal@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+  resolved "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
   integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
 
 sharkdown@^0.1.0:
@@ -18269,6 +18412,11 @@ signale@1.4.0:
     figures "^2.0.0"
     pkg-conf "^2.1.0"
 
+simple-statistics@^7.1.0:
+  version "7.7.5"
+  resolved "https://registry.yarnpkg.com/simple-statistics/-/simple-statistics-7.7.5.tgz#b34ad7dc18f4dea7bce2cbd376c467147062e839"
+  integrity sha512-CYq683Yg2mb7M4mklQ6FtxEdsYeziGa2giaLvqXobfK1qVqZDKd7BIqLnngnKQSw9GsfNinbiScbfjc3IRWdQA==
+
 simple-swizzle@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -18711,8 +18859,8 @@ string-argv@0.3.1:
 
 string-convert@^0.2.0:
   version "0.2.1"
-  resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
-  integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=
+  resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
+  integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
 
 string-length@^4.0.1:
   version "4.0.2"
@@ -19503,8 +19651,8 @@ to-regex@^3.0.1, to-regex@^3.0.2:
 
 toggle-selection@^1.0.6:
   version "1.0.6"
-  resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
-  integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
+  resolved "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+  integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
 
 toidentifier@1.0.0:
   version "1.0.0"