Bläddra i källkod

fix(merge): merge xyh

lind 3 år sedan
förälder
incheckning
c209106ed8
46 ändrade filer med 3295 tillägg och 97 borttagningar
  1. 1 0
      package.json
  2. 1 1
      src/app.tsx
  3. 1 0
      src/components/AMapComponent/PathSimplifier/PathNavigator.tsx
  4. 30 5
      src/components/AMapComponent/PathSimplifier/index.tsx
  5. 20 0
      src/components/AMapComponent/hooks/Distance.tsx
  6. 3 3
      src/components/RightContent/AvatarDropdown.tsx
  7. 51 9
      src/pages/Northbound/AliCloud/Detail/index.tsx
  8. 2 2
      src/pages/account/Center/edit/infoEdit.tsx
  9. 10 0
      src/pages/account/Center/index.less
  10. 26 2
      src/pages/account/Center/index.tsx
  11. 22 0
      src/pages/account/NotificationSubscription/save/index.tsx
  12. 61 32
      src/pages/demo/AMap/index.tsx
  13. 339 0
      src/pages/device/Instance/Detail/Modbus/index.tsx
  14. 332 0
      src/pages/device/Instance/Detail/Opcua/index.tsx
  15. 10 2
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  16. 12 0
      src/pages/device/Instance/Detail/index.tsx
  17. 1 0
      src/pages/device/Instance/service.ts
  18. 22 0
      src/pages/link/Certificate/Detail/index.tsx
  19. 2 0
      src/pages/link/Certificate/index.tsx
  20. 262 0
      src/pages/link/Channel/Modbus/Access/addPoint/index.tsx
  21. 108 0
      src/pages/link/Channel/Modbus/Access/bindDevice/index.tsx
  22. 24 0
      src/pages/link/Channel/Modbus/Access/index.less
  23. 369 0
      src/pages/link/Channel/Modbus/Access/index.tsx
  24. 173 0
      src/pages/link/Channel/Modbus/Save/index.tsx
  25. 30 0
      src/pages/link/Channel/Modbus/index.less
  26. 266 0
      src/pages/link/Channel/Modbus/index.tsx
  27. 51 0
      src/pages/link/Channel/Modbus/service.ts
  28. 254 0
      src/pages/link/Channel/Opcua/Access/addPoint/index.tsx
  29. 124 0
      src/pages/link/Channel/Opcua/Access/bindDevice/index.tsx
  30. 24 0
      src/pages/link/Channel/Opcua/Access/index.less
  31. 374 1
      src/pages/link/Channel/Opcua/Access/index.tsx
  32. 28 8
      src/pages/link/Channel/Opcua/Save/index.tsx
  33. 1 0
      src/pages/link/Channel/Opcua/index.tsx
  34. 59 0
      src/pages/link/Channel/Opcua/service.ts
  35. 1 1
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  36. 3 3
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  37. 9 5
      src/pages/rule-engine/Scene/Save/index.tsx
  38. 4 1
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  39. 15 5
      src/pages/rule-engine/Scene/Save/trigger/operation.tsx
  40. 35 9
      src/pages/system/Platforms/Api/basePage.tsx
  41. 7 1
      src/pages/system/Platforms/service.ts
  42. 1 1
      src/pages/user/Login/index.tsx
  43. 6 6
      src/pages/user/Login/user.d.ts
  44. 10 0
      src/utils/menu/index.ts
  45. 2 0
      src/utils/menu/router.ts
  46. 109 0
      yarn.lock

+ 1 - 0
package.json

@@ -62,6 +62,7 @@
     "@ant-design/pro-descriptions": "^1.6.8",
     "@ant-design/pro-form": "^1.18.3",
     "@ant-design/pro-layout": "^6.27.2",
+    "@ant-design/pro-list": "^1.21.61",
     "@formily/antd": "2.1.2",
     "@formily/core": "2.1.2",
     "@formily/json-schema": "2.1.2",

+ 1 - 1
src/app.tsx

@@ -269,7 +269,7 @@ export function render(oldRender: any) {
             url: '/demo',
           });
         }
-        extraRoutes = handleRoutes([...extraRouteArr, ...res.result]);
+        extraRoutes = handleRoutes([...res.result, ...extraRouteArr]);
         saveMenusCache(extraRoutes);
       }
       oldRender();

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

@@ -86,6 +86,7 @@ export default (props: PathNavigatorProps) => {
     return () => {
       if (PathNavigatorRef.current) {
         removeEvent();
+        PathNavigatorRef.current.destroy();
       }
     };
   }, []);

+ 30 - 5
src/components/AMapComponent/PathSimplifier/index.tsx

@@ -71,12 +71,29 @@ const PathSimplifier = (props: PathSimplifierProps) => {
     });
   };
 
+  const clear = () => {
+    if (pathSimplifierRef.current) {
+      setLoading(false);
+      pathSimplifierRef.current!.clearPathNavigators();
+      pathSimplifierRef.current?.setData([]);
+      props.__map__.remove(pathSimplifierRef.current);
+    }
+  };
+
   useEffect(() => {
-    if (pathSimplifierRef.current && props.pathData) {
-      pathSimplifierRef.current?.setData(
-        props.pathData.map((item) => ({ name: item.name || '路线', path: item.path })),
-      );
-      setLoading(true);
+    if (pathSimplifierRef.current) {
+      if (props.pathData && props.pathData.length) {
+        setLoading(false);
+        setTimeout(() => {
+          pathSimplifierRef.current?.setData(
+            props.pathData!.map((item) => ({ name: item.name || '路线', path: item.path })),
+          );
+          setLoading(true);
+        }, 10);
+      } else {
+        setLoading(false);
+        pathSimplifierRef.current.setData([]);
+      }
     }
   }, [props.pathData]);
 
@@ -86,6 +103,14 @@ const PathSimplifier = (props: PathSimplifierProps) => {
     }
   }, [__map__]);
 
+  useEffect(() => {
+    return () => {
+      if (props.__map__) {
+        clear();
+      }
+    };
+  }, []);
+
   return <>{loading && renderChildren()}</>;
 };
 

+ 20 - 0
src/components/AMapComponent/hooks/Distance.tsx

@@ -0,0 +1,20 @@
+import { useState } from 'react';
+
+const useDistance = () => {
+  const [distance, setDistance] = useState(0);
+
+  const onDistance = (data: number[][]) => {
+    if (window.AMap && data && data.length > 2) {
+      const pointArr = data.map((point) => new window.AMap.LngLat(point[0], point[1]));
+      const distanceOfLine = AMap.GeometryUtil.distanceOfLine(pointArr);
+      setDistance(Math.round(distanceOfLine));
+    }
+  };
+
+  return {
+    distance,
+    onDistance,
+  };
+};
+
+export default useDistance;

+ 3 - 3
src/components/RightContent/AvatarDropdown.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback } from 'react';
-import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
 import { Avatar, Menu, Spin } from 'antd';
 import { history, useModel } from 'umi';
 import { stringify } from 'querystring';
@@ -87,7 +87,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
           })}
         </Menu.Item>
       )}
-      {menu && (
+      {/* {menu && (
         <Menu.Item key="settings">
           <SettingOutlined />
           {intl.formatMessage({
@@ -95,7 +95,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
             defaultMessage: '个人设置',
           })}
         </Menu.Item>
-      )}
+      )} */}
       {menu && <Menu.Divider />}
 
       <Menu.Item key="logout">

+ 51 - 9
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -134,6 +134,10 @@ const Detail = observer(() => {
             max: 64,
             message: '最多可输入64个字符',
           },
+          {
+            required: true,
+            message: '请输入名称',
+          },
         ],
       },
       accessConfig: {
@@ -155,6 +159,12 @@ const Detail = observer(() => {
               tooltip: '阿里云内部给每台机器设置的唯一编号',
             },
             'x-reactions': ['{{useAsyncDataSource(queryRegionsList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择服务地址',
+              },
+            ],
           },
           accessKeyId: {
             type: 'string',
@@ -170,6 +180,10 @@ const Detail = observer(() => {
                 max: 64,
                 message: '最多可输入64个字符',
               },
+              {
+                required: true,
+                message: '请输入accessKey',
+              },
             ],
             'x-decorator-props': {
               tooltip: '用于程序通知方式调用云服务API的用户标识',
@@ -186,6 +200,10 @@ const Detail = observer(() => {
             },
             'x-validator': [
               {
+                required: true,
+                message: '请输入accessSecret',
+              },
+              {
                 max: 64,
                 message: '最多可输入64个字符',
               },
@@ -211,26 +229,36 @@ const Detail = observer(() => {
         'x-decorator-props': {
           tooltip: '物联网平台对应的阿里云产品',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择网桥产品',
+          },
+        ],
       },
       mappings: {
         type: 'array',
         required: true,
         'x-component': 'ArrayCollapse',
-        title: '产品映射',
+        'x-decorator': 'FormItem',
         items: {
           type: 'object',
-          required: true,
           'x-component': 'ArrayCollapse.CollapsePanel',
           'x-component-props': {
             header: '产品映射',
           },
           properties: {
-            grid: {
+            index: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Index',
+            },
+            layout: {
               type: 'void',
-              'x-component': 'FormGrid',
-              'x-component-props': {
-                minColumns: [24],
-                maxColumns: [24],
+              'x-decorator': 'FormGrid',
+              'x-decorator-props': {
+                maxColumns: 2,
+                minColumns: 2,
+                columnGap: 24,
               },
               properties: {
                 type: 'object',
@@ -247,10 +275,17 @@ const Detail = observer(() => {
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
                   'x-decorator-props': {
-                    gridSpan: 12,
+                    layout: 'vertical',
+                    labelAlign: 'left',
                     tooltip: '阿里云物联网平台产品标识',
                   },
                   'x-reactions': ['{{useAsyncDataSource(queryAliyunProductList)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择阿里云产品',
+                    },
+                  ],
                 },
                 productId: {
                   type: 'string',
@@ -259,7 +294,8 @@ const Detail = observer(() => {
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
                   'x-decorator-props': {
-                    gridSpan: 12,
+                    layout: 'vertical',
+                    labelAlign: 'left',
                   },
                   'x-component-props': {
                     placeholder: '请选择平台产品',
@@ -268,6 +304,12 @@ const Detail = observer(() => {
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
                   'x-reactions': ['{{useAsyncDataSource(queryProductList)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择平台产品',
+                    },
+                  ],
                 },
               },
             },

+ 2 - 2
src/pages/account/Center/edit/infoEdit.tsx

@@ -65,12 +65,12 @@ const InfoEdit = (props: Props) => {
         <Row gutter={[24, 24]}>
           <Col span={12}>
             <Form.Item label="角色" name="role">
-              <Input placeholder="请输入姓名" disabled />
+              <Input placeholder="请输入角色" disabled />
             </Form.Item>
           </Col>
           <Col span={12}>
             <Form.Item label="部门" name="org">
-              <Input placeholder="请输入用户名" disabled />
+              <Input placeholder="请输入部门" disabled />
             </Form.Item>
           </Col>
         </Row>

+ 10 - 0
src/pages/account/Center/index.less

@@ -18,6 +18,16 @@
   .content {
     width: 80%;
     padding-top: 15px;
+
+    :global {
+      .ant-descriptions-item-label::after {
+        content: none;
+      }
+
+      .ant-descriptions-item-content {
+        color: #666363d9;
+      }
+    }
   }
 
   .action {

+ 26 - 2
src/pages/account/Center/index.tsx

@@ -21,10 +21,12 @@ import InfoEdit from './edit/infoEdit';
 import PasswordEdit from './edit/passwordEdit';
 import Service from '@/pages/account/Center/service';
 import moment from 'moment';
+import { useModel } from 'umi';
 
 export const service = new Service();
 
 const Center = () => {
+  const { initialState, setInitialState } = useModel('@@initialState');
   const [data, setData] = useState<any>();
   const [imageUrl, setImageUrl] = useState<string>('');
   // const [loading, setLoading] = useState<boolean>(false)
@@ -42,6 +44,7 @@ const Center = () => {
 
   const uploadProps: UploadProps = {
     showUploadList: false,
+    accept: 'image/jpeg,image/png',
     action: `/${SystemConst.API_BASE}/file/static`,
     headers: {
       'X-Access-Token': Token.get(),
@@ -79,6 +82,12 @@ const Center = () => {
     service.getUserDetail().subscribe((res) => {
       setData(res.result);
       setImageUrl(res.result.avatar);
+      // setInitialState({
+      //   ...initialState,
+      //   currentUser:{
+
+      //   }
+      // })
     });
   };
   const saveInfo = (parms: UserDetail) => {
@@ -102,7 +111,6 @@ const Center = () => {
   const getBindInfo = () => {
     service.bindInfo().then((res) => {
       if (res.status === 200) {
-        console.log(res);
         setBindList(res.result);
       }
     });
@@ -121,6 +129,22 @@ const Center = () => {
     getBindInfo();
   }, []);
 
+  useEffect(() => {
+    if (data?.name) {
+      const item = {
+        ...initialState?.currentUser?.user,
+        name: data.name,
+      };
+      setInitialState({
+        ...initialState,
+        currentUser: {
+          ...initialState?.currentUser,
+          user: item,
+        },
+      });
+    }
+  }, [data]);
+
   return (
     <PageContainer>
       <Card>
@@ -141,7 +165,7 @@ const Center = () => {
             </Upload>
           </div>
           <div className={styles.content}>
-            <Descriptions column={4} layout="vertical">
+            <Descriptions column={4} layout="vertical" labelStyle={{ fontWeight: 600 }}>
               <Descriptions.Item label="登录账号">{data?.username}</Descriptions.Item>
               <Descriptions.Item label="账号ID">{data?.id}</Descriptions.Item>
               <Descriptions.Item label="注册时间">

+ 22 - 0
src/pages/account/NotificationSubscription/save/index.tsx

@@ -65,6 +65,10 @@ const Save = (props: Props) => {
             },
             'x-validator': [
               {
+                required: true,
+                message: '请输入名称',
+              },
+              {
                 max: 64,
                 message: '最多可输入64个字符',
               },
@@ -84,6 +88,12 @@ const Save = (props: Props) => {
               placeholder: '请选择类型',
             },
             'x-reactions': ['{{useAsyncDataSource(queryProvidersList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择类型',
+              },
+            ],
           },
           'topicConfig.alarmConfigId': {
             title: '告警规则',
@@ -99,6 +109,12 @@ const Save = (props: Props) => {
               placeholder: '请选择告警规则',
             },
             'x-reactions': ['{{useAsyncDataSource(queryAlarmConfigList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择告警规则',
+              },
+            ],
           },
           notice: {
             title: '通知方式',
@@ -127,6 +143,12 @@ const Save = (props: Props) => {
               labelAlign: 'left',
               layout: 'vertical',
             },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择通知方式',
+              },
+            ],
           },
         },
       },

+ 61 - 32
src/pages/demo/AMap/index.tsx

@@ -1,39 +1,47 @@
 import { AMap, PathSimplifier } from '@/components';
 import { usePlaceSearch } from '@/components/AMapComponent/hooks';
 import { Marker } from 'react-amap';
-import { useState } from 'react';
-import { Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { Button, Select } from 'antd';
 import { debounce } from 'lodash';
+import useDistance from '@/components/AMapComponent/hooks/Distance';
 
 export default () => {
-  const [speed] = useState(100000);
+  const { distance, onDistance } = useDistance();
+  const [speed, setSpeed] = useState(100000);
   const [markerCenter, setMarkerCenter] = useState<any>({ longitude: 0, latitude: 0 });
   const [map, setMap] = useState(null);
+  const [show, setShow] = useState(false);
 
   const { data, search } = usePlaceSearch(map);
 
+  const paths = [
+    [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],
+  ];
+
   const onSearch = (value: string) => {
     search(value);
   };
-  console.log(data);
+
+  useEffect(() => {
+    setSpeed((distance / 5) * 3.6);
+  }, [distance]);
 
   const [pathData] = useState([
     {
       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],
-      ],
+      path: paths,
     },
   ]);
 
@@ -45,7 +53,9 @@ export default () => {
           height: 500,
           width: '100%',
         }}
-        onInstanceCreated={setMap}
+        onInstanceCreated={(_map) => {
+          setMap(_map);
+        }}
         events={{
           click: (e: any) => {
             setMarkerCenter({
@@ -59,19 +69,24 @@ export default () => {
           // @ts-ignore
           <Marker position={markerCenter} />
         ) : null}
-        <PathSimplifier pathData={pathData}>
-          <PathSimplifier.PathNavigator
-            speed={speed}
-            onCreate={(nav) => {
-              setTimeout(() => {
-                nav.pause();
-              }, 5000);
-              setTimeout(() => {
-                nav.resume(); // 恢复
-              }, 7000);
-            }}
-          />
-        </PathSimplifier>
+        {show && (
+          <PathSimplifier pathData={pathData}>
+            <PathSimplifier.PathNavigator
+              isAuto={false}
+              speed={speed}
+              onCreate={(nav) => {
+                onDistance(paths);
+
+                setTimeout(() => {
+                  nav.start();
+                }, 300);
+                // setTimeout(() => {
+                //   nav.resume(); // 恢复
+                // }, 7000);
+              }}
+            />
+          </PathSimplifier>
+        )}
       </AMap>
       <div style={{ position: 'absolute', top: 0 }}>
         <Select
@@ -84,6 +99,20 @@ export default () => {
             console.log(key, node);
           }}
         />
+        <Button
+          onClick={() => {
+            setShow(false);
+          }}
+        >
+          清除
+        </Button>
+        <Button
+          onClick={() => {
+            setShow(true);
+          }}
+        >
+          显示
+        </Button>
       </div>
     </div>
   );

+ 339 - 0
src/pages/device/Instance/Detail/Modbus/index.tsx

@@ -0,0 +1,339 @@
+import PermissionButton from '@/components/PermissionButton';
+import { Badge, Card, Empty, message, Tabs } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { useIntl } from 'umi';
+import styles from '@/pages/link/Channel/Opcua/Access/index.less';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { service } from '@/pages/link/Channel/Modbus';
+import Save from '@/pages/link/Channel/Modbus/Save';
+import { InstanceModel } from '@/pages/device/Instance';
+import AddPoint from '@/pages/link/Channel/Opcua/Access/addPoint';
+
+const Modbus = () => {
+  const intl = useIntl();
+  const { permission } = PermissionButton.usePermission('link/Channel/Modbus');
+  const [bindList, setBindList] = useState<any>([]);
+  const [opcId, setOpcId] = useState<string>('');
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [channel, setChannel] = useState<any>({});
+  const [pointVisiable, setPointVisiable] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
+  const [deviceId, setDeviceId] = useState<any>('');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '属性ID',
+      dataIndex: 'metadataId',
+    },
+    {
+      title: '功能码',
+      render: (record: any) => <>{record.function?.text}</>,
+    },
+    {
+      title: '读取起始位置',
+      render: (record: any) => <>{record.codecConfig?.readIndex}</>,
+    },
+    {
+      title: '读取长度',
+      render: (record: any) => <>{record.codecConfig?.readLength}</>,
+    },
+    {
+      title: '值',
+      // dataIndex: '4',
+      //   render: (record: any) => <>{propertyValue[record.property]}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            setPointVisiable(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'disabled' ? 'disabled' : 'enabled'
+              }.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              const item = {
+                ...record,
+                state: record.state.value === 'enabled' ? 'disabled' : 'enabled',
+              };
+              await service.saveMetadataConfig(opcId, deviceId, item);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'disabled' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'enabled'}
+          tooltip={{
+            title:
+              record.state.value === 'disabled'
+                ? intl.formatMessage({
+                    id: 'pages.data.option.remove',
+                    defaultMessage: '删除',
+                  })
+                : '请先禁用该组件,再删除。',
+          }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              const resp: any = await service.removeMetadataConfig(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const getModbus = () => {
+    service
+      .query({
+        paging: false,
+      })
+      .then((res) => {
+        setBindList(res.result.data);
+        setOpcId(res.result?.[0]?.id);
+        // setParam({
+        //     sorts: [{ name: 'createTime', order: 'desc' }],
+        // })
+      });
+  };
+
+  useEffect(() => {
+    const { id } = InstanceModel.detail;
+    setDeviceId(id);
+    if (id) {
+      getModbus();
+    }
+  }, [visible]);
+
+  // useEffect(() => {
+  //     if(opcId){
+  //         setLoading(false)
+  //     }
+  //  }, [opcId])
+
+  return (
+    <Card>
+      <PermissionButton
+        onClick={() => {
+          setVisible(true);
+          setChannel({});
+        }}
+        isPermission={permission.add}
+        key="add"
+        icon={<PlusOutlined />}
+        type="dashed"
+        style={{ width: '200px', marginLeft: 20, marginBottom: 5 }}
+      >
+        新增通道
+      </PermissionButton>
+      {bindList.length > 0 ? (
+        <Tabs
+          tabPosition={'left'}
+          defaultActiveKey={opcId}
+          onChange={(e) => {
+            setOpcId(e);
+            setParam({
+              terms: [{ column: 'opcUaId', value: e }],
+            });
+          }}
+        >
+          {bindList.map((item: any) => (
+            <Tabs.TabPane
+              key={item.id}
+              tab={
+                <div className={styles.left}>
+                  <div style={{ width: '100px', textAlign: 'left' }}>{item.name}</div>
+                  <PermissionButton
+                    isPermission={permission.update}
+                    key="edit"
+                    onClick={() => {
+                      setVisible(true);
+                      setChannel(item);
+                    }}
+                    type={'link'}
+                    style={{ padding: 0 }}
+                    tooltip={{
+                      title: intl.formatMessage({
+                        id: 'pages.data.option.edit',
+                        defaultMessage: '编辑',
+                      }),
+                    }}
+                  >
+                    <EditOutlined />
+                  </PermissionButton>
+                  <PermissionButton
+                    isPermission={permission.delete}
+                    style={{ padding: 0 }}
+                    popConfirm={{
+                      title: '确认删除',
+                      onConfirm: async () => {
+                        const resp: any = await service.remove(item.id);
+                        if (resp.status === 200) {
+                          getModbus();
+                          message.success(
+                            intl.formatMessage({
+                              id: 'pages.data.option.success',
+                              defaultMessage: '操作成功!',
+                            }),
+                          );
+                        }
+                      },
+                    }}
+                    key="delete"
+                    type="link"
+                  >
+                    <DeleteOutlined />
+                  </PermissionButton>
+                </div>
+              }
+            >
+              <ProTable
+                actionRef={actionRef}
+                // loading={loading}
+                params={param}
+                columns={columns}
+                rowKey="id"
+                search={false}
+                headerTitle={
+                  <>
+                    <PermissionButton
+                      onClick={() => {
+                        setPointVisiable(true);
+                        setCurrent({});
+                      }}
+                      isPermission={permission.add}
+                      key="add"
+                      icon={<PlusOutlined />}
+                      type="primary"
+                    >
+                      {intl.formatMessage({
+                        id: 'pages.data.option.add',
+                        defaultMessage: '新增',
+                      })}
+                    </PermissionButton>
+                  </>
+                }
+                request={async (params) => {
+                  console.log(opcId);
+                  // setTimeout(() => {
+                  //     const master =
+                  // }, 10);
+                  const res = await service.queryMetadataConfig(opcId, deviceId, {
+                    ...params,
+                    sorts: [{ name: 'createTime', order: 'desc' }],
+                  });
+                  // setData(res.result.data);
+                  return {
+                    code: res.message,
+                    result: {
+                      data: res.result.data,
+                      pageIndex: 0,
+                      pageSize: 0,
+                      total: 0,
+                    },
+                    status: res.status,
+                  };
+                }}
+              />
+            </Tabs.TabPane>
+          ))}
+        </Tabs>
+      ) : (
+        <Empty />
+      )}
+      {visible && (
+        <Save
+          data={channel}
+          close={() => {
+            setVisible(false);
+          }}
+          device={InstanceModel.detail}
+        />
+      )}
+      {pointVisiable && (
+        <AddPoint
+          deviceId={deviceId}
+          opcUaId={opcId}
+          data={current}
+          close={() => {
+            setPointVisiable(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </Card>
+  );
+};
+export default Modbus;

+ 332 - 0
src/pages/device/Instance/Detail/Opcua/index.tsx

@@ -0,0 +1,332 @@
+import PermissionButton from '@/components/PermissionButton';
+import { Badge, Card, Empty, message, Tabs } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { useIntl } from 'umi';
+import styles from '@/pages/link/Channel/Opcua/Access/index.less';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { service } from '@/pages/link/Channel/Opcua';
+import Save from '@/pages/link/Channel/Opcua/Save';
+import { InstanceModel } from '@/pages/device/Instance';
+import AddPoint from '@/pages/link/Channel/Opcua/Access/addPoint';
+
+const Opcua = () => {
+  const intl = useIntl();
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+  const [bindList, setBindList] = useState<any>([]);
+  const [opcId, setOpcId] = useState<string>('');
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [channel, setChannel] = useState<any>({});
+  const [pointVisiable, setPointVisiable] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
+  const [deviceId, setDeviceId] = useState<any>('');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '属性ID',
+      dataIndex: 'property',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'OPC点位ID',
+      dataIndex: 'opcPointId',
+    },
+    {
+      title: '数据类型',
+      dataIndex: 'dataType',
+    },
+    {
+      title: '值',
+      // dataIndex: '4',
+      //   render: (record: any) => <>{propertyValue[record.property]}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disable' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            setPointVisiable(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disable' ? 'disable' : 'good'}.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              if (record.state.value === 'disable') {
+                await service.enablePoint(record.deviceId, [record.id]);
+              } else {
+                await service.stopPoint(record.deviceId, [record.id]);
+              }
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disable' ? 'disable' : 'good'}`,
+              defaultMessage: record.state.value !== 'disable' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disable' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'good'}
+          tooltip={{
+            title:
+              record.state.value === 'disable'
+                ? intl.formatMessage({
+                    id: 'pages.data.option.remove',
+                    defaultMessage: '删除',
+                  })
+                : '请先禁用该组件,再删除。',
+          }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              const resp: any = await service.deletePoint(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const getOpc = (id: string) => {
+    service
+      .noPagingOpcua({
+        paging: false,
+        terms: [
+          {
+            column: 'id$bind-opc',
+            value: id,
+          },
+        ],
+      })
+      .then((res) => {
+        setBindList(res.result);
+        setOpcId(res.result?.[0]?.id);
+        setParam({
+          terms: [{ column: 'opcUaId', value: res.result?.[0]?.id }],
+        });
+      });
+  };
+
+  useEffect(() => {
+    const { id } = InstanceModel.detail;
+    setDeviceId(id);
+    if (id) {
+      getOpc(id);
+    }
+  }, [visible]);
+
+  return (
+    <Card>
+      <PermissionButton
+        onClick={() => {
+          setVisible(true);
+          setChannel({});
+        }}
+        isPermission={permission.add}
+        key="add"
+        icon={<PlusOutlined />}
+        type="dashed"
+        style={{ width: '200px', marginLeft: 20, marginBottom: 5 }}
+      >
+        新增通道
+      </PermissionButton>
+      {bindList.length > 0 ? (
+        <Tabs
+          tabPosition={'left'}
+          defaultActiveKey={opcId}
+          onChange={(e) => {
+            setOpcId(e);
+            setParam({
+              terms: [{ column: 'opcUaId', value: e }],
+            });
+          }}
+        >
+          {bindList.map((item: any) => (
+            <Tabs.TabPane
+              key={item.id}
+              tab={
+                <div className={styles.left}>
+                  <div style={{ width: '100px', textAlign: 'left' }}>{item.name}</div>
+                  <PermissionButton
+                    isPermission={permission.update}
+                    key="edit"
+                    onClick={() => {
+                      setVisible(true);
+                      setChannel(item);
+                    }}
+                    type={'link'}
+                    style={{ padding: 0 }}
+                    tooltip={{
+                      title: intl.formatMessage({
+                        id: 'pages.data.option.edit',
+                        defaultMessage: '编辑',
+                      }),
+                    }}
+                  >
+                    <EditOutlined />
+                  </PermissionButton>
+                  <PermissionButton
+                    isPermission={permission.delete}
+                    style={{ padding: 0 }}
+                    popConfirm={{
+                      title: '确认删除',
+                      onConfirm: async () => {
+                        const resp: any = await service.remove(item.id);
+                        if (resp.status === 200) {
+                          getOpc(deviceId);
+                          message.success(
+                            intl.formatMessage({
+                              id: 'pages.data.option.success',
+                              defaultMessage: '操作成功!',
+                            }),
+                          );
+                        }
+                      },
+                    }}
+                    key="delete"
+                    type="link"
+                  >
+                    <DeleteOutlined />
+                  </PermissionButton>
+                </div>
+              }
+            >
+              <ProTable
+                actionRef={actionRef}
+                params={param}
+                columns={columns}
+                rowKey="id"
+                search={false}
+                headerTitle={
+                  <>
+                    <PermissionButton
+                      onClick={() => {
+                        setPointVisiable(true);
+                        setCurrent({});
+                      }}
+                      isPermission={permission.add}
+                      key="add"
+                      icon={<PlusOutlined />}
+                      type="primary"
+                    >
+                      {intl.formatMessage({
+                        id: 'pages.data.option.add',
+                        defaultMessage: '新增',
+                      })}
+                    </PermissionButton>
+                  </>
+                }
+                request={async (params) => {
+                  const res = await service.PointList({
+                    ...params,
+                    sorts: [{ name: 'createTime', order: 'desc' }],
+                  });
+                  // setData(res.result.data);
+                  return {
+                    code: res.message,
+                    result: {
+                      data: res.result.data,
+                      pageIndex: 0,
+                      pageSize: 0,
+                      total: 0,
+                    },
+                    status: res.status,
+                  };
+                }}
+              />
+            </Tabs.TabPane>
+          ))}
+        </Tabs>
+      ) : (
+        <Empty />
+      )}
+      {visible && (
+        <Save
+          data={channel}
+          close={() => {
+            setVisible(false);
+          }}
+          device={InstanceModel.detail}
+        />
+      )}
+      {pointVisiable && (
+        <AddPoint
+          deviceId={deviceId}
+          opcUaId={opcId}
+          data={current}
+          close={() => {
+            setPointVisiable(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </Card>
+  );
+};
+export default Opcua;

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

@@ -1,7 +1,7 @@
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import styles from './index.less';
 import Detail from './Detail';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { message, Tooltip } from 'antd';
 
 interface Props {
@@ -29,6 +29,11 @@ const FileComponent = (props: Props) => {
   const [type, setType] = useState<string>('other');
   const [visible, setVisible] = useState<boolean>(false);
   const isHttps = document.location.protocol === 'https:';
+  const [temp, setTemp] = useState<boolean>(false);
+
+  useEffect(() => {
+    setTemp(false);
+  }, [props.value]);
 
   const renderValue = () => {
     if (!value?.formatValue) {
@@ -46,7 +51,7 @@ const FileComponent = (props: Props) => {
           </div>
         );
       }
-      if (['.jpg', '.png'].some((item) => value?.formatValue.includes(item))) {
+      if (['.jpg', '.png', '.swf', '.tiff'].some((item) => value?.formatValue.includes(item))) {
         // 图片
         return (
           <div
@@ -54,6 +59,8 @@ const FileComponent = (props: Props) => {
             onClick={() => {
               if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
                 message.error('域名为https时,不支持访问http地址');
+              } else if (temp) {
+                message.error('该图片无法访问');
               } else {
                 const flag =
                   ['.jpg', '.png'].find((item) => value?.formatValue.includes(item)) || '';
@@ -66,6 +73,7 @@ const FileComponent = (props: Props) => {
               src={value?.formatValue}
               onError={(e: any) => {
                 e.target.src = imgMap.get('error');
+                setTemp(true);
               }}
             />
           </div>

+ 12 - 0
src/pages/device/Instance/Detail/index.tsx

@@ -13,6 +13,8 @@ import Running from '@/pages/device/Instance/Detail/Running';
 import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
 import Diagnose from '@/pages/device/Instance/Detail/Diagnose';
 import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
+import Opcua from '@/pages/device/Instance/Detail/Opcua';
+import Modbus from '@/pages/device/Instance/Detail/Modbus';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
@@ -147,6 +149,16 @@ const InstanceDetail = observer(() => {
       tab: '物模型映射',
       component: <MetadataMap type="device" />,
     },
+    {
+      key: 'opcua',
+      tab: 'OPC UA',
+      component: <Opcua />,
+    },
+    {
+      key: 'modbus',
+      tab: 'Modbus',
+      component: <Modbus />,
+    },
   ];
   const [list, setList] =
     useState<{ key: string; tab: string | ReactNode; component: ReactNode }[]>(baseList);

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

@@ -279,6 +279,7 @@ class Service extends BaseService<DeviceInstance> {
       method: 'POST',
       data: {
         paging: false,
+        sorts: [{ name: 'name', order: 'asc' }],
       },
     });
   // 保存设备的物模型指标

+ 22 - 0
src/pages/link/Certificate/Detail/index.tsx

@@ -50,6 +50,12 @@ const Detail = observer(() => {
         default: 'common',
         'x-decorator': 'FormItem',
         'x-component': 'Standard',
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择证书标准',
+          },
+        ],
       },
       name: {
         type: 'string',
@@ -62,6 +68,10 @@ const Detail = observer(() => {
         },
         'x-validator': [
           {
+            required: true,
+            message: '请输入证书名称',
+          },
+          {
             max: 64,
             message: '最多可输入64个字符',
           },
@@ -77,6 +87,12 @@ const Detail = observer(() => {
           placeholder:
             '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请上传证书文件',
+          },
+        ],
       },
       'configs.key': {
         title: '证书私钥',
@@ -88,6 +104,12 @@ const Detail = observer(() => {
           placeholder:
             '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请输入证书私钥',
+          },
+        ],
       },
       description: {
         title: '说明',

+ 2 - 0
src/pages/link/Certificate/index.tsx

@@ -24,6 +24,7 @@ const Certificate = () => {
     {
       dataIndex: 'type',
       title: '证书标准',
+      render: (text: any) => <span>{text?.text || '-'}</span>,
     },
     {
       dataIndex: 'name',
@@ -101,6 +102,7 @@ const Certificate = () => {
         params={param}
         columns={columns}
         search={false}
+        rowKey="id"
         headerTitle={
           <PermissionButton
             onClick={() => {

+ 262 - 0
src/pages/link/Channel/Modbus/Access/addPoint/index.tsx

@@ -0,0 +1,262 @@
+import { Col, Form, Input, InputNumber, message, Modal, Row, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/Channel/Modbus';
+
+interface Props {
+  data: any;
+  deviceId: string;
+  opcUaId: string;
+  close: Function;
+}
+
+const AddPoint = (props: Props) => {
+  const { opcUaId, deviceId, data } = props;
+  const [form] = Form.useForm();
+  const [property, setProperty] = useState<any>([]);
+
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    service
+      .saveMetadataConfig(opcUaId, deviceId, {
+        ...data,
+        ...formData,
+        metadataType: 'property',
+        codec: 'number',
+      })
+      .then((res) => {
+        if (res.status === 200) {
+          message.success('操作成功!');
+          props.close();
+        }
+      });
+    console.log(formData);
+  };
+
+  useEffect(() => {
+    service.deviceDetail(props.deviceId).then((res) => {
+      if (res.result.metadata) {
+        const item = JSON.parse(res.result?.metadata);
+        setProperty(item.properties);
+      }
+    });
+  }, []);
+  return (
+    <Modal
+      title={props.data.id ? '编辑' : '新增'}
+      visible
+      width="40vw"
+      destroyOnClose
+      onOk={handleSave}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          ...props.data,
+          initialValue: props.data?.configuration?.initialValue,
+          multiple: props.data?.configuration?.multiple,
+        }}
+      >
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="属性"
+              name="metadataId"
+              required
+              rules={[{ required: true, message: '属性必选' }]}
+            >
+              <Select placeholder="请选择属性">
+                {property.map((item: any) => (
+                  <Select.Option value={item.id} key={item.id}>
+                    {item.name}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="功能码"
+              name="function"
+              required
+              rules={[{ required: true, message: '功能码必选' }]}
+            >
+              <Select placeholder="请选择">
+                <Select.Option value={'Coils'}>线圈寄存器</Select.Option>
+                <Select.Option value={'HoldingRegisters'}>保存寄存器</Select.Option>
+                <Select.Option value={'InputRegisters'}>输入寄存器</Select.Option>
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item
+              label="从站ID"
+              name="unitId"
+              required
+              rules={[
+                { required: true, message: '从站ID' },
+                ({}) => ({
+                  validator(_, value) {
+                    if (value !== 0 || /(^[1-9]\d*$)/.test(value)) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入非0正整数'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" min={0} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="读取起始位置"
+              name={['codecConfig', 'readIndex']}
+              required
+              rules={[
+                { required: true, message: '请输入读取起始位置' },
+                ({}) => ({
+                  validator(_, value) {
+                    if (/(^[1-9]\d*$)/.test(value)) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入正整数'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" min={0} />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="读取长度"
+              name={['codecConfig', 'readLength']}
+              required
+              rules={[
+                { required: true, message: '请输入读取长度' },
+                ({}) => ({
+                  validator(_, value) {
+                    if (value !== 0 || /(^[1-9]\d*$)/.test(value)) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入正整数'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" min={1} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item
+              label="地址"
+              name="address"
+              tooltip="要获取的对象地址"
+              rules={[
+                { required: true, message: '请输入读取长度' },
+                ({}) => ({
+                  validator(_, value) {
+                    if (value > 1 && value < 255) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入1~255之间的数字'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" min={1} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="变换器寄存器高低字节"
+              name={['codecConfig', 'revertBytes']}
+              required
+              rules={[{ required: true, message: '变换器寄存器高低字节必填' }]}
+            >
+              <Select placeholder="请选择">
+                <Select.Option value={false}>否</Select.Option>
+                <Select.Option value={true}>是</Select.Option>
+              </Select>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="缩放因子"
+              name={['codecConfig', 'scaleFactor']}
+              required
+              tooltip="基于原始数据按比例进行数据缩放。默认比例为1,不能为0"
+              rules={[
+                { required: true, message: '请输入缩放因子' },
+                ({}) => ({
+                  validator(_, value) {
+                    if (value !== 0) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入正整数'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="数据格式转换"
+              name={['codecConfig', 'unsigned']}
+              required
+              rules={[{ required: true, message: '数据格式转换必填' }]}
+            >
+              <Select placeholder="请选择">
+                <Select.Option value={false}>无符号数字</Select.Option>
+                <Select.Option value={true}>有符号数字</Select.Option>
+              </Select>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="读取数据周期"
+              name="interval"
+              tooltip="若不填写表示不定时拉取数据"
+              rules={[
+                ({}) => ({
+                  validator(_, value) {
+                    if (value !== 0 || /(^[1-9]\d*$)/.test(value)) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('请输入正整数'));
+                  },
+                }),
+              ]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入" addonAfter={<>ms</>} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item label="说明" name="description">
+              <Input.TextArea maxLength={200} />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+};
+export default AddPoint;

+ 108 - 0
src/pages/link/Channel/Modbus/Access/bindDevice/index.tsx

@@ -0,0 +1,108 @@
+import { Modal } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, message } from 'antd';
+import { useRef, useState } from 'react';
+import { service } from '@/pages/link/Channel/Modbus';
+import moment from 'moment';
+
+interface Props {
+  id: string;
+  close: () => void;
+}
+
+const BindDevice = (props: Props) => {
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [keys, setKeys] = useState<any>([]);
+
+  const statusMap = new Map();
+  statusMap.set('在线', 'success');
+  statusMap.set('离线', 'error');
+  statusMap.set('未激活', 'processing');
+  statusMap.set('online', 'success');
+  statusMap.set('offline', 'error');
+  statusMap.set('notActive', 'processing');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '设备ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '产品名称',
+      dataIndex: 'productName',
+    },
+    {
+      title: '注册时间',
+      // dataIndex: 'registryTime',
+      render: (_, record) => (
+        <>{record.registryTime ? moment(record.registryTime).format('YYYY-MM-DD HH:mm:ss') : '-'}</>
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => <Badge text={state?.text} status={statusMap.get(state.value)} />,
+    },
+  ];
+
+  const save = () => {
+    service.bind(keys, props.id).then((res) => {
+      if (res.status === 200) {
+        message.success('绑定成功');
+        props.close();
+      }
+    });
+  };
+
+  return (
+    <Modal
+      title={'绑定设备'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={() => {
+        save();
+      }}
+      width={1300}
+      permissionCode={'link/Channel/Modbus'}
+      permission={['add', 'edit', 'view']}
+    >
+      <SearchComponent
+        field={columns}
+        target="bindDevice"
+        defaultParam={[{ column: 'id$modbus-master$not', value: props.id }]}
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        request={async (params) =>
+          service.getDevice({
+            ...params,
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          })
+        }
+        rowSelection={{
+          selectedRowKeys: keys,
+          onChange: (selectedRowKeys) => {
+            setKeys(selectedRowKeys);
+          },
+        }}
+      />
+    </Modal>
+  );
+};
+export default BindDevice;

+ 24 - 0
src/pages/link/Channel/Modbus/Access/index.less

@@ -0,0 +1,24 @@
+.list {
+  :global {
+    .ant-tabs-tab .ant-tabs-tab-active .ant-tabs-tab-btn {
+      text-shadow: 0 0 0 currentColor;
+    }
+  }
+}
+
+.left {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 200px;
+
+  .icon {
+    display: none;
+  }
+}
+
+.left:hover {
+  .icon {
+    display: block;
+  }
+}

+ 369 - 0
src/pages/link/Channel/Modbus/Access/index.tsx

@@ -0,0 +1,369 @@
+import PermissionButton from '@/components/PermissionButton';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, Card, Empty, Input, message, Popconfirm, Tabs } from 'antd';
+import { useIntl, useLocation } from 'umi';
+import { useEffect, useRef, useState } from 'react';
+import {
+  DeleteOutlined,
+  DisconnectOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import BindDevice from '@/pages/link/Channel/Modbus/Access/bindDevice';
+import { service } from '@/pages/link/Channel/Modbus';
+import encodeQuery from '@/utils/encodeQuery';
+import styles from './index.less';
+import AddPoint from './addPoint';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { map } from 'rxjs/operators';
+
+const Access = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const location = useLocation<string>();
+  const [param, setParam] = useState({});
+  const [opcUaId, setOpcUaId] = useState<any>('');
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+  const [deviceVisiable, setDeviceVisiable] = useState<boolean>(false);
+  const [pointVisiable, setPointVisiable] = useState<boolean>(false);
+  const [bindList, setBindList] = useState<any>([]);
+  const [deviceId, setDeviceId] = useState<string>('');
+  const [productId, setProductId] = useState<string>('');
+  const [current, setCurrent] = useState<any>({});
+  const [data, setData] = useState<any>([]);
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [propertyValue, setPropertyValue] = useState<any>({});
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '属性ID',
+      dataIndex: 'metadataId',
+    },
+    {
+      title: '功能码',
+      render: (record: any) => <>{record.function?.text}</>,
+    },
+    {
+      title: '读取起始位置',
+      render: (record: any) => <>{record.codecConfig?.readIndex}</>,
+    },
+    {
+      title: '读取长度',
+      render: (record: any) => <>{record.codecConfig?.readLength}</>,
+    },
+    {
+      title: '值',
+      // dataIndex: '4',
+      render: (record: any) => <>{propertyValue[record.property]}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            setPointVisiable(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'disabled' ? 'disabled' : 'enabled'
+              }.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              const item = {
+                ...record,
+                state: record.state.value === 'enabled' ? 'disabled' : 'enabled',
+              };
+              await service.saveMetadataConfig(opcUaId, deviceId, item);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'disabled' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'enabled'}
+          tooltip={{
+            title:
+              record.state.value === 'disabled'
+                ? intl.formatMessage({
+                    id: 'pages.data.option.remove',
+                    defaultMessage: '删除',
+                  })
+                : '请先禁用该组件,再删除。',
+          }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              const resp: any = await service.removeMetadataConfig(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const pointWs = () => {
+    if (productId && deviceId) {
+      const id = `instance-info-property-${deviceId}-${productId}-opc-point`;
+      const topic = `/dashboard/device/${productId}/properties/realTime`;
+      subscribeTopic!(id, topic, {
+        deviceId: deviceId,
+        properties: data.map((item: any) => item.property),
+        history: 0,
+      })
+        ?.pipe(map((res) => res.patload))
+        .subscribe((payload: any) => {
+          const { value } = payload;
+          console.log(value);
+          propertyValue[value.property] = { ...payload, ...value };
+          setPropertyValue({ ...propertyValue });
+        });
+    }
+  };
+
+  const getBindList = (masterId: any) => {
+    service
+      .bindDevice(
+        encodeQuery({
+          terms: {
+            'id$modbus-master': masterId,
+          },
+        }),
+      )
+      .then((res) => {
+        console.log(res.result);
+        if (res.status === 200) {
+          setDeviceId(res.result[0]?.id);
+          setProductId(res.result[0]?.productId);
+          setParam({
+            terms: [{ column: 'deviceId', value: res.result[0]?.deviceId }],
+          });
+          setBindList(res.result);
+        }
+      });
+  };
+
+  // useEffect(() => {
+  //   pointWs();
+  // }, [deviceId, productId])
+
+  useEffect(() => {
+    pointWs();
+  }, [data]);
+
+  useEffect(() => {
+    const item = new URLSearchParams(location.search);
+    const id = item.get('id');
+    if (id) {
+      setOpcUaId(id);
+      getBindList(id);
+    }
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card className={styles.list}>
+        <PermissionButton
+          onClick={() => {
+            setDeviceVisiable(true);
+          }}
+          isPermission={permission.add}
+          key="add"
+          icon={<PlusOutlined />}
+          type="dashed"
+          style={{ width: '200px', marginLeft: 20, marginBottom: 5 }}
+        >
+          绑定设备
+        </PermissionButton>
+        {bindList.length > 0 ? (
+          <Tabs
+            tabPosition={'left'}
+            defaultActiveKey={deviceId}
+            onChange={(e) => {
+              setDeviceId(e);
+              const items = bindList.find((item: any) => item.id === e);
+              setProductId(items[0]?.productId);
+              setParam({
+                terms: [{ column: 'deviceId', value: e }],
+              });
+            }}
+          >
+            {bindList.map((item: any) => (
+              <Tabs.TabPane
+                key={item.id}
+                tab={
+                  <div className={styles.left}>
+                    <div style={{ width: '100px', textAlign: 'left' }}>{item.name}</div>
+                    <Popconfirm
+                      title="确认解绑该设备嘛?"
+                      onConfirm={() => {
+                        service.unbind([item.id], opcUaId).then((res) => {
+                          if (res.status === 200) {
+                            message.success('解绑成功');
+                            getBindList(opcUaId);
+                          }
+                        });
+                      }}
+                      okText="Yes"
+                      cancelText="No"
+                    >
+                      <DisconnectOutlined className={styles.icon} />
+                    </Popconfirm>
+                  </div>
+                }
+              >
+                <ProTable
+                  actionRef={actionRef}
+                  params={param}
+                  columns={columns}
+                  rowKey="id"
+                  search={false}
+                  headerTitle={
+                    <>
+                      <PermissionButton
+                        onClick={() => {
+                          setPointVisiable(true);
+                          setCurrent({});
+                        }}
+                        isPermission={permission.add}
+                        key="add"
+                        icon={<PlusOutlined />}
+                        type="primary"
+                      >
+                        {intl.formatMessage({
+                          id: 'pages.data.option.add',
+                          defaultMessage: '新增',
+                        })}
+                      </PermissionButton>
+                      <div style={{ marginLeft: 10 }}>
+                        <Input.Search
+                          placeholder="请输入属性ID"
+                          allowClear
+                          onSearch={(value) => {
+                            console.log(value);
+                            if (value) {
+                              setParam({
+                                terms: [
+                                  { column: 'metadataId', value: `%${value}%`, termType: 'like' },
+                                ],
+                              });
+                            } else {
+                              setParam({
+                                terms: [{ column: 'deviceId', value: deviceId }],
+                              });
+                            }
+                          }}
+                        />
+                      </div>
+                    </>
+                  }
+                  request={async (params) => {
+                    const res = await service.queryMetadataConfig(opcUaId, deviceId, {
+                      ...params,
+                      sorts: [{ name: 'createTime', order: 'desc' }],
+                    });
+                    setData(res.result.data);
+                    return {
+                      code: res.message,
+                      result: {
+                        data: res.result.data,
+                        pageIndex: 0,
+                        pageSize: 0,
+                        total: 0,
+                      },
+                      status: res.status,
+                    };
+                  }}
+                />
+              </Tabs.TabPane>
+            ))}
+          </Tabs>
+        ) : (
+          <Empty />
+        )}
+      </Card>
+      {deviceVisiable && (
+        <BindDevice
+          id={opcUaId}
+          close={() => {
+            setDeviceVisiable(false);
+            getBindList(opcUaId);
+          }}
+        />
+      )}
+      {pointVisiable && (
+        <AddPoint
+          deviceId={deviceId}
+          opcUaId={opcUaId}
+          data={current}
+          close={() => {
+            setPointVisiable(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Access;

+ 173 - 0
src/pages/link/Channel/Modbus/Save/index.tsx

@@ -0,0 +1,173 @@
+import { useIntl } from 'umi';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, NumberPicker, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/link/Channel/Modbus';
+import { Modal } from '@/components';
+import { message } from 'antd';
+
+interface Props {
+  data: any;
+  close: () => void;
+  device?: any;
+}
+
+const Save = (props: Props) => {
+  const intl = useIntl();
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+      NumberPicker,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+          },
+          host: {
+            title: 'IP',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入IP',
+            },
+            'x-validator': ['ipv4'],
+            name: 'host',
+            required: true,
+          },
+          port: {
+            title: '端口',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'NumberPicker',
+            'x-component-props': {
+              placeholder: '请输入端口',
+            },
+            default: 502,
+            'x-validator': [
+              {
+                min: 1,
+                max: 65535,
+                message: '请输入1~65535之间的正整数',
+              },
+            ],
+            name: 'host',
+            required: true,
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-component-props': {
+              rows: 5,
+              placeholder: '请输入说明',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                max: 200,
+                message: '最多可输入200个字符',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<any>();
+    if (props.data.id) {
+      service
+        .edit({
+          ...value,
+          id: props.data.id,
+        })
+        .then((res: any) => {
+          if (res.status === 200) {
+            message.success('保存成功');
+            props.close();
+          }
+        });
+    } else {
+      service.save(value).then((res: any) => {
+        if (res.status === 200) {
+          message.success('保存成功');
+          props.close();
+        }
+      });
+    }
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.data.id ? 'edit' : 'add'}`,
+        defaultMessage: '编辑',
+      })}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+      permissionCode={'link/Channel/Modbus'}
+      permission={['add', 'edit']}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 30 - 0
src/pages/link/Channel/Modbus/index.less

@@ -0,0 +1,30 @@
+.topCard {
+  display: flex;
+  align-items: center;
+
+  .img {
+    position: relative;
+    top: 10px;
+    left: 20px;
+  }
+
+  .text {
+    margin-left: 24px;
+
+    .p1 {
+      height: 22px;
+      margin-bottom: 7px;
+      font-weight: 700;
+      font-size: 18px;
+      line-height: 22px;
+    }
+
+    .p2 {
+      height: 20px;
+      color: rgba(0, 0, 0, 0.75);
+      font-weight: 400;
+      font-size: 12px;
+      line-height: 20px;
+    }
+  }
+}

+ 266 - 0
src/pages/link/Channel/Modbus/index.tsx

@@ -0,0 +1,266 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, Card, Col, message, Row } from 'antd';
+import styles from './index.less';
+import { PermissionButton } from '@/components';
+import { history, useIntl } from 'umi';
+import {
+  ControlOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+import Service from './service';
+import Save from './Save';
+import { getMenuPathByCode } from '@/utils/menu';
+
+export const service = new Service('modbus/master');
+
+const Modbus = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const { permission } = PermissionButton.usePermission('link/Channel/Modbus');
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<OpaUa>>({});
+
+  const iconMap = new Map();
+  iconMap.set('1', require('/public/images/channel/1.png'));
+  iconMap.set('2', require('/public/images/channel/2.png'));
+  iconMap.set('3', require('/public/images/channel/3.png'));
+  iconMap.set('4', require('/public/images/channel/4.png'));
+  const background = require('/public/images/channel/background.png');
+
+  const columns: ProColumns<OpaUa>[] = [
+    {
+      title: '通道名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'IP',
+      dataIndex: 'host',
+    },
+    {
+      title: '端口',
+      dataIndex: 'port',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'disabled' ? 'disabled' : 'enabled'
+              }.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              if (record.state.value === 'disabled') {
+                await service.edit({
+                  id: record.id,
+                  state: 'enabled',
+                });
+              } else {
+                await service.edit({
+                  id: record.id,
+                  state: 'disabled',
+                });
+              }
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'disabled' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.view}
+          style={{ padding: 0 }}
+          key="link"
+          type="link"
+          tooltip={{
+            title: '设备接入',
+          }}
+          onClick={() => {
+            history.push(`${getMenuPathByCode('link/Channel/Modbus/Access')}?id=${record.id}`);
+          }}
+        >
+          <ControlOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'enabled'}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              const resp: any = await service.remove(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const topCard = [
+    {
+      numeber: '1',
+      title: 'Modbus通道',
+      text: '配置Modbus通道',
+    },
+    {
+      numeber: '2',
+      title: '设备接入网关',
+      text: '创建Modbus设备接入网关',
+    },
+    {
+      numeber: '3',
+      title: '创建产品',
+      text: '创建产品,并选择接入方式为Modbus',
+    },
+    {
+      numeber: '4',
+      title: '添加设备',
+      text: '添加设备,单独为每一个设备进行数据点绑定',
+    },
+  ];
+  return (
+    <PageContainer>
+      <Card style={{ marginBottom: 10 }}>
+        <Row gutter={[24, 24]}>
+          {topCard.map((item) => (
+            <Col span={6} key={item.numeber}>
+              <Card>
+                <div className={styles.topCard}>
+                  <div
+                    style={{
+                      background: `url(${background}) no-repeat`,
+                      backgroundSize: '100% 100%',
+                      width: '56px',
+                      height: '56px',
+                    }}
+                  >
+                    <img src={iconMap.get(item.numeber)} className={styles.img} />
+                  </div>
+                  <div className={styles.text}>
+                    <p className={styles.p1}>{item.title}</p>
+                    <p className={styles.p2}>{item.text}</p>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+
+      <SearchComponent<any>
+        field={columns}
+        target="opcua"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<OpaUa>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              // setMode('add');
+              setVisible(true);
+              setCurrent({});
+            }}
+            isPermission={permission.add}
+            key="add"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+      />
+      {visible && (
+        <Save
+          data={current}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Modbus;

+ 51 - 0
src/pages/link/Channel/Modbus/service.ts

@@ -0,0 +1,51 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<OpaUa> {
+  edit = (data: any) =>
+    request(`/${SystemConst.API_BASE}/modbus/master`, {
+      method: 'PATCH',
+      data,
+    });
+  bindDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
+  getDevice = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  bind = (params: any, id: string) =>
+    request(`/${SystemConst.API_BASE}/modbus/master/${id}/_bind`, {
+      method: 'POST',
+      data: params,
+    });
+  unbind = (params: any, id: string) =>
+    request(`/${SystemConst.API_BASE}/modbus/master/${id}/_unbind`, {
+      method: 'POST',
+      data: params,
+    });
+  deviceDetail = (id: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${id}/detail`, {
+      method: 'GET',
+    });
+  saveMetadataConfig = (master: string, deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/modbus/master/${master}/${deviceId}/metadata`, {
+      method: 'POST',
+      data,
+    });
+  queryMetadataConfig = (master: string, deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/modbus/master/${master}/${deviceId}/metadata/_query`, {
+      method: 'POST',
+      data,
+    });
+  removeMetadataConfig = (metadataId: string) =>
+    request(`/${SystemConst.API_BASE}/modbus/master/metadata/${metadataId}`, {
+      method: 'DELETE',
+    });
+}
+
+export default Service;

+ 254 - 0
src/pages/link/Channel/Opcua/Access/addPoint/index.tsx

@@ -0,0 +1,254 @@
+import { Col, Form, Input, InputNumber, message, Modal, Radio, Row, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/Channel/Opcua';
+import { DataTypeList } from '@/pages/device/data';
+
+interface Props {
+  data: any;
+  deviceId: string;
+  opcUaId: string;
+  close: Function;
+}
+
+const AddPoint = (props: Props) => {
+  const [form] = Form.useForm();
+  const [enableCalculate, setEnableCalculate] = useState<boolean>();
+  const [property, setProperty] = useState<any>([]);
+
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    const { name } = property?.find((item: any) => item.id === formData.property);
+    if (props.data.id) {
+      if (formData.enableCalculate) {
+        service
+          .editPoint(
+            {
+              ...formData,
+              name: name,
+              deviceId: props.deviceId,
+              opcUaId: props.opcUaId,
+              configuration: {
+                initialValue: formData.initialValue,
+                multiple: formData.multiple,
+              },
+            },
+            props.data.id,
+          )
+          .then((res) => {
+            if (res.status === 200) {
+              message.success('保存成功');
+              props.close();
+            }
+          });
+      } else {
+        service
+          .editPoint(
+            {
+              ...formData,
+              name: name,
+              deviceId: props.deviceId,
+              opcUaId: props.opcUaId,
+            },
+            props.data.id,
+          )
+          .then((res) => {
+            if (res.status === 200) {
+              message.success('保存成功');
+              props.close();
+            }
+          });
+      }
+    } else {
+      if (formData.enableCalculate) {
+        service
+          .addPoint({
+            ...formData,
+            name: name,
+            deviceId: props.deviceId,
+            opcUaId: props.opcUaId,
+            configuration: {
+              initialValue: formData.initialValue,
+              multiple: formData.multiple,
+            },
+          })
+          .then((res) => {
+            if (res.status === 200) {
+              message.success('保存成功');
+              props.close();
+            }
+          });
+      } else {
+        service
+          .addPoint({
+            ...formData,
+            name: name,
+            deviceId: props.deviceId,
+            opcUaId: props.opcUaId,
+          })
+          .then((res) => {
+            if (res.status === 200) {
+              message.success('保存成功');
+              props.close();
+            }
+          });
+      }
+    }
+  };
+
+  useEffect(() => {
+    console.log(props.data);
+    service.deviceDetail(props.deviceId).then((res) => {
+      if (res.result.metadata) {
+        const item = JSON.parse(res.result?.metadata);
+        setProperty(item.properties);
+      }
+    });
+    if (props.data.enableCalculate) {
+      setEnableCalculate(true);
+    }
+  }, []);
+  return (
+    <Modal
+      title={props.data.id ? '编辑' : '新增'}
+      visible
+      width="40vw"
+      destroyOnClose
+      onOk={handleSave}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          ...props.data,
+          initialValue: props.data?.configuration?.initialValue,
+          multiple: props.data?.configuration?.multiple,
+        }}
+      >
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item
+              label="OPC点位ID"
+              required
+              name="opcPointId"
+              rules={[
+                { type: 'string', max: 64 },
+                { required: true, message: '请输入OPC点位ID' },
+              ]}
+            >
+              <Input placeholder="请输入OPC点位ID" />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="属性"
+              name="property"
+              required
+              rules={[{ required: true, message: '属性必选' }]}
+            >
+              <Select placeholder="请选择属性">
+                {property.map((item: any) => (
+                  <Select.Option value={item.id} key={item.id}>
+                    {item.name}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="数据类型"
+              name="dataType"
+              required
+              rules={[{ required: true, message: '数据类型必选' }]}
+            >
+              <Select placeholder="请选择数据类型">
+                {DataTypeList.map((item) => (
+                  <Select.Option value={item.value} key={item.value}>
+                    {item.label}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="数据模式"
+              name="dataMode"
+              required
+              rules={[{ required: true, message: '数据模式必选' }]}
+            >
+              <Select placeholder="请选择数据模式">
+                <Select.Option value="pull" key={'pull'}>
+                  拉取
+                </Select.Option>
+                <Select.Option value="sub" key={'sub'}>
+                  订阅
+                </Select.Option>
+              </Select>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="采样频率"
+              name="interval"
+              required
+              rules={[{ required: true, message: '采样频率必填' }]}
+            >
+              <InputNumber style={{ width: '100%' }} placeholder="请输入采样频率" min={1} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item
+              label="开启计算"
+              name="enableCalculate"
+              required
+              rules={[{ required: true, message: '开启计算必选' }]}
+            >
+              <Radio.Group
+                buttonStyle="solid"
+                onChange={(e) => {
+                  console.log(e.target.value);
+                  setEnableCalculate(e.target.value);
+                }}
+              >
+                <Radio.Button value={true}>是</Radio.Button>
+                <Radio.Button value={false}>否</Radio.Button>
+              </Radio.Group>
+            </Form.Item>
+          </Col>
+        </Row>
+        {enableCalculate && (
+          <Row gutter={[24, 24]}>
+            <Col span={12}>
+              <Form.Item label="初始值" name="initialValue" required>
+                <Input placeholder="请输入初始值" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label="倍数" name="multiple" required>
+                <InputNumber style={{ width: '100%' }} min={1} />
+              </Form.Item>
+            </Col>
+          </Row>
+        )}
+        <Row gutter={[24, 24]}>
+          <Col span={24}>
+            <Form.Item label="说明" name="description">
+              <Input.TextArea maxLength={200} />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+};
+export default AddPoint;

+ 124 - 0
src/pages/link/Channel/Opcua/Access/bindDevice/index.tsx

@@ -0,0 +1,124 @@
+import { Modal } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '@/pages/link/Channel/Opcua';
+import moment from 'moment';
+
+interface Props {
+  id: string;
+  close: () => void;
+}
+
+const BindDevice = (props: Props) => {
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [keys, setKeys] = useState<any>([]);
+  const [bindDevice, setBindDevice] = useState<any>([]);
+
+  const statusMap = new Map();
+  statusMap.set('在线', 'success');
+  statusMap.set('离线', 'error');
+  statusMap.set('未激活', 'processing');
+  statusMap.set('online', 'success');
+  statusMap.set('offline', 'error');
+  statusMap.set('notActive', 'processing');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '设备ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '产品名称',
+      dataIndex: 'productName',
+    },
+    {
+      title: '注册时间',
+      // dataIndex: 'registryTime',
+      render: (_, record) => (
+        <>{record.registryTime ? moment(record.registryTime).format('YYYY-MM-DD HH:mm:ss') : '-'}</>
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => <Badge text={state?.text} status={statusMap.get(state.value)} />,
+    },
+  ];
+
+  const save = () => {
+    const params = bindDevice.map((item: any) => ({
+      opcUaId: props.id,
+      deviceId: item.id,
+      deviceName: item.name,
+      productId: item.productId,
+      productName: item.productName,
+    }));
+    service.bind(params).then((res) => {
+      if (res.status === 200) {
+        message.success('绑定成功');
+        props.close();
+      }
+    });
+  };
+
+  useEffect(() => {
+    console.log(props.id);
+  }, []);
+
+  return (
+    <Modal
+      title={'绑定设备'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={() => {
+        save();
+      }}
+      width={1300}
+      permissionCode={'link/Channel/Opcua'}
+      permission={['add', 'edit', 'view']}
+    >
+      <SearchComponent
+        field={columns}
+        target="bindDevice"
+        defaultParam={[
+          { column: 'productId$dev-protocol', value: 'opc-ua' },
+          { column: 'id$opc-bind$not', value: props.id, type: 'and' },
+        ]}
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        request={async (params) =>
+          service.getDevice({
+            ...params,
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          })
+        }
+        rowSelection={{
+          selectedRowKeys: keys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            setBindDevice(selectedRows);
+            setKeys(selectedRowKeys);
+          },
+        }}
+      />
+    </Modal>
+  );
+};
+export default BindDevice;

+ 24 - 0
src/pages/link/Channel/Opcua/Access/index.less

@@ -0,0 +1,24 @@
+.list {
+  :global {
+    .ant-tabs-tab .ant-tabs-tab-active .ant-tabs-tab-btn {
+      text-shadow: 0 0 0 currentColor;
+    }
+  }
+}
+
+.left {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 200px;
+
+  .icon {
+    display: none;
+  }
+}
+
+.left:hover {
+  .icon {
+    display: block;
+  }
+}

+ 374 - 1
src/pages/link/Channel/Opcua/Access/index.tsx

@@ -1,6 +1,379 @@
+import PermissionButton from '@/components/PermissionButton';
 import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, Card, Empty, Input, message, Popconfirm, Tabs } from 'antd';
+import { useIntl, useLocation } from 'umi';
+import { useEffect, useRef, useState } from 'react';
+import {
+  DeleteOutlined,
+  DisconnectOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import BindDevice from '@/pages/link/Channel/Opcua/Access/bindDevice';
+import { service } from '@/pages/link/Channel/Opcua';
+import encodeQuery from '@/utils/encodeQuery';
+import styles from './index.less';
+import AddPoint from './addPoint';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { map } from 'rxjs/operators';
 
 const Access = () => {
-  return <PageContainer>Access</PageContainer>;
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const location = useLocation<string>();
+  const [param, setParam] = useState({});
+  const [opcUaId, setOpcUaId] = useState<any>('');
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+  const [deviceVisiable, setDeviceVisiable] = useState<boolean>(false);
+  const [pointVisiable, setPointVisiable] = useState<boolean>(false);
+  const [bindList, setBindList] = useState<any>([]);
+  const [deviceId, setDeviceId] = useState<string>('');
+  const [productId, setProductId] = useState<string>('');
+  const [current, setCurrent] = useState<any>({});
+  const [data, setData] = useState<any>([]);
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [propertyValue, setPropertyValue] = useState<any>({});
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '属性ID',
+      dataIndex: 'property',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'OPC点位ID',
+      dataIndex: 'opcPointId',
+    },
+    {
+      title: '数据类型',
+      dataIndex: 'dataType',
+    },
+    {
+      title: '值',
+      // dataIndex: '4',
+      render: (record: any) => <>{propertyValue[record.property]}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disable' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            setPointVisiable(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disable' ? 'disable' : 'good'}.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: async () => {
+              if (record.state.value === 'disable') {
+                await service.enablePoint(record.deviceId, [record.id]);
+              } else {
+                await service.stopPoint(record.deviceId, [record.id]);
+              }
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disable' ? 'disable' : 'good'}`,
+              defaultMessage: record.state.value !== 'disable' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disable' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'good'}
+          tooltip={{
+            title:
+              record.state.value === 'disable'
+                ? intl.formatMessage({
+                    id: 'pages.data.option.remove',
+                    defaultMessage: '删除',
+                  })
+                : '请先禁用该组件,再删除。',
+          }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              console.log(111);
+              const resp: any = await service.deletePoint(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const pointWs = () => {
+    if (productId && deviceId) {
+      const id = `instance-info-property-${deviceId}-${productId}-opc-point`;
+      const topic = `/dashboard/device/${productId}/properties/realTime`;
+      subscribeTopic!(id, topic, {
+        deviceId: deviceId,
+        properties: data.map((item: any) => item.property),
+        history: 0,
+      })
+        ?.pipe(map((res) => res.patload))
+        .subscribe((payload: any) => {
+          const { value } = payload;
+          console.log(value);
+          propertyValue[value.property] = { ...payload, ...value };
+          setPropertyValue({ ...propertyValue });
+        });
+    }
+  };
+
+  const getBindList = (params: any) => {
+    service.getBindList(params).then((res) => {
+      console.log(res.result);
+      if (res.status === 200) {
+        setDeviceId(res.result[0]?.deviceId);
+        setProductId(res.result[0]?.productId);
+        setParam({
+          terms: [{ column: 'deviceId', value: res.result[0]?.deviceId }],
+        });
+        setBindList(res.result);
+      }
+    });
+  };
+
+  // useEffect(() => {
+  //   pointWs();
+  // }, [deviceId, productId])
+
+  useEffect(() => {
+    pointWs();
+  }, [data]);
+
+  useEffect(() => {
+    const item = new URLSearchParams(location.search);
+    const id = item.get('id');
+    if (id) {
+      setOpcUaId(id);
+      getBindList(
+        encodeQuery({
+          terms: {
+            opcUaId: id,
+          },
+        }),
+      );
+    }
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card className={styles.list}>
+        <PermissionButton
+          onClick={() => {
+            setDeviceVisiable(true);
+          }}
+          isPermission={permission.add}
+          key="add"
+          icon={<PlusOutlined />}
+          type="dashed"
+          style={{ width: '200px', marginLeft: 20, marginBottom: 5 }}
+        >
+          绑定设备
+        </PermissionButton>
+        {bindList.length > 0 ? (
+          <Tabs
+            tabPosition={'left'}
+            defaultActiveKey={deviceId}
+            onChange={(e) => {
+              setDeviceId(e);
+              const items = bindList.find((item: any) => item.deviceId === e);
+              setProductId(items[0]?.productId);
+              setParam({
+                terms: [{ column: 'deviceId', value: e }],
+              });
+            }}
+          >
+            {bindList.map((item: any) => (
+              <Tabs.TabPane
+                key={item.deviceId}
+                tab={
+                  <div className={styles.left}>
+                    <div style={{ width: '100px', textAlign: 'left' }}>{item.name}</div>
+                    <Popconfirm
+                      title="确认解绑该设备嘛?"
+                      onConfirm={() => {
+                        service.unbind([item.deviceId], opcUaId).then((res) => {
+                          if (res.status === 200) {
+                            message.success('解绑成功');
+                            getBindList(
+                              encodeQuery({
+                                terms: {
+                                  opcUaId: opcUaId,
+                                },
+                              }),
+                            );
+                          }
+                        });
+                      }}
+                      okText="Yes"
+                      cancelText="No"
+                    >
+                      <DisconnectOutlined className={styles.icon} />
+                    </Popconfirm>
+                  </div>
+                }
+              >
+                <ProTable
+                  actionRef={actionRef}
+                  params={param}
+                  columns={columns}
+                  rowKey="id"
+                  search={false}
+                  headerTitle={
+                    <>
+                      <PermissionButton
+                        onClick={() => {
+                          setPointVisiable(true);
+                          setCurrent({});
+                        }}
+                        isPermission={permission.add}
+                        key="add"
+                        icon={<PlusOutlined />}
+                        type="primary"
+                      >
+                        {intl.formatMessage({
+                          id: 'pages.data.option.add',
+                          defaultMessage: '新增',
+                        })}
+                      </PermissionButton>
+                      <div style={{ marginLeft: 10 }}>
+                        <Input.Search
+                          placeholder="请输入属性"
+                          allowClear
+                          onSearch={(value) => {
+                            console.log(value);
+                            if (value) {
+                              setParam({
+                                terms: [
+                                  { column: 'deviceId', value: deviceId },
+                                  { column: 'property', value: `%${value}%`, termType: 'like' },
+                                ],
+                              });
+                            } else {
+                              setParam({
+                                terms: [{ column: 'deviceId', value: deviceId }],
+                              });
+                            }
+                          }}
+                        />
+                      </div>
+                    </>
+                  }
+                  request={async (params) => {
+                    const res = await service.PointList({
+                      ...params,
+                      sorts: [{ name: 'createTime', order: 'desc' }],
+                    });
+                    setData(res.result.data);
+                    return {
+                      code: res.message,
+                      result: {
+                        data: res.result.data,
+                        pageIndex: 0,
+                        pageSize: 0,
+                        total: 0,
+                      },
+                      status: res.status,
+                    };
+                  }}
+                />
+              </Tabs.TabPane>
+            ))}
+          </Tabs>
+        ) : (
+          <Empty />
+        )}
+      </Card>
+      {deviceVisiable && (
+        <BindDevice
+          id={opcUaId}
+          close={() => {
+            setDeviceVisiable(false);
+            getBindList(
+              encodeQuery({
+                terms: {
+                  opcUaId: opcUaId,
+                },
+              }),
+            );
+          }}
+        />
+      )}
+      {pointVisiable && (
+        <AddPoint
+          deviceId={deviceId}
+          opcUaId={opcUaId}
+          data={current}
+          close={() => {
+            setPointVisiable(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
 };
 export default Access;

+ 28 - 8
src/pages/link/Channel/Opcua/Save/index.tsx

@@ -11,6 +11,7 @@ import { useEffect, useState } from 'react';
 interface Props {
   data: Partial<OpaUa>;
   close: () => void;
+  device?: any;
 }
 
 const Save = (props: Props) => {
@@ -219,19 +220,38 @@ const Save = (props: Props) => {
         }
       });
     } else {
-      service.save(item).then((res: any) => {
-        if (res.status === 200) {
-          message.success('保存成功');
-          props.close();
-        }
-      });
+      if (props.device) {
+        service.save(item).then((res: any) => {
+          if (res.status === 200) {
+            const params = {
+              opcUaId: res.result.id,
+              deviceId: props.device.id,
+              deviceName: props.device.name,
+              productId: props.device.productId,
+              productName: props.device.productName,
+            };
+            service.bind(params).then((resp) => {
+              if (resp.status === 200) {
+                message.success('保存成功');
+                props.close();
+              }
+            });
+          }
+        });
+      } else {
+        service.save(item).then((res: any) => {
+          if (res.status === 200) {
+            message.success('保存成功');
+            props.close();
+          }
+        });
+      }
     }
   };
 
   useEffect(() => {
     service.policies().then((res) => setPolicies(res.result));
     service.modes().then((res) => setModes(res.result));
-    console.log(props.data.clientConfigs?.[0]);
   }, []);
   return (
     <Modal
@@ -244,7 +264,7 @@ const Save = (props: Props) => {
       onCancel={props.close}
       onOk={save}
       width="35vw"
-      permissionCode={'system/Relationship'}
+      permissionCode={'link/Channel/Opcua'}
       permission={['add', 'edit']}
     >
       <Form form={form} layout="vertical">

+ 1 - 0
src/pages/link/Channel/Opcua/index.tsx

@@ -133,6 +133,7 @@ const Opcua = () => {
         <PermissionButton
           isPermission={permission.delete}
           style={{ padding: 0 }}
+          disabled={record.state.value === 'enabled'}
           popConfirm={{
             title: '确认删除',
             onConfirm: async () => {

+ 59 - 0
src/pages/link/Channel/Opcua/service.ts

@@ -21,6 +21,65 @@ class Service extends BaseService<OpaUa> {
       method: 'GET',
       params,
     });
+  getDevice = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  //绑定设备
+  bind = (params: any) =>
+    request(`/${SystemConst.API_BASE}/opc/device-bind/batch/_create`, {
+      method: 'POST',
+      data: params,
+    });
+  getBindList = (params: any) =>
+    request(`/${SystemConst.API_BASE}/opc/device-bind/device-details/_query/no-paging`, {
+      method: 'GET',
+      params,
+    });
+  unbind = (params: any, opcUaId: string) =>
+    request(`${SystemConst.API_BASE}/opc/device-bind/batch/${opcUaId}/_delete`, {
+      method: 'POST',
+      data: params,
+    });
+  deviceDetail = (id: any) =>
+    request(`${SystemConst.API_BASE}/device-instance/${id}/detail`, {
+      method: 'GET',
+    });
+  addPoint = (params: any) =>
+    request(`${SystemConst.API_BASE}/opc/point`, {
+      method: 'POST',
+      data: params,
+    });
+  PointList = (params: any) =>
+    request(`${SystemConst.API_BASE}/opc/point/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  editPoint = (params: any, id: string) =>
+    request(`/${SystemConst.API_BASE}/opc/point/${id}`, {
+      method: 'PUT',
+      data: params,
+    });
+  deletePoint = (id: string) =>
+    request(`/${SystemConst.API_BASE}/opc/point/${id}`, {
+      method: 'DELETE',
+    });
+  enablePoint = (bindDeviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/opc/device-bind/points/${bindDeviceId}/_start`, {
+      method: 'POST',
+      data,
+    });
+  stopPoint = (bindDeviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/opc/device-bind/points/${bindDeviceId}/_stop`, {
+      method: 'POST',
+      data,
+    });
+  noPagingOpcua = (data: any) =>
+    request(`/${SystemConst.API_BASE}/opc/client/_query/no-paging`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

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

@@ -91,7 +91,7 @@ const TabComponent = observer((props: Props) => {
             type: 'and',
           },
         ],
-        sorts: [{ name: 'alarmDate', order: 'desc' }],
+        sorts: [{ name: 'alarmTime', order: 'desc' }],
       })
       .then((resp) => {
         if (resp.status === 200) {

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

@@ -193,8 +193,8 @@ export default (props: TimingTrigger) => {
                 <TimePicker.RangePicker
                   format={'HH:mm:ss'}
                   value={[
-                    moment(data.period?.from, 'HH:mm:ss'),
-                    moment(data.period?.to, 'hh:mm:ss'),
+                    moment(data.period?.from || new Date(), 'HH:mm:ss'),
+                    moment(data.period?.to || new Date(), 'hh:mm:ss'),
                   ]}
                   onChange={(_, dateString) => {
                     onChange({
@@ -210,7 +210,7 @@ export default (props: TimingTrigger) => {
               ) : (
                 <TimePicker
                   format={'HH:mm:ss'}
-                  value={moment(data.once?.time, 'HH:mm:ss')}
+                  value={moment(data.once?.time || new Date(), 'HH:mm:ss')}
                   onChange={(_, dateString) => {
                     onChange({
                       ...data,

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

@@ -60,6 +60,7 @@ export default () => {
 
   const [requestParams, setRequestParams] = useState<any>(undefined);
   const [triggerValue, setTriggerValue] = useState<any>([]);
+  const [triggerDatas, setTriggerDatas] = useState<any>({});
   const [actionParams, setActionParams] = useState<any>(undefined);
 
   const [actionsData, setActionsData] = useState<any[]>([]);
@@ -76,6 +77,7 @@ export default () => {
         setParallel(_data.parallel);
 
         setTriggerValue({ trigger: _data.terms || [] });
+        setTriggerDatas(_data.terms);
         if (_data.trigger?.shakeLimit) {
           setShakeLimit(_data.trigger?.shakeLimit || DefaultShakeLimit);
         }
@@ -214,6 +216,12 @@ export default () => {
                 if (changeValue.trigger.device.productId) {
                   setTriggerValue([]);
                   setRequestParams({ trigger: allValues.trigger });
+                } else if (
+                  changeValue.trigger.device.selectorValues ||
+                  (changeValue.trigger.device.operation &&
+                    changeValue.trigger.device.operation.operator)
+                ) {
+                  setTriggerDatas(allValues.trigger);
                 }
               } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
                 setActionParams({ trigger: allValues.trigger });
@@ -317,11 +325,7 @@ export default () => {
               // >
               //   <TriggerDevice className={'trigger-type-content'} />
               // </Form.Item>
-              <TriggerDevice
-                value={requestParams && requestParams.trigger}
-                className={'trigger-type-content'}
-                form={form}
-              />
+              <TriggerDevice value={triggerDatas} className={'trigger-type-content'} form={form} />
             )}
           </Form.Item>
           {triggerType === TriggerWayType.device &&

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

@@ -142,6 +142,8 @@ export default observer((props: TriggerProps) => {
     getProducts();
   }, []);
 
+  console.log('triggerModel', FormModel);
+
   useEffect(() => {
     const triggerData = props.value;
     console.log('triggerData', triggerData);
@@ -174,7 +176,7 @@ export default observer((props: TriggerProps) => {
               onChange={(key: any, node: any) => {
                 props.form?.resetFields([['trigger', 'device', 'selector']]);
                 props.form?.resetFields([['trigger', 'device', 'selectorValues']]);
-                props.form?.resetFields([['trigger', 'device', 'operation', 'operator']]);
+                props.form?.resetFields([['trigger', 'device', 'operation']]);
                 productIdChange(key, node.metadata);
                 props.form?.setFieldsValue({
                   trigger: {
@@ -207,6 +209,7 @@ export default observer((props: TriggerProps) => {
                     ]}
                     // fieldNames={{ label: 'name', value: 'id' }}
                     onSelect={(key: string) => {
+                      props.form?.resetFields([['trigger', 'device', 'selectorValues']]);
                       setSelector(key);
                     }}
                     style={{ width: 120 }}

+ 15 - 5
src/pages/rule-engine/Scene/Save/trigger/operation.tsx

@@ -25,12 +25,22 @@ export default (props: OperatorProps) => {
       if (props.propertiesList) {
         return _key.map((item) => {
           const proItem = props.propertiesList!.find((a: any) => a.id === item);
+          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[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,
+            id: item,
+            name: item,
+            type: '',
+            format: undefined,
+            options: undefined,
             value: value[item],
           };
         });

+ 35 - 9
src/pages/system/Platforms/Api/basePage.tsx

@@ -1,5 +1,5 @@
 import { Button, message, Table } from 'antd';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import { useLocation } from 'umi';
 import { service } from '../index';
 import { ApiModel } from '@/pages/system/Platforms/Api/base';
@@ -15,6 +15,9 @@ export default (props: TableProps) => {
   const [selectKeys, setSelectKeys] = useState<string[]>([]);
   const [dataSource, setDataSource] = useState<any[]>([]);
   const [loading, setLoading] = useState(false);
+
+  const grantCache = useRef<string[]>([]);
+
   const location = useLocation();
 
   const getApiGrant = useCallback(() => {
@@ -23,6 +26,7 @@ export default (props: TableProps) => {
 
     service.getApiGranted(code!).then((resp: any) => {
       if (resp.status === 200) {
+        grantCache.current = resp.result;
         setSelectKeys(resp.result);
       }
     });
@@ -30,9 +34,6 @@ export default (props: TableProps) => {
 
   const getOperations = async (apiData: any[], operations: string[]) => {
     // 过滤只能授权的接口,当isShowGranted为true时,过滤为已赋权的接口
-    console.log(
-      apiData.filter((item) => item && item.operationId && operations.includes(item.operationId)),
-    );
     setDataSource(
       apiData.filter((item) => item && item.operationId && operations.includes(item.operationId)),
     );
@@ -65,7 +66,23 @@ export default (props: TableProps) => {
   const save = useCallback(async () => {
     const param = new URLSearchParams(location.search);
     const code = param.get('code');
-    const operations = selectKeys.map((a: string) => {
+    // 和原有已授权数据进行对比
+    const addGrant = selectKeys.filter((key) => {
+      if (grantCache.current.includes(key)) {
+        return false;
+      }
+      return true;
+    });
+
+    // 获取删除的数据
+    const removeGrant = grantCache.current.filter((key) => {
+      if (selectKeys.includes(key)) {
+        return false;
+      }
+      return true;
+    });
+
+    const addOperations = addGrant.map((a: string) => {
       const item = dataSource.find((b) => b.operationId === a);
       return {
         id: a,
@@ -73,16 +90,25 @@ export default (props: TableProps) => {
       };
     });
 
+    const removeOperations = removeGrant.map((a: string) => {
+      const item = dataSource.find((b) => b.operationId === a);
+      return {
+        id: a,
+        permissions: item.security,
+      };
+    });
+
+    grantCache.current = addGrant;
+
     setLoading(true);
-    const resp = await service.saveApiGrant(code!, { operations });
+    const resp = await service.addApiGrant(code!, { operations: addOperations });
+    const resp2 = await service.removeApiGrant(code!, { operations: removeOperations });
     setLoading(false);
-    if (resp.status === 200) {
+    if (resp.status === 200 || resp2.status === 200) {
       message.success('操作成功');
     }
   }, [selectKeys, location, dataSource]);
 
-  console.log(dataSource);
-
   return (
     <div className={'platforms-api-table'}>
       <Table<any>

+ 7 - 1
src/pages/system/Platforms/service.ts

@@ -48,6 +48,12 @@ class Service extends BaseService<platformsType> {
   saveApiGrant = (id: string, data: any) =>
     request(`/${SystemConst.API_BASE}/api-client/${id}/grant`, { method: 'POST', data });
 
+  addApiGrant = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/grant/_add`, { method: 'POST', data });
+
+  removeApiGrant = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/grant/_delete`, { method: 'POST', data });
+
   /**
    * 获取已授权的接口ID
    * @param id 第三方平台的ID
@@ -59,7 +65,7 @@ class Service extends BaseService<platformsType> {
    * 获取可授权的接口ID
    */
   apiOperations = () =>
-    request(`/${SystemConst.API_BASE}//api-client/operations`, { method: 'GET' });
+    request(`/${SystemConst.API_BASE}/api-client/operations`, { method: 'GET' });
 }
 
 export default Service;

+ 1 - 1
src/pages/user/Login/index.tsx

@@ -127,7 +127,7 @@ const Login: React.FC = () => {
     setLoading(true);
     Service.login({ expires: loginRef.current.expires, verifyKey: captcha.key, ...data }).subscribe(
       {
-        next: async (userInfo: UserInfo) => {
+        next: async (userInfo) => {
           Token.set(userInfo.token);
           await fetchUserInfo();
           goto();

+ 6 - 6
src/pages/user/Login/user.d.ts

@@ -33,11 +33,11 @@ type Permission = {
 };
 
 type UserInfo = {
-  userId: string;
+  userId?: string;
   user: Partial<UserBase>;
-  token: string;
-  roles: Role[];
-  permissions: Partial<Permission>[];
-  expires: number;
-  currentAuthority: string[];
+  token?: string;
+  roles?: Role[];
+  permissions?: Partial<Permission>[];
+  expires?: number;
+  currentAuthority?: string[];
 };

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

@@ -48,6 +48,16 @@ const extraRouteObj = {
           },
         ],
       },
+      {
+        code: 'Modbus',
+        name: 'OPC UA',
+        children: [
+          {
+            code: 'Access',
+            name: '数据点绑定',
+          },
+        ],
+      },
     ],
   },
   demo: {

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

@@ -37,7 +37,9 @@ export enum MENUS_CODE {
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
   'link/Channel/Opcua' = 'link/Channel/Opcua',
+  'link/Channel/Modbus' = 'link/Channel/Modbus',
   'link/Channel/Opcua/Access' = 'link/Channel/Opcua/Access',
+  'link/Channel/Modbus/Access' = 'link/Channel/Modbus/Access',
   'link/Protocol/Debug' = 'link/Protocol/Debug',
   'link/Protocol' = 'link/Protocol',
   'link/Type' = 'link/Type',

+ 109 - 0
yarn.lock

@@ -121,6 +121,18 @@
     classnames "^2.2.6"
     omit.js "^2.0.2"
 
+"@ant-design/pro-card@1.20.4":
+  version "1.20.4"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-card/-/pro-card-1.20.4.tgz#b370bd3b0240628b1deb4a02319829861066e914"
+  integrity sha512-XWD5bInxS+G5Bnvg9L7uSDcIyEgKpJiI9CFoZgomSbYU+IctK+2PEYGEgFqS0OuXM21KPNglTJ3L/SSm57KdcQ==
+  dependencies:
+    "@ant-design/icons" "^4.2.1"
+    "@ant-design/pro-utils" "1.41.2"
+    "@babel/runtime" "^7.16.3"
+    classnames "^2.2.6"
+    omit.js "^2.0.2"
+    rc-util "^5.4.0"
+
 "@ant-design/pro-card@^1.16.2":
   version "1.19.2"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-card/-/pro-card-1.19.2.tgz#439d880bfa132d3974df89b976ac55a974938b08"
@@ -213,6 +225,23 @@
     react-color "2.19.3"
     swr "^1.2.0"
 
+"@ant-design/pro-field@1.34.10":
+  version "1.34.10"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-field/-/pro-field-1.34.10.tgz#9fe8f051808b8ca3ac78362a40fc9e4e7c06cd70"
+  integrity sha512-lKVPAH/lplRvXL8D9w6jF+31akfbKklXWi9NU4/4pt1VmooAFp1NAyhokjgDMk7Cr8UA1r3P4DZvv21Ujg1DJg==
+  dependencies:
+    "@ant-design/icons" "^4.2.1"
+    "@ant-design/pro-provider" "1.6.5"
+    "@ant-design/pro-utils" "1.41.2"
+    "@babel/runtime" "^7.16.3"
+    "@chenshuai2144/sketch-color" "^1.0.8"
+    classnames "^2.2.6"
+    lodash.tonumber "^4.0.3"
+    moment "^2.27.0"
+    omit.js "^2.0.2"
+    rc-util "^5.4.0"
+    swr "^1.2.0"
+
 "@ant-design/pro-field@1.34.2":
   version "1.34.2"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-field/-/pro-field-1.34.2.tgz#b0a0eb1fbeac1c18cbc2d6738e5f7c526324b5b3"
@@ -284,6 +313,25 @@
     use-json-comparison "^1.0.5"
     use-media-antd-query "^1.0.6"
 
+"@ant-design/pro-form@1.67.3":
+  version "1.67.3"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-form/-/pro-form-1.67.3.tgz#62b96cdabe9ec2f3140f0246c30e4dcb82b06d48"
+  integrity sha512-axMVFBLUeSDvPIu0gJX96flKO8KuIqRMWEuX6o0jWqAiK36F8blKj6i9UZofxhn9nSnqwQsNsWSOuTbHyOB/VA==
+  dependencies:
+    "@ant-design/icons" "^4.2.1"
+    "@ant-design/pro-field" "1.34.10"
+    "@ant-design/pro-provider" "1.6.5"
+    "@ant-design/pro-utils" "1.41.2"
+    "@babel/runtime" "^7.16.3"
+    "@umijs/use-params" "^1.0.9"
+    classnames "^2.2.6"
+    lodash "^4.17.21"
+    omit.js "^2.0.2"
+    rc-resize-observer "^1.1.0"
+    rc-util "^5.0.6"
+    use-json-comparison "^1.0.5"
+    use-media-antd-query "^1.0.6"
+
 "@ant-design/pro-layout@^6.27.2":
   version "6.34.9"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-6.34.9.tgz#d84178595eaf1fd03c707d1e3b6779bb6266305c"
@@ -308,6 +356,23 @@
     use-media-antd-query "^1.0.6"
     warning "^4.0.3"
 
+"@ant-design/pro-list@^1.21.61":
+  version "1.21.61"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-list/-/pro-list-1.21.61.tgz#cfc22c3c8d5819c7665e755d964d49e1b7fb6811"
+  integrity sha512-fOI6jKmxFDqymsl877yPACAdwYvSLepRAjGE9netuSgjP6etsTjRg3grL8C/tsh3SGQ4yJoGAOEsPFRDBaTjsQ==
+  dependencies:
+    "@ant-design/icons" "^4.0.0"
+    "@ant-design/pro-card" "1.20.4"
+    "@ant-design/pro-field" "1.34.10"
+    "@ant-design/pro-table" "2.74.3"
+    "@babel/runtime" "^7.16.3"
+    classnames "^2.2.6"
+    moment "^2.24.0"
+    rc-resize-observer "^1.0.0"
+    rc-util "^4.19.0"
+    unstated-next "^1.1.0"
+    use-media-antd-query "1.0.6"
+
 "@ant-design/pro-provider@1.4.15":
   version "1.4.15"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.4.15.tgz#2786c65d968bd0ed3cccebe46d326e9e6aaa3cb5"
@@ -333,6 +398,15 @@
     rc-util "^5.0.1"
     swr "^1.2.0"
 
+"@ant-design/pro-provider@1.6.5":
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.6.5.tgz#e30af6eec4602bd33b3cec894e918ea347f266ee"
+  integrity sha512-83hy+q5vCQLRT7QY/3Wo4YL6eAWWi+Svqc7B7Mxw73WT7yPqzhOaRqLyyytPtQrSCT5ldJsqtfWgJBtWIkoNYw==
+  dependencies:
+    "@babel/runtime" "^7.16.3"
+    rc-util "^5.0.1"
+    swr "^1.2.0"
+
 "@ant-design/pro-skeleton@1.0.5":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-skeleton/-/pro-skeleton-1.0.5.tgz#d66234571cce7c4b195d0cfe6b951391ef42dcc7"
@@ -341,6 +415,27 @@
     "@babel/runtime" "^7.16.3"
     use-media-antd-query "^1.0.6"
 
+"@ant-design/pro-table@2.74.3":
+  version "2.74.3"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-table/-/pro-table-2.74.3.tgz#4d322ca73f2fc7abfdc51b4a2afe91f6a97b1b1c"
+  integrity sha512-XZOzJmjaeJ/iF2Z0eyhAiGeVF87aWAzI2uD3dTU8wAvfCb3Q2ANfMnj8ZeYBmtAGbCinVSm4b4yYxEXORc/43w==
+  dependencies:
+    "@ant-design/icons" "^4.1.0"
+    "@ant-design/pro-card" "1.20.4"
+    "@ant-design/pro-field" "1.34.10"
+    "@ant-design/pro-form" "1.67.3"
+    "@ant-design/pro-provider" "1.6.5"
+    "@ant-design/pro-utils" "1.41.2"
+    "@babel/runtime" "^7.16.3"
+    classnames "^2.2.6"
+    moment "^2.24.0"
+    omit.js "^2.0.2"
+    rc-util "^5.0.1"
+    react-sortable-hoc "^2.0.0"
+    unstated-next "^1.1.0"
+    use-json-comparison "^1.0.5"
+    use-media-antd-query "^1.1.0"
+
 "@ant-design/pro-utils@1.19.5":
   version "1.19.5"
   resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.19.5.tgz#df49b3f0e99fe4adcc896ea37e28abbff31838bc"
@@ -383,6 +478,20 @@
     react-sortable-hoc "^2.0.0"
     swr "^1.2.0"
 
+"@ant-design/pro-utils@1.41.2":
+  version "1.41.2"
+  resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.41.2.tgz#a0c76c7a1201d26e385e7b6b56fbf708306bcd4a"
+  integrity sha512-wVDMwFrLVP0Ng7yDUGrZqot0zCdN1wtpVtGdF6KTZzjMwZ5glWBQWWZ2oFNFqhgRZ9QlojkNWISQl4NnDS/4LA==
+  dependencies:
+    "@ant-design/icons" "^4.3.0"
+    "@ant-design/pro-provider" "1.6.5"
+    "@babel/runtime" "^7.16.3"
+    classnames "^2.2.6"
+    moment "^2.27.0"
+    rc-util "^5.0.6"
+    react-sortable-hoc "^2.0.0"
+    swr "^1.2.0"
+
 "@ant-design/react-slick@~0.28.1":
   version "0.28.4"
   resolved "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-0.28.4.tgz#8b296b87ad7c7ae877f2a527b81b7eebd9dd29a9"