Преглед изворни кода

feat(merge): merge wzy

Next wzy
Lind пре 3 година
родитељ
комит
501d6ba2d8
30 измењених фајлова са 3000 додато и 26 уклоњено
  1. 2 2
      config/proxy.ts
  2. 1 0
      package.json
  3. 1 1
      src/app.tsx
  4. 3 3
      src/components/RightContent/AvatarDropdown.tsx
  5. 2 2
      src/pages/account/Center/edit/infoEdit.tsx
  6. 8 0
      src/pages/account/Center/index.less
  7. 26 2
      src/pages/account/Center/index.tsx
  8. 339 0
      src/pages/device/Instance/Detail/Modbus/index.tsx
  9. 332 0
      src/pages/device/Instance/Detail/Opcua/index.tsx
  10. 12 0
      src/pages/device/Instance/Detail/index.tsx
  11. 262 0
      src/pages/link/Channel/Modbus/Access/addPoint/index.tsx
  12. 108 0
      src/pages/link/Channel/Modbus/Access/bindDevice/index.tsx
  13. 24 0
      src/pages/link/Channel/Modbus/Access/index.less
  14. 369 0
      src/pages/link/Channel/Modbus/Access/index.tsx
  15. 173 0
      src/pages/link/Channel/Modbus/Save/index.tsx
  16. 29 0
      src/pages/link/Channel/Modbus/index.less
  17. 266 0
      src/pages/link/Channel/Modbus/index.tsx
  18. 51 0
      src/pages/link/Channel/Modbus/service.ts
  19. 254 0
      src/pages/link/Channel/Opcua/Access/addPoint/index.tsx
  20. 124 0
      src/pages/link/Channel/Opcua/Access/bindDevice/index.tsx
  21. 24 0
      src/pages/link/Channel/Opcua/Access/index.less
  22. 374 1
      src/pages/link/Channel/Opcua/Access/index.tsx
  23. 28 8
      src/pages/link/Channel/Opcua/Save/index.tsx
  24. 1 0
      src/pages/link/Channel/Opcua/index.tsx
  25. 59 0
      src/pages/link/Channel/Opcua/service.ts
  26. 1 1
      src/pages/user/Login/index.tsx
  27. 6 6
      src/pages/user/Login/user.d.ts
  28. 10 0
      src/utils/menu/index.ts
  29. 2 0
      src/utils/menu/router.ts
  30. 109 0
      yarn.lock

+ 2 - 2
config/proxy.ts

@@ -9,8 +9,8 @@
 export default {
   dev: {
     '/jetlinks': {
-      // target: 'http://192.168.32.44:8844/',
-      // ws: 'ws://192.168.32.44:8844/',
+      // target: 'http://192.168.32.8:8844/',
+      // ws: 'ws://192.168.32.8:8844/',
       target: 'http://120.79.18.123:8844/',
       ws: 'ws://120.79.18.123:8844/',
       // target: 'http://192.168.66.5:8844/',

+ 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();

+ 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">

+ 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>

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

@@ -18,6 +18,14 @@
   .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="注册时间">

+ 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;

+ 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);

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

@@ -0,0 +1,262 @@
+import { Col, Form, Input, Modal, Row, Select, InputNumber, message } 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 { useState, useRef } 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, Popconfirm, message, Tabs, Empty, Input } 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, Select, NumberPicker } 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;

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

@@ -0,0 +1,29 @@
+.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, Modal, Row, Select, InputNumber, Radio, message } 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, useState, useRef } 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, Popconfirm, message, Tabs, Empty, Input } 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/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"