Explorar o código

feat: 数据采集

100011797 %!s(int64=3) %!d(string=hai) anos
pai
achega
7d998b58eb
Modificáronse 30 ficheiros con 2741 adicións e 36 borrados
  1. BIN=BIN
      public/images/DataCollect/channel-modbus.png
  2. BIN=BIN
      public/images/DataCollect/channel-opcua.png
  3. BIN=BIN
      public/images/DataCollect/device-modbus.png
  4. BIN=BIN
      public/images/DataCollect/device-opcua.png
  5. BIN=BIN
      public/images/DataCollect/tree-channel.png
  6. BIN=BIN
      public/images/DataCollect/tree-device.png
  7. 59 0
      src/components/ProTableCard/CardItems/DataCollect/Point.tsx
  8. 69 0
      src/components/ProTableCard/CardItems/DataCollect/channel.tsx
  9. 63 0
      src/components/ProTableCard/CardItems/DataCollect/device.tsx
  10. 86 0
      src/components/ProTableCard/CardItems/DataCollect/index.less
  11. 3 3
      src/pages/device/Category/index.tsx
  12. 5 0
      src/pages/link/DataCollect/Dashboard/index.tsx
  13. 13 0
      src/pages/link/DataCollect/DataGathering/index.less
  14. 43 0
      src/pages/link/DataCollect/DataGathering/index.tsx
  15. 43 0
      src/pages/link/DataCollect/IntegratedQuery/index.tsx
  16. 347 0
      src/pages/link/DataCollect/components/Channel/Save/index.tsx
  17. 220 0
      src/pages/link/DataCollect/components/Channel/index.tsx
  18. 201 0
      src/pages/link/DataCollect/components/Device/Save/index.tsx
  19. 306 0
      src/pages/link/DataCollect/components/Device/index.tsx
  20. 291 0
      src/pages/link/DataCollect/components/Point/Save/modbus.tsx
  21. 191 0
      src/pages/link/DataCollect/components/Point/Save/opc-ua.tsx
  22. 144 0
      src/pages/link/DataCollect/components/Point/Save/scan.tsx
  23. 211 0
      src/pages/link/DataCollect/components/Point/index.tsx
  24. 16 0
      src/pages/link/DataCollect/components/Tree/index.less
  25. 168 0
      src/pages/link/DataCollect/components/Tree/index.tsx
  26. 5 0
      src/pages/link/DataCollect/components/index.less
  27. 101 0
      src/pages/link/DataCollect/service.ts
  28. 105 0
      src/pages/link/DataCollect/typings.d.ts
  29. 24 32
      src/pages/system/Department/Assets/index.tsx
  30. 27 1
      src/pages/system/Department/Tree/tree.tsx

BIN=BIN
public/images/DataCollect/channel-modbus.png


BIN=BIN
public/images/DataCollect/channel-opcua.png


BIN=BIN
public/images/DataCollect/device-modbus.png


BIN=BIN
public/images/DataCollect/device-opcua.png


BIN=BIN
public/images/DataCollect/tree-channel.png


BIN=BIN
public/images/DataCollect/tree-device.png


+ 59 - 0
src/components/ProTableCard/CardItems/DataCollect/Point.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard, Ellipsis } from '@/components';
+import '@/style/common.less';
+import './index.less';
+
+export interface PointCardProps extends Partial<PointItem> {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  grantedPermissions?: string[];
+  onUnBind?: (e: any) => void;
+  showBindBtn?: boolean;
+  cardType?: 'bind' | 'unbind';
+  showTool?: boolean;
+}
+
+const defaultImage = require('/public/images/device-type-3-big.png');
+
+export default (props: PointCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.actions}
+      status={props.state?.value}
+      statusText={props.state?.text}
+      statusNames={{
+        enabled: StatusColorEnum.success,
+        disabled: StatusColorEnum.error,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            {/*<div>*/}
+            {/*  <label>设备类型</label>*/}
+            {/*  <Ellipsis title={props.deviceType ? props.deviceType.text : ''} />*/}
+            {/*  /!*<div className={'ellipsis'}>{props.deviceType ? props.deviceType.text : ''}</div>*!/*/}
+            {/*</div>*/}
+            {/*<div>*/}
+            {/*  <label>产品名称</label>*/}
+            {/*  <Ellipsis title={props.productName} />*/}
+            {/*  /!*<div className={'ellipsis'}>{props.productName || ''}</div>*!/*/}
+            {/*</div>*/}
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 69 - 0
src/components/ProTableCard/CardItems/DataCollect/channel.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard, Ellipsis } from '@/components';
+import '@/style/common.less';
+import './index.less';
+
+export interface ChannelCardProps extends Partial<ChannelItem> {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  grantedPermissions?: string[];
+  onUnBind?: (e: any) => void;
+  showBindBtn?: boolean;
+  cardType?: 'bind' | 'unbind';
+  showTool?: boolean;
+}
+
+const modbusImage = require('/public/images/DataCollect/channel-modbus.png');
+const opcuaImage = require('/public/images/DataCollect/channel-opcua.png');
+
+export default (props: ChannelCardProps) => {
+  return (
+    <TableCard
+      actions={props.actions}
+      status={props.state?.value}
+      statusText={props.state?.text}
+      showMask={false}
+      statusNames={{
+        enabled: StatusColorEnum.success,
+        disabled: StatusColorEnum.error,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={props.provider === 'MODBUS_TCP' ? modbusImage : opcuaImage}
+            alt={''}
+          />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>协议</label>
+              <Ellipsis title={props.provider} />
+            </div>
+            <div>
+              <label>地址</label>
+              <Ellipsis
+                title={
+                  props.provider === 'MODBUS_TCP'
+                    ? props?.configuration?.host
+                    : props?.configuration?.endpoint
+                }
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 63 - 0
src/components/ProTableCard/CardItems/DataCollect/device.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard, Ellipsis } from '@/components';
+import '@/style/common.less';
+import './index.less';
+
+export interface CollectorCardProps extends Partial<CollectorItem> {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  grantedPermissions?: string[];
+  onUnBind?: (e: any) => void;
+  showBindBtn?: boolean;
+  cardType?: 'bind' | 'unbind';
+  showTool?: boolean;
+}
+
+const modbusImage = require('/public/images/DataCollect/device-modbus.png');
+const opcuaImage = require('/public/images/DataCollect/device-opcua.png');
+
+export default (props: CollectorCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.actions}
+      status={props.state?.value}
+      statusText={props.state?.text}
+      statusNames={{
+        enabled: StatusColorEnum.success,
+        disabled: StatusColorEnum.error,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img
+            width={88}
+            height={88}
+            src={props.provider === 'MODBUS_TCP' ? modbusImage : opcuaImage}
+            alt={''}
+          />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>协议</label>
+              <Ellipsis title={props.provider} />
+            </div>
+            <div>
+              <label>所属通道</label>
+              <Ellipsis title={props?.channelName || ''} />
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 86 - 0
src/components/ProTableCard/CardItems/DataCollect/index.less

@@ -0,0 +1,86 @@
+@import '~antd/es/style/themes/default.less';
+
+.pro-table-card-item {
+  display: flex;
+
+  .card-item-avatar {
+    margin-right: 16px;
+  }
+
+  .card-item-body {
+    display: flex;
+    flex-direction: column;
+    flex-grow: 1;
+    width: 0;
+
+    .card-item-header {
+      display: flex;
+      width: calc(100% - 86px);
+      margin-bottom: 12px;
+
+      .card-item-header-name {
+        font-weight: bold;
+        font-size: 16px;
+      }
+    }
+
+    .card-item-content-flex {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+
+      .flex-auto {
+        display: flex;
+        flex-direction: column;
+        flex-grow: 1;
+        width: 0;
+      }
+
+      .flex-button {
+        display: flex;
+        flex: 0 0 50px;
+        align-items: center;
+        justify-content: center;
+        height: 50px;
+        color: @primary-color;
+        border: 1px solid @primary-color-active;
+        cursor: pointer;
+      }
+
+      label {
+        color: rgba(#000, 0.75);
+        font-size: 12px;
+      }
+
+      .ellipsis {
+        font-weight: bold;
+        font-size: 14px;
+      }
+    }
+
+    .card-item-content {
+      display: flex;
+      flex-wrap: wrap;
+
+      > div {
+        width: 50%;
+
+        &:nth-child(even) {
+          width: calc(50% - 12px);
+          margin-left: 12px;
+        }
+      }
+
+      label {
+        color: rgba(#000, 0.75);
+        font-size: 12px;
+      }
+
+      .ellipsis {
+        max-width: 70%;
+        font-weight: bold;
+        font-size: 14px;
+      }
+    }
+  }
+}

+ 3 - 3
src/pages/device/Category/index.tsx

@@ -120,15 +120,15 @@ const Category = observer(() => {
           type="link"
           isPermission={permission.add}
           onClick={() => {
-            if (record.level <= 5) {
+            if (record.level >= 5) {
+              onlyMessage('最多可添加5层', 'error');
+            } else {
               state.visible = true;
               const sortIndex = getSortIndex(treeData, record.id);
               state.parentId = record.id;
               state.current = {
                 sortIndex,
               };
-            } else {
-              onlyMessage('最多可添加5层', 'error');
             }
           }}
         >

+ 5 - 0
src/pages/link/DataCollect/Dashboard/index.tsx

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

+ 13 - 0
src/pages/link/DataCollect/DataGathering/index.less

@@ -0,0 +1,13 @@
+.container {
+  display: flex;
+  min-height: calc(100vh - 180px);
+  .left {
+    width: 300px;
+    margin-right: 20px;
+    padding: 10px;
+    border-right: 1px solid #eee;
+  }
+  .right {
+    width: calc(100% - 300px);
+  }
+}

+ 43 - 0
src/pages/link/DataCollect/DataGathering/index.tsx

@@ -0,0 +1,43 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card } from 'antd';
+import styles from './index.less';
+import ChannelTree from '../components/Tree';
+import { observer } from '@formily/reactive-react';
+import { model } from '@formily/reactive';
+import Device from '../components/Device';
+import Point from '../components/Point';
+
+const DataCollectModel = model<{
+  id: Partial<string>;
+  type: 'channel' | 'device';
+  provider: 'OPC_UA' | 'MODBUS_TCP';
+}>({
+  type: 'channel',
+  id: '',
+  provider: 'MODBUS_TCP',
+});
+
+export default observer(() => {
+  const obj = {
+    channel: <Device type={false} id={DataCollectModel.id} />,
+    device: <Point type={false} provider={DataCollectModel.provider} id={DataCollectModel.id} />,
+  };
+  return (
+    <PageContainer>
+      <Card bordered={false}>
+        <div className={styles.container}>
+          <div className={styles.left}>
+            <ChannelTree
+              change={(key, type, provider) => {
+                DataCollectModel.id = key;
+                DataCollectModel.type = type;
+                DataCollectModel.provider = provider;
+              }}
+            />
+          </div>
+          <div className={styles.right}>{obj[DataCollectModel.type]}</div>
+        </div>
+      </Card>
+    </PageContainer>
+  );
+});

+ 43 - 0
src/pages/link/DataCollect/IntegratedQuery/index.tsx

@@ -0,0 +1,43 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { observer } from '@formily/reactive-react';
+import { model } from '@formily/reactive';
+import Point from '../components/Point';
+import Device from '../components/Device';
+import Channel from '../components/Channel';
+
+const dataModel = model<{
+  tab: string;
+}>({
+  tab: 'channel',
+});
+
+export default observer(() => {
+  const list = [
+    {
+      key: 'channel',
+      tab: '通道',
+      component: <Channel type={true} />,
+    },
+    {
+      key: 'device',
+      tab: '采集器',
+      component: <Device type={true} />,
+    },
+    {
+      key: 'point',
+      tab: '点位',
+      component: <Point type={true} />,
+    },
+  ];
+  return (
+    <PageContainer
+      tabList={list}
+      tabActiveKey={dataModel.tab}
+      onTabChange={(key: string) => {
+        dataModel.tab = key;
+      }}
+    >
+      {list.find((item) => item.key === dataModel.tab)?.component}
+    </PageContainer>
+  );
+});

+ 347 - 0
src/pages/link/DataCollect/components/Channel/Save/index.tsx

@@ -0,0 +1,347 @@
+import { Button, Modal } from 'antd';
+import { createForm, Field } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select, NumberPicker, Password } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import service from '@/pages/link/DataCollect/service';
+import { PermissionButton } from '@/components';
+import { onlyMessage } from '@/utils/util';
+import { action } from '@formily/reactive';
+
+interface Props {
+  data: Partial<ChannelItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+export default (props: Props) => {
+  const { permission } = PermissionButton.usePermission('link/Protocol');
+  const [data, setData] = useState<Partial<ChannelItem>>(props.data);
+  const [loading, setLoading] = useState<boolean>(false);
+
+  useEffect(() => {
+    if (props.data?.id) {
+      service.queryChannelByID(props.data.id).then((resp) => {
+        if (resp.status === 200) {
+          setData(resp.result);
+        }
+      });
+    }
+  }, [props.data]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data || {},
+  });
+
+  const getSecurityPolicyList = () => service.querySecurityPolicyList({});
+
+  const useAsyncDataSource = (services: (arg0: Field) => Promise<any>) => (field: Field) => {
+    field.loading = true;
+    services(field).then(
+      action.bound!((resp: any) => {
+        field.dataSource = (resp?.result || []).map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      NumberPicker,
+      Password,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          provider: {
+            title: '通讯协议',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择通讯协议',
+            },
+            enum: [
+              { label: 'OPC_UA', value: 'OPC_UA' },
+              { label: 'MODBUS_TCP', value: 'MODBUS_TCP' },
+            ],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择通讯协议',
+              },
+            ],
+          },
+          'configuration.host': {
+            title: 'Modbus主机IP',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入Modbus主机IP',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入Modbus主机IP',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['..provider'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="MODBUS_TCP"}}',
+                },
+              },
+            },
+          },
+          'configuration.port': {
+            title: '端口',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入端口',
+            },
+            default: 502,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入端口',
+              },
+              {
+                max: 65535,
+                message: '请输入0-65535之间的整整数',
+              },
+              {
+                min: 0,
+                message: '请输入0-65535之间的整整数',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['..provider'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="MODBUS_TCP"}}',
+                },
+              },
+            },
+          },
+          'configuration.endpoint': {
+            title: '端点url',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入端点url',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入端点url',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['..provider'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="OPC_UA"}}',
+                },
+              },
+            },
+          },
+          'configuration.securityPolicy': {
+            title: '安全策略',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择安全策略',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择安全策略',
+              },
+            ],
+            'x-reactions': [
+              '{{useAsyncDataSource(getSecurityPolicyList)}}',
+              {
+                dependencies: ['..provider'],
+                fulfill: {
+                  state: {
+                    visible: '{{$deps[0]==="OPC_UA"}}',
+                  },
+                },
+              },
+            ],
+          },
+          'configuration.username': {
+            title: '用户名',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入用户名',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入用户名',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['..provider'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="OPC_UA"}}',
+                },
+              },
+            },
+          },
+          'configuration.password': {
+            title: '密码',
+            'x-component': 'Password',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入密码',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入密码',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['..provider'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="OPC_UA"}}',
+                },
+              },
+            },
+          },
+          description: {
+            title: '说明',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<ChannelItem>();
+    setLoading(true);
+    const response: any = props.data?.id
+      ? await service.updateChannel(props.data?.id, { ...props.data, ...value })
+      : await service.saveChannel({ ...props.data, ...value });
+    setLoading(false);
+    if (response && response?.status === 200) {
+      onlyMessage('操作成功');
+      props.reload();
+    }
+  };
+
+  return (
+    <Modal
+      title={props?.data?.id ? '编辑' : '新增'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            save();
+          }}
+          loading={loading}
+          disabled={props.data?.id ? !permission.update : !permission.add}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getSecurityPolicyList }} />
+      </Form>
+    </Modal>
+  );
+};

+ 220 - 0
src/pages/link/DataCollect/components/Channel/index.tsx

@@ -0,0 +1,220 @@
+import { observer } from '@formily/react';
+import SearchComponent from '@/components/SearchComponent';
+import { ProColumns } from '@jetlinks/pro-table';
+import { useEffect, useState } from 'react';
+import { useDomFullHeight } from '@/hooks';
+import service from '@/pages/link/DataCollect/service';
+import ChannelCard from '@/components/ProTableCard/CardItems/DataCollect/channel';
+import { Empty, PermissionButton } from '@/components';
+import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
+import { onlyMessage } from '@/utils/util';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Card, Col, Pagination, Row } from 'antd';
+import { model } from '@formily/reactive';
+import Save from '@/pages/link/DataCollect/components/Channel/Save';
+
+interface Props {
+  type: boolean; // true: 综合查询  false: 数据采集
+}
+
+const ChannelModel = model<{
+  visible: boolean;
+  current: Partial<ChannelItem>;
+}>({
+  visible: false,
+  current: {},
+});
+
+export default observer((props: Props) => {
+  const intl = useIntl();
+  const { minHeight } = useDomFullHeight(`.data-collect-channel`, 24);
+  const [param, setParam] = useState({});
+  const { permission } = PermissionButton.usePermission('device/Instance');
+  const [loading, setLoading] = useState<boolean>(true);
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const columns: ProColumns<ChannelItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '通讯协议',
+      dataIndex: 'provider',
+      valueType: 'select',
+      valueEnum: {
+        OPC_UA: {
+          text: 'OPC_UA',
+          status: 'OPC_UA',
+        },
+        MODBUS_TCP: {
+          text: 'MODBUS_TCP',
+          status: 'MODBUS_TCP',
+        },
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        enabled: {
+          text: '正常',
+          status: 'enabled',
+        },
+        disabled: {
+          text: '禁用',
+          status: 'disabled',
+        },
+      },
+    },
+  ];
+
+  const handleSearch = (params: any) => {
+    setLoading(true);
+    setParam(params);
+    service
+      .queryChannel({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+        setLoading(false);
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, []);
+
+  return (
+    <div>
+      <SearchComponent<ChannelItem>
+        field={columns}
+        target="data-collect-channel"
+        onSearch={(data) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card loading={loading} bordered={false}>
+        <div style={{ position: 'relative', minHeight }}>
+          <div style={{ height: '100%', paddingBottom: 48 }}>
+            {dataSource?.data.length ? (
+              <>
+                <Row gutter={[24, 24]} style={{ marginTop: 10 }}>
+                  {(dataSource?.data || []).map((record: any) => (
+                    <Col key={record.id} span={props.type ? 8 : 12}>
+                      <ChannelCard
+                        {...record}
+                        actions={[
+                          <PermissionButton
+                            type={'link'}
+                            onClick={() => {
+                              ChannelModel.current = record;
+                              ChannelModel.visible = true;
+                            }}
+                            key={'edit'}
+                            isPermission={permission.update}
+                          >
+                            <EditOutlined />
+                            {intl.formatMessage({
+                              id: 'pages.data.option.edit',
+                              defaultMessage: '编辑',
+                            })}
+                          </PermissionButton>,
+                          <PermissionButton
+                            key="delete"
+                            isPermission={permission.delete}
+                            type={'link'}
+                            style={{ padding: 0 }}
+                            popConfirm={{
+                              title: intl.formatMessage({
+                                id: 'pages.data.option.remove.tips',
+                              }),
+                              disabled: record?.state?.value !== 'disabled',
+                              onConfirm: async () => {
+                                await service.removeChannel(record.id);
+                                onlyMessage(
+                                  intl.formatMessage({
+                                    id: 'pages.data.option.success',
+                                    defaultMessage: '操作成功!',
+                                  }),
+                                );
+                              },
+                            }}
+                          >
+                            <DeleteOutlined />
+                          </PermissionButton>,
+                        ]}
+                      />
+                    </Col>
+                  ))}
+                </Row>
+                <div
+                  style={{
+                    display: 'flex',
+                    justifyContent: 'flex-end',
+                    position: 'absolute',
+                    width: '100%',
+                    bottom: 0,
+                  }}
+                >
+                  <Pagination
+                    showSizeChanger
+                    size="small"
+                    className={'pro-table-card-pagination'}
+                    total={dataSource?.total || 0}
+                    current={dataSource?.pageIndex + 1}
+                    onChange={(page, size) => {
+                      handleSearch({
+                        ...param,
+                        pageIndex: page - 1,
+                        pageSize: size,
+                      });
+                    }}
+                    pageSizeOptions={[10, 20, 50, 100]}
+                    pageSize={dataSource?.pageSize}
+                    showTotal={(num) => {
+                      const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                      const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                      return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+                    }}
+                  />
+                </div>
+              </>
+            ) : (
+              <div style={{ height: minHeight - 150 }}>
+                <Empty />
+              </div>
+            )}
+          </div>
+        </div>
+      </Card>
+      {ChannelModel.visible && (
+        <Save
+          data={ChannelModel.current}
+          close={() => {
+            ChannelModel.visible = false;
+          }}
+          reload={() => {
+            ChannelModel.visible = false;
+            handleSearch(param);
+          }}
+        />
+      )}
+    </div>
+  );
+});

+ 201 - 0
src/pages/link/DataCollect/components/Device/Save/index.tsx

@@ -0,0 +1,201 @@
+import { Button, Modal } from 'antd';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select, NumberPicker, Password } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import service from '@/pages/link/DataCollect/service';
+import { PermissionButton } from '@/components';
+import { onlyMessage } from '@/utils/util';
+
+interface Props {
+  channelId?: string;
+  data: Partial<CollectorItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+export default (props: Props) => {
+  const { permission } = PermissionButton.usePermission('link/Protocol');
+  const [data, setData] = useState<Partial<ChannelItem>>(props.data);
+
+  useEffect(() => {
+    setData(props.data);
+  }, [props.data]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data || {},
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      NumberPicker,
+      Password,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          'configuration.unitId': {
+            title: '从机地址',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入从机地址',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入从机地址',
+              },
+              {
+                max: 255,
+                message: '请输入0-255之间的整整数',
+              },
+              {
+                min: 0,
+                message: '请输入0-255之间的整整数',
+              },
+            ],
+          },
+          'circuitBreaker.type': {
+            title: '处理方式',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择处理方式',
+            },
+            default: 'LowerFrequency',
+            enum: [
+              { label: '降频', value: 'LowerFrequency' },
+              { label: '熔断', value: 'Break' },
+              { label: '忽略', value: 'Ignore' },
+            ],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择处理方式',
+              },
+            ],
+          },
+          description: {
+            title: '说明',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<CollectorItem>();
+    let response: any = null;
+    if (props.data?.id) {
+      response = await service.updateCollector(props.data?.id, { ...props.data, ...value });
+    } else {
+      if (props.channelId) {
+        const resp = await service.queryChannelByID(props.channelId);
+        if (resp.status === 200) {
+          const obj = {
+            ...value,
+            provider: resp.result.provider,
+            channelId: resp.result.channelId,
+            configuration: {},
+          };
+          response = await service.saveCollector({ ...obj });
+        }
+      }
+    }
+    if (response && response?.status === 200) {
+      onlyMessage('操作成功');
+      props.reload();
+    }
+  };
+
+  return (
+    <Modal
+      title={props?.data?.id ? '编辑' : '新增'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            save();
+          }}
+          disabled={props.data?.id ? !permission.update : !permission.add}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};

+ 306 - 0
src/pages/link/DataCollect/components/Device/index.tsx

@@ -0,0 +1,306 @@
+import { observer } from '@formily/react';
+import SearchComponent from '@/components/SearchComponent';
+import type { ProColumns } from '@jetlinks/pro-table';
+import { useEffect, useState } from 'react';
+import { useDomFullHeight } from '@/hooks';
+import service from '@/pages/link/DataCollect/service';
+import CollectorCard from '@/components/ProTableCard/CardItems/DataCollect/device';
+import { Empty, PermissionButton } from '@/components';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { DeleteOutlined, EditOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
+import { onlyMessage } from '@/utils/util';
+import { Card, Col, Pagination, Row } from 'antd';
+import { model } from '@formily/reactive';
+import Save from '@/pages/link/DataCollect/components/Device/Save/index';
+
+interface Props {
+  type: boolean; // true: 综合查询  false: 数据采集
+  id?: any;
+}
+
+const CollectorModel = model<{
+  visible: boolean;
+  current: Partial<CollectorItem>;
+}>({
+  visible: false,
+  current: {},
+});
+
+export default observer((props: Props) => {
+  const { minHeight } = useDomFullHeight(`.data-collect-collector`, 24);
+  const [param, setParam] = useState({
+    terms: [],
+  });
+  const [loading, setLoading] = useState<boolean>(true);
+  const intl = useIntl();
+  const { permission } = PermissionButton.usePermission('device/Instance');
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const columns: ProColumns<CollectorItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '通讯协议',
+      dataIndex: 'provider',
+      valueType: 'select',
+      valueEnum: {
+        OPC_UA: {
+          text: 'OPC_UA',
+          status: 'OPC_UA',
+        },
+        MODBUS_TCP: {
+          text: 'MODBUS_TCP',
+          status: 'MODBUS_TCP',
+        },
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        enabled: {
+          text: '正常',
+          status: 'enabled',
+        },
+        disabled: {
+          text: '禁用',
+          status: 'disabled',
+        },
+      },
+    },
+  ];
+  const handleSearch = (params: any) => {
+    setLoading(true);
+    setParam(params);
+    service
+      .queryCollector({
+        ...params,
+        terms: [
+          ...params?.terms,
+          {
+            terms: [{ column: 'channelId', value: props?.id }],
+          },
+        ],
+        sorts: [{ name: 'createTime', order: 'desc' }],
+      })
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+        setLoading(false);
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, [props.id]);
+
+  return (
+    <div>
+      <SearchComponent<CollectorItem>
+        field={columns}
+        target="data-collect-collector"
+        onSearch={(data) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card bordered={false} loading={loading}>
+        <div style={{ minHeight, position: 'relative' }}>
+          <div style={{ paddingBottom: 48, height: '100%' }}>
+            {!props.type && (
+              <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
+                <PermissionButton
+                  isPermission={permission.add}
+                  onClick={() => {
+                    CollectorModel.current = {};
+                    CollectorModel.visible = true;
+                  }}
+                  key="button"
+                  type="primary"
+                >
+                  新增
+                </PermissionButton>
+              </div>
+            )}
+            {dataSource?.data.length ? (
+              <>
+                <Row gutter={[18, 18]} style={{ marginTop: 10 }}>
+                  {(dataSource?.data || []).map((record: any) => (
+                    <Col key={record.id} span={props.type ? 8 : 12}>
+                      <CollectorCard
+                        {...record}
+                        actions={[
+                          <PermissionButton
+                            type={'link'}
+                            onClick={() => {
+                              CollectorModel.current = { ...record };
+                              CollectorModel.visible = true;
+                            }}
+                            key={'edit'}
+                            isPermission={permission.update}
+                          >
+                            <EditOutlined />
+                            {intl.formatMessage({
+                              id: 'pages.data.option.edit',
+                              defaultMessage: '编辑',
+                            })}
+                          </PermissionButton>,
+                          <PermissionButton
+                            key={'action'}
+                            type={'link'}
+                            style={{ padding: 0 }}
+                            isPermission={permission.action}
+                            popConfirm={{
+                              title: intl.formatMessage({
+                                id: `pages.data.option.${
+                                  record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+                                }.tips`,
+                                defaultMessage: '确认禁用?',
+                              }),
+                              onConfirm: async () => {
+                                const resp =
+                                  record?.state?.value !== 'disabled'
+                                    ? await service.updateCollector(record.id, {
+                                        state: 'disabled',
+                                      })
+                                    : await service.updateCollector(record.id, {
+                                        state: 'enabled',
+                                      });
+                                if (resp.status === 200) {
+                                  onlyMessage('操作成功!');
+                                  handleSearch(param);
+                                } else {
+                                  onlyMessage('操作失败!', 'error');
+                                }
+                              },
+                            }}
+                          >
+                            {record?.state?.value !== 'disabled' ? (
+                              <StopOutlined />
+                            ) : (
+                              <PlayCircleOutlined />
+                            )}
+                            {intl.formatMessage({
+                              id: `pages.data.option.${
+                                record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+                              }`,
+                              defaultMessage: record?.state?.value !== 'disabled' ? '禁用' : '启用',
+                            })}
+                          </PermissionButton>,
+                          <PermissionButton
+                            key="delete"
+                            isPermission={permission.delete}
+                            type={'link'}
+                            style={{ padding: 0 }}
+                            tooltip={
+                              record?.state?.value !== 'disabled'
+                                ? {
+                                    title: intl.formatMessage({
+                                      id: 'pages.device.instance.deleteTip',
+                                    }),
+                                  }
+                                : undefined
+                            }
+                            disabled={record?.state?.value !== 'disabled'}
+                            popConfirm={{
+                              title: intl.formatMessage({
+                                id: 'pages.data.option.remove.tips',
+                              }),
+                              disabled: record?.state?.value !== 'disabled',
+                              onConfirm: async () => {
+                                if (record?.state?.value === 'disabled') {
+                                  await service.removeCollector(record.id);
+                                  onlyMessage(
+                                    intl.formatMessage({
+                                      id: 'pages.data.option.success',
+                                      defaultMessage: '操作成功!',
+                                    }),
+                                  );
+                                } else {
+                                  onlyMessage(
+                                    intl.formatMessage({ id: 'pages.device.instance.deleteTip' }),
+                                    'error',
+                                  );
+                                }
+                              },
+                            }}
+                          >
+                            <DeleteOutlined />
+                          </PermissionButton>,
+                        ]}
+                      />
+                    </Col>
+                  ))}
+                </Row>
+                <div
+                  style={{
+                    display: 'flex',
+                    justifyContent: 'flex-end',
+                    position: 'absolute',
+                    width: '100%',
+                    bottom: 0,
+                  }}
+                >
+                  <Pagination
+                    showSizeChanger
+                    size="small"
+                    className={'pro-table-card-pagination'}
+                    total={dataSource?.total || 0}
+                    current={dataSource?.pageIndex + 1}
+                    onChange={(page, size) => {
+                      handleSearch({
+                        ...param,
+                        pageIndex: page - 1,
+                        pageSize: size,
+                      });
+                    }}
+                    pageSizeOptions={[10, 20, 50, 100]}
+                    pageSize={dataSource?.pageSize}
+                    showTotal={(num) => {
+                      const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                      const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                      return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+                    }}
+                  />
+                </div>
+              </>
+            ) : (
+              <div style={{ height: minHeight - 150 }}>
+                <Empty />
+              </div>
+            )}
+          </div>
+        </div>
+      </Card>
+      {CollectorModel.visible && (
+        <Save
+          data={CollectorModel.current}
+          channelId={props.id}
+          close={() => {
+            CollectorModel.visible = false;
+          }}
+          reload={() => {
+            CollectorModel.visible = false;
+            handleSearch(param);
+          }}
+        />
+      )}
+    </div>
+  );
+});

+ 291 - 0
src/pages/link/DataCollect/components/Point/Save/modbus.tsx

@@ -0,0 +1,291 @@
+import { Button, Modal } from 'antd';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select, NumberPicker, Password } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+// import service from '@/pages/link/DataCollect/service';
+import { PermissionButton } from '@/components';
+// import { onlyMessage } from '@/utils/util';
+
+interface Props {
+  data: Partial<PointItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+export default (props: Props) => {
+  const { permission } = PermissionButton.usePermission('link/Protocol');
+  const [data, setData] = useState<Partial<PointItem>>(props.data);
+
+  useEffect(() => {
+    setData(props.data);
+  }, [props.data]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data || {},
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      NumberPicker,
+      Password,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '点位名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入点位名称',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入点位名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          'configuration.function': {
+            title: '功能码',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择功能码',
+            },
+            enum: [
+              { label: '离散输入寄存器', value: 'DiscreteInputs' },
+              { label: '保存寄存器', value: 'HoldingRegisters' },
+              { label: '输入寄存器', value: 'InputRegisters' },
+            ],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择功能码',
+              },
+            ],
+          },
+          'configuration.parameter.address': {
+            title: '地址',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入地址',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入地址',
+              },
+              {
+                max: 255,
+                message: '请输入0-255之间的整整数',
+              },
+              {
+                min: 0,
+                message: '请输入0-255之间的整整数',
+              },
+            ],
+          },
+          'configuration.codec.configuration.readIndex': {
+            title: '起始位置',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入起始位置',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入起始位置',
+              },
+            ],
+          },
+          'configuration.parameter.quantity': {
+            title: '寄存器数量',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入寄存器数量',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入寄存器数量',
+              },
+            ],
+          },
+          'configuration.codec.provider': {
+            title: '数据类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择数据类型',
+            },
+            enum: [],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择数据类型',
+              },
+            ],
+          },
+          'configuration.codec.configuration.scaleFactor': {
+            title: '缩放因子',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            default: 1,
+            'x-component-props': {
+              placeholder: '请输入缩放因子',
+            },
+            enum: [],
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入缩放因子',
+              },
+            ],
+          },
+          accessModes: {
+            title: '访问类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择访问类型',
+            },
+            enum: [
+              { label: '读', value: 'read' },
+              { label: '写', value: 'write' },
+              { label: '订阅', value: 'subscribe' },
+            ],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择访问类型',
+              },
+            ],
+          },
+          'configuration.interval': {
+            title: '采集频率',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            default: 3,
+            'x-component-props': {
+              placeholder: '请输入采集频率',
+              addonAfter: '秒',
+              style: {
+                width: '100%',
+              },
+            },
+            enum: [],
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入采集频率',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    // const value = await form.submit<ProtocolItem>();
+    // const response: any = props.data?.id
+    //   ? await service.savePatch({ ...props.data, ...value })
+    //   : await service.save({ ...props.data, ...value });
+    // if (response && response?.status === 200) {
+    //   onlyMessage('操作成功');
+    //   props.reload();
+    //   if ((window as any).onTabSaveSuccess) {
+    //     (window as any).onTabSaveSuccess(response);
+    //     setTimeout(() => window.close(), 300);
+    //   }
+    // }
+  };
+
+  return (
+    <Modal
+      title={props?.data?.id ? '编辑' : '新增'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            save();
+          }}
+          disabled={props.data?.id ? !permission.update : !permission.add}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};

+ 191 - 0
src/pages/link/DataCollect/components/Point/Save/opc-ua.tsx

@@ -0,0 +1,191 @@
+import { Button, Modal } from 'antd';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select, NumberPicker, Password } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+// import service from '@/pages/link/DataCollect/service';
+import { PermissionButton } from '@/components';
+// import { onlyMessage } from '@/utils/util';
+
+interface Props {
+  data: Partial<PointItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+export default (props: Props) => {
+  const { permission } = PermissionButton.usePermission('link/Protocol');
+  const [data, setData] = useState<Partial<PointItem>>(props.data);
+
+  useEffect(() => {
+    setData(props.data);
+  }, [props.data]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data || {},
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      NumberPicker,
+      Password,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '点位名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入点位名称',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入点位名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          'configuration.codec.provider': {
+            title: '数据类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择数据类型',
+            },
+            enum: [],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择数据类型',
+              },
+            ],
+          },
+          accessModes: {
+            title: '访问类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择访问类型',
+            },
+            enum: [
+              { label: '读', value: 'read' },
+              { label: '写', value: 'write' },
+              { label: '订阅', value: 'subscribe' },
+            ],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择访问类型',
+              },
+            ],
+          },
+          'configuration.interval': {
+            title: '采集频率',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            default: 3,
+            'x-component-props': {
+              placeholder: '请输入采集频率',
+              addonAfter: '秒',
+              style: {
+                width: '100%',
+              },
+            },
+            enum: [],
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入采集频率',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    // const value = await form.submit<ProtocolItem>();
+    // const response: any = props.data?.id
+    //   ? await service.savePatch({ ...props.data, ...value })
+    //   : await service.save({ ...props.data, ...value });
+    // if (response && response?.status === 200) {
+    //   onlyMessage('操作成功');
+    //   props.reload();
+    //   if ((window as any).onTabSaveSuccess) {
+    //     (window as any).onTabSaveSuccess(response);
+    //     setTimeout(() => window.close(), 300);
+    //   }
+    // }
+  };
+
+  return (
+    <Modal
+      title={props?.data?.id ? '编辑' : '新增'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            save();
+          }}
+          disabled={props.data?.id ? !permission.update : !permission.add}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};

+ 144 - 0
src/pages/link/DataCollect/components/Point/Save/scan.tsx

@@ -0,0 +1,144 @@
+import { Button, Modal, Transfer, Tree } from 'antd';
+import type { TransferDirection, TransferItem } from 'antd/es/transfer';
+import { DataNode } from 'antd/es/tree';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  // data: Partial<PointItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+interface TreeTransferProps {
+  dataSource: DataNode[];
+  targetKeys: string[];
+  onChange: (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void;
+}
+
+const TreeTransfer = ({ dataSource, targetKeys, ...restProps }: TreeTransferProps) => {
+  const transferDataSource: TransferItem[] = [];
+  function flatten(list: DataNode[] = []) {
+    list.forEach((item) => {
+      transferDataSource.push(item as TransferItem);
+      flatten(item.children);
+    });
+  }
+  useEffect(() => {
+    flatten(dataSource);
+  }, [dataSource]);
+
+  // Customize Table Transfer
+  const isChecked = (selectedKeys: (string | number)[], eventKey: string | number) =>
+    selectedKeys.includes(eventKey);
+
+  const generateTree = (treeNodes: DataNode[] = [], checkedKeys: string[] = []): DataNode[] =>
+    treeNodes.map(({ children, ...props }) => ({
+      ...props,
+      disabled: checkedKeys.includes(props.key as string),
+      children: generateTree(children, checkedKeys),
+    }));
+
+  // const queryTree = (treeNodes: DataNode[] = [], key: string) => {
+  //   treeNodes.forEach(item => {
+  //     if(item.key === key){
+  //       return item
+  //     }
+  //     if (item.children) {
+  //       return queryTree(item.children)
+  //     }
+  //     return {}
+  //   })
+  //   return {}
+  // }
+  //
+  // const generateTargetTree = (treeNodes: DataNode[] = []) => {
+  //   return targetKeys.map(item => {
+  //     const obj = {}
+  //     const obj = treeNodes.find(i => item === i.key)
+  //     return obj
+  //   })
+  // }
+
+  console.log(targetKeys);
+
+  return (
+    <Transfer
+      {...restProps}
+      targetKeys={targetKeys}
+      dataSource={transferDataSource}
+      className="tree-transfer"
+      render={(item) => item.title!}
+      showSelectAll={false}
+    >
+      {({ direction, onItemSelect, selectedKeys }) => {
+        if (direction === 'left') {
+          const checkedKeys = [...selectedKeys, ...targetKeys];
+          return (
+            <Tree
+              blockNode
+              checkable
+              checkStrictly
+              defaultExpandAll
+              checkedKeys={checkedKeys}
+              treeData={generateTree(dataSource, targetKeys)}
+              onCheck={(_, { node: { key } }) => {
+                onItemSelect(key as string, !isChecked(checkedKeys, key));
+              }}
+              onSelect={(_, { node: { key } }) => {
+                onItemSelect(key as string, !isChecked(checkedKeys, key));
+              }}
+            />
+          );
+        } else {
+          return <Tree blockNode defaultExpandAll treeData={[]} />;
+        }
+      }}
+    </Transfer>
+  );
+};
+
+export default (props: Props) => {
+  const [targetKeys, setTargetKeys] = useState<string[]>([]);
+  const onChange = (keys: string[]) => {
+    setTargetKeys(keys);
+  };
+
+  const treeData: DataNode[] = [
+    { key: '0-0', title: '0-0' },
+    {
+      key: '0-1',
+      title: '0-1',
+      children: [
+        { key: '0-1-0', title: '0-1-0' },
+        { key: '0-1-1', title: '0-1-1' },
+      ],
+    },
+    { key: '0-2', title: '0-3' },
+  ];
+
+  return (
+    <Modal
+      title={'扫描'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            // save();
+          }}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
+    </Modal>
+  );
+};

+ 211 - 0
src/pages/link/DataCollect/components/Point/index.tsx

@@ -0,0 +1,211 @@
+import { observer } from '@formily/react';
+import SearchComponent from '@/components/SearchComponent';
+import type { ProColumns } from '@jetlinks/pro-table';
+import { useEffect, useState } from 'react';
+import { useDomFullHeight } from '@/hooks';
+import service from '@/pages/link/DataCollect/service';
+import CollectorCard from '@/components/ProTableCard/CardItems/DataCollect/device';
+import { Empty, PermissionButton } from '@/components';
+import { Card, Col, Pagination, Row } from 'antd';
+import { model } from '@formily/reactive';
+import ModbusSave from '@/pages/link/DataCollect/components/Point/Save/modbus';
+import OpcUASave from '@/pages/link/DataCollect/components/Point/Save/opc-ua';
+import Scan from '@/pages/link/DataCollect/components/Point/Save/scan';
+
+interface Props {
+  type: boolean; // true: 综合查询  false: 数据采集
+  id?: Partial<string>;
+  provider?: 'OPC_UA' | 'MODBUS_TCP';
+}
+
+const PointModel = model<{
+  m_visible: boolean;
+  p_visible: boolean;
+  p_add_visible: boolean;
+  current: Partial<PointItem>;
+}>({
+  m_visible: false,
+  p_visible: false,
+  p_add_visible: false,
+  current: {},
+});
+
+export default observer((props: Props) => {
+  const { minHeight } = useDomFullHeight(`.data-collect-point`, 24);
+  const [param, setParam] = useState({
+    terms: [],
+  });
+  const [loading, setLoading] = useState<boolean>(true);
+  const { permission } = PermissionButton.usePermission('device/Instance');
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const columns: ProColumns<PointItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 100,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      ellipsis: true,
+      width: 200,
+    },
+  ];
+  const handleSearch = (params: any) => {
+    setLoading(true);
+    setParam(params);
+    service
+      .queryPoint({
+        ...params,
+        terms: [
+          ...params?.terms,
+          {
+            terms: [{ column: 'collectorId', value: props.id }],
+          },
+        ],
+        sorts: [{ name: 'createTime', order: 'desc' }],
+      })
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+        setLoading(false);
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, [props.id]);
+
+  return (
+    <div>
+      <SearchComponent<PointItem>
+        field={columns}
+        target="data-collect-point"
+        onSearch={(data) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card loading={loading} bordered={false}>
+        <div style={{ position: 'relative', minHeight }}>
+          <div style={{ height: '100%', paddingBottom: 48 }}>
+            {!props.type && (
+              <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
+                <PermissionButton
+                  isPermission={permission.add}
+                  onClick={() => {
+                    if (props.provider === 'OPC_UA') {
+                      PointModel.p_add_visible = true;
+                    } else {
+                      PointModel.m_visible = true;
+                    }
+                    PointModel.current = {};
+                  }}
+                  key="button"
+                  type="primary"
+                >
+                  新增
+                </PermissionButton>
+              </div>
+            )}
+            {dataSource?.data.length > 0 ? (
+              <>
+                <Row gutter={[18, 18]} style={{ marginTop: 10 }}>
+                  {(dataSource?.data || []).map((record: any) => (
+                    <Col key={record.id} span={props.type ? 8 : 12}>
+                      <CollectorCard {...record} />
+                    </Col>
+                  ))}
+                </Row>
+                <div
+                  style={{
+                    display: 'flex',
+                    justifyContent: 'flex-end',
+                    position: 'absolute',
+                    width: '100%',
+                    bottom: 0,
+                  }}
+                >
+                  <Pagination
+                    showSizeChanger
+                    size="small"
+                    className={'pro-table-card-pagination'}
+                    total={dataSource?.total || 0}
+                    current={dataSource?.pageIndex + 1}
+                    onChange={(page, size) => {
+                      handleSearch({
+                        ...param,
+                        pageIndex: page - 1,
+                        pageSize: size,
+                      });
+                    }}
+                    pageSizeOptions={[10, 20, 50, 100]}
+                    pageSize={dataSource?.pageSize}
+                    showTotal={(num) => {
+                      const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                      const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                      return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+                    }}
+                  />
+                </div>
+              </>
+            ) : (
+              <div style={{ height: minHeight - 150 }}>
+                <Empty />
+              </div>
+            )}
+          </div>
+        </div>
+      </Card>
+      {PointModel.m_visible && (
+        <ModbusSave
+          data={PointModel.current}
+          // channelId={props.id}
+          close={() => {
+            PointModel.m_visible = false;
+          }}
+          reload={() => {
+            PointModel.m_visible = false;
+            handleSearch(param);
+          }}
+        />
+      )}
+      {PointModel.p_visible && (
+        <OpcUASave
+          data={PointModel.current}
+          // channelId={props.id}
+          close={() => {
+            PointModel.p_visible = false;
+          }}
+          reload={() => {
+            PointModel.p_visible = false;
+            handleSearch(param);
+          }}
+        />
+      )}
+      {PointModel.p_add_visible && (
+        <Scan
+          close={() => {
+            PointModel.p_add_visible = false;
+          }}
+          reload={() => {
+            PointModel.p_add_visible = false;
+            handleSearch(param);
+          }}
+        />
+      )}
+    </div>
+  );
+});

+ 16 - 0
src/pages/link/DataCollect/components/Tree/index.less

@@ -0,0 +1,16 @@
+.treeTitle {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 240px;
+}
+
+.title {
+  display: flex;
+  align-items: center;
+  width: 200px;
+}
+
+.iconColor {
+  color: #999;
+}

+ 168 - 0
src/pages/link/DataCollect/components/Tree/index.tsx

@@ -0,0 +1,168 @@
+import { DownOutlined, PlusOutlined, FormOutlined, DeleteOutlined } from '@ant-design/icons';
+import { Button, Input, Tree, Space, Popconfirm } from 'antd';
+import { observer } from '@formily/react';
+import { model } from '@formily/reactive';
+import { Empty } from '@/components';
+import styles from './index.less';
+import service from '@/pages/link/DataCollect/service';
+import { useEffect } from 'react';
+import Save from '../../components/Channel/Save/index';
+import { onlyMessage } from '@/utils/util';
+
+const TreeModel = model<{
+  selectedKeys: string[];
+  dataSource: any[];
+  loading: boolean;
+  param: any;
+  visible: boolean;
+  current: Partial<ChannelItem>;
+}>({
+  selectedKeys: [],
+  dataSource: [],
+  loading: true,
+  param: {},
+  visible: false,
+  current: {},
+});
+interface Props {
+  change: (key: string, type: 'channel' | 'device', provider: 'OPC_UA' | 'MODBUS_TCP') => void;
+}
+
+export default observer((props: Props) => {
+  const channelImg = require('/public/images/DataCollect/tree-channel.png');
+  const deviceImg = require('/public/images/DataCollect/tree-device.png');
+
+  const handleSearch = (params: any) => {
+    TreeModel.loading = true;
+    TreeModel.param = params;
+    service
+      .queryChannelTree({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+      .then((resp) => {
+        if (resp.status === 200) {
+          TreeModel.dataSource = resp.result;
+          if (resp.result.length) {
+            TreeModel.selectedKeys = [resp.result[0].id];
+            props.change(resp.result[0].id, 'channel', resp.result[0].provider);
+          }
+        }
+        TreeModel.loading = false;
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(TreeModel.param);
+  }, [TreeModel.param]);
+
+  return (
+    <div>
+      <div>
+        <Input.Search
+          placeholder="搜索"
+          onSearch={(val) => {
+            TreeModel.param = {
+              terms: [{ column: 'name', value: `%${val}%`, termType: 'like' }],
+            };
+          }}
+          style={{ width: '100%' }}
+        />
+      </div>
+      <div style={{ margin: '16px 0' }}>
+        <Button
+          type="primary"
+          ghost
+          style={{ width: '100%' }}
+          icon={<PlusOutlined />}
+          onClick={() => {
+            TreeModel.visible = true;
+            TreeModel.current = {};
+          }}
+        >
+          新增
+        </Button>
+      </div>
+      <div>
+        {TreeModel.dataSource.length ? (
+          <Tree showIcon selectedKeys={TreeModel.selectedKeys} switcherIcon={<DownOutlined />}>
+            {(TreeModel.dataSource || []).map((item: any) => (
+              <Tree.TreeNode
+                key={item.id}
+                title={() => {
+                  return (
+                    <div className={styles.treeTitle}>
+                      <div
+                        className={styles.title}
+                        onClick={() => {
+                          TreeModel.selectedKeys = [item.id];
+                          props.change(item.id, 'channel', item.provider);
+                        }}
+                      >
+                        <img width={'20px'} style={{ marginRight: 5 }} src={channelImg} />
+                        <div className={'ellipsis'}>{item.name}</div>
+                      </div>
+                      <div>
+                        <Space className={styles.iconColor}>
+                          <FormOutlined
+                            onClick={() => {
+                              TreeModel.current = item;
+                              TreeModel.visible = true;
+                            }}
+                          />
+                          <Popconfirm
+                            title={'确认删除?'}
+                            onConfirm={async () => {
+                              const resp = await service.removeChannel(item.id);
+                              if (resp.status === 200) {
+                                TreeModel.param = {};
+                                handleSearch(TreeModel.param);
+                                onlyMessage('操作成功');
+                              }
+                            }}
+                          >
+                            <DeleteOutlined />
+                          </Popconfirm>
+                        </Space>
+                      </div>
+                    </div>
+                  );
+                }}
+              >
+                {(item?.collectors || []).map((i: any) => (
+                  <Tree.TreeNode
+                    key={i.id}
+                    title={() => {
+                      return (
+                        <div
+                          className={styles.title}
+                          onClick={() => {
+                            TreeModel.selectedKeys = [i.id];
+                            props.change(i.id, 'device', item.provider);
+                          }}
+                        >
+                          <img width={'20px'} style={{ marginRight: 5 }} src={deviceImg} />
+                          <div className={'ellipsis'}>{i.name}</div>
+                        </div>
+                      );
+                    }}
+                  />
+                ))}
+              </Tree.TreeNode>
+            ))}
+          </Tree>
+        ) : (
+          <Empty />
+        )}
+      </div>
+      {TreeModel.visible && (
+        <Save
+          data={TreeModel.current}
+          close={() => {
+            TreeModel.visible = false;
+          }}
+          reload={() => {
+            TreeModel.visible = false;
+          }}
+        />
+      )}
+    </div>
+  );
+});

+ 5 - 0
src/pages/link/DataCollect/components/index.less

@@ -0,0 +1,5 @@
+:global {
+  .ant-pagination-item {
+    display: none;
+  }
+}

+ 101 - 0
src/pages/link/DataCollect/service.ts

@@ -0,0 +1,101 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service {
+  // 点位
+  public savePoint = (params: PointItem) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryPoint = (params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryPointByID = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/${id}`, {
+      method: 'GET',
+    });
+  public updatePoint = (id: string, params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/${id}`, {
+      method: 'PUT',
+      data: params,
+    });
+  public readPoint = (collectorId: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}data-collect/collector/${collectorId}/points/_read`, {
+      method: 'POST',
+      data,
+    });
+  public writePoint = (collectorId: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}data-collect/collector/${collectorId}/points/_write`, {
+      method: 'POST',
+      data,
+    });
+  public removePoint = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/${id}`, {
+      method: 'DELETE',
+    });
+  // 采集器
+  public saveCollector = (params: CollectorItem) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryCollector = (params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryCollectorByID = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector/${id}`, {
+      method: 'GET',
+    });
+  public updateCollector = (id: string, params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector/${id}`, {
+      method: 'PUT',
+      data: params,
+    });
+  public removeCollector = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector/${id}`, {
+      method: 'DELETE',
+    });
+  // 通道
+  public saveChannel = (params: ChannelItem) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryChannel = (params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/_query`, {
+      method: 'POST',
+      data: params,
+    });
+  public queryChannelByID = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/${id}`, {
+      method: 'GET',
+    });
+  public updateChannel = (id: string, params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/${id}`, {
+      method: 'PUT',
+      data: params,
+    });
+  public removeChannel = (id: string) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/${id}`, {
+      method: 'DELETE',
+    });
+  public queryChannelTree = (params: ChannelItem) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/_all/tree`, {
+      method: 'POST',
+      data: params,
+    });
+  public querySecurityPolicyList = (params: any) =>
+    request(`/${SystemConst.API_BASE}/edge/operations/local/opcua-security-policies/invoke`, {
+      method: 'POST',
+      data: params,
+    });
+}
+
+const service = new Service();
+
+export default service;

+ 105 - 0
src/pages/link/DataCollect/typings.d.ts

@@ -0,0 +1,105 @@
+type ModbusItem = {
+  interval?: number;
+  function: string;
+  parameter: {
+    address: string;
+    quantity: number;
+  };
+  codec: {
+    provider: string;
+    configuration: {
+      scaleFactor: number;
+      readIndex: number;
+    };
+  };
+};
+
+type OpcuaItem = {
+  interval?: number;
+  nodeId: string;
+};
+
+type PointItem = {
+  id?: string;
+  name: string;
+  description?: string;
+  provider: string;
+  collectorId: string;
+  circuitBreaker: {
+    type: 'Ignore' | 'Break' | 'LowerFrequency';
+    maxConsecutiveErrors?: string;
+  };
+  features?: string[];
+  accessModes: string[];
+  configuration: ModbusItem | OpcuaItem;
+  state?: {
+    text: string;
+    value: string;
+  };
+  runningState?: {
+    text: string;
+    value: string;
+  };
+};
+type CollectorModbusItem = {
+  unitId: number;
+};
+
+type CollectorOpcuaItem = {
+  batchSize?: number;
+};
+
+type CollectorItem = {
+  id: string;
+  name: string;
+  description?: string;
+  provider: 'OPC_UA' | 'MODBUS_TCP';
+  channelId: string;
+  circuitBreaker: {
+    type: 'Ignore' | 'Break' | 'LowerFrequency';
+    maxConsecutiveErrors?: string;
+  };
+  configuration: any; // CollectorModbusItem | CollectorOpcuaItem;
+  state: {
+    text: string;
+    value: string;
+  };
+  runningState: {
+    text: string;
+    value: string;
+  };
+  channelName?: string;
+  channelId?: string;
+};
+
+type ChannelModbusItem = {
+  port?: number;
+  host?: string;
+};
+
+type ChannelOpcuaItem = {
+  endpoint?: string;
+  securityPolicy?: string;
+  username?: string;
+  password?: string;
+};
+
+type ChannelItem = {
+  id: string;
+  name: string;
+  description?: string;
+  provider: 'OPC_UA' | 'MODBUS_TCP';
+  circuitBreaker: {
+    type: 'Ignore' | 'Break' | 'LowerFrequency';
+    maxConsecutiveErrors?: string;
+  };
+  configuration: any; //ChannelOpcuaItem | ChannelModbusItem;
+  state: {
+    text: string;
+    value: string;
+  };
+  runningState: {
+    text: string;
+    value: string;
+  };
+};

+ 24 - 32
src/pages/system/Department/Assets/index.tsx

@@ -67,20 +67,8 @@ const Assets = observer((props: AssetsProps) => {
   ];
 
   useEffect(() => {
-    if (isNoCommunity) {
-      AssetsModel.tabsIndex = ASSETS_TABS_ENUM.Product;
-      AssetsModel.tabsArray = [...TabsArray];
-    } else {
-      AssetsModel.tabsIndex = ASSETS_TABS_ENUM.User;
-      AssetsModel.tabsArray = [
-        {
-          intlTitle: '1',
-          defaultMessage: '用户',
-          key: ASSETS_TABS_ENUM.User,
-          components: Member,
-        },
-      ];
-    }
+    AssetsModel.tabsIndex = ASSETS_TABS_ENUM.Product;
+    AssetsModel.tabsArray = [...TabsArray];
   }, []);
 
   useEffect(() => {
@@ -93,24 +81,28 @@ const Assets = observer((props: AssetsProps) => {
         <ExclamationCircleOutlined style={{ marginRight: 12 }} />
         部门拥有的资产为所有类型资产的并集
       </div> */}
-      <Tabs
-        activeKey={AssetsModel.tabsIndex}
-        onChange={(key) => {
-          AssetsModel.tabsIndex = key;
-        }}
-      >
-        {(AssetsModel?.tabsArray || []).map((item) => (
-          <Tabs.TabPane
-            tab={intl.formatMessage({
-              id: item.intlTitle,
-              defaultMessage: item.defaultMessage,
-            })}
-            key={item.key}
-          >
-            <item.components parentId={props.parentId} />
-          </Tabs.TabPane>
-        ))}
-      </Tabs>
+      {isNoCommunity ? (
+        <Tabs
+          activeKey={AssetsModel.tabsIndex}
+          onChange={(key) => {
+            AssetsModel.tabsIndex = key;
+          }}
+        >
+          {(AssetsModel?.tabsArray || []).map((item) => (
+            <Tabs.TabPane
+              tab={intl.formatMessage({
+                id: item.intlTitle,
+                defaultMessage: item.defaultMessage,
+              })}
+              key={item.key}
+            >
+              <item.components parentId={props.parentId} />
+            </Tabs.TabPane>
+          ))}
+        </Tabs>
+      ) : (
+        <Member parentId={props.parentId} />
+      )}
     </div>
   );
 });

+ 27 - 1
src/pages/system/Department/Tree/tree.tsx

@@ -16,6 +16,7 @@ import { ISchema } from '@formily/json-schema';
 import { DepartmentItem } from '@/pages/system/Department/typings';
 import { onlyMessage } from '@/utils/util';
 import classnames from 'classnames';
+import _ from 'lodash';
 
 interface TreeProps {
   onSelect: (id: string) => void;
@@ -45,6 +46,7 @@ export const getSortIndex = (data: DepartmentItem[], pId?: string): number => {
 export default (props: TreeProps) => {
   const intl = useIntl();
   const [treeData, setTreeData] = useState<undefined | any[]>(undefined);
+  const [treeDataList, setTreeDataList] = useState<undefined | any[]>(undefined);
   const [loading, setLoading] = useState(false);
   const [keys, setKeys] = useState<any[]>([]);
   const [visible, setVisible] = useState(false);
@@ -79,6 +81,27 @@ export default (props: TreeProps) => {
     }
   };
 
+  const queryList = (list: any, id: string, flag?: boolean) => {
+    if (list && Array.isArray(list) && list.length) {
+      return list.map((item) => {
+        if (item.id === id || flag) {
+          item.disabled = true;
+        }
+        if (item.children && Array.isArray(item.children) && item.children.length) {
+          item.children = queryList(item.children, id, item.id === id || flag);
+        }
+        return item;
+      });
+    } else {
+      return [];
+    }
+  };
+
+  const updateOrg = (id: string) => {
+    const list = _.cloneDeep(treeData);
+    setTreeDataList(queryList(list, id));
+  };
+
   const deleteItem = async (id: string) => {
     const response: any = await service.remove(id);
     if (response.status === 200) {
@@ -112,7 +135,7 @@ export default (props: TreeProps) => {
           },
           placeholder: '请选择上级组织',
         },
-        enum: treeData,
+        enum: treeDataList,
       },
       name: {
         type: 'string',
@@ -167,6 +190,7 @@ export default (props: TreeProps) => {
     if ((location as any).query?.save === 'true') {
       setData({ sortIndex: treeData && treeData.length + 1 });
       setVisible(true);
+      setTreeDataList(treeData);
     }
   }, [location, treeData]);
 
@@ -201,6 +225,7 @@ export default (props: TreeProps) => {
         onClick={() => {
           setData({ sortIndex: treeData && treeData.length + 1 });
           setVisible(true);
+          setTreeDataList(treeData);
         }}
       >
         新增
@@ -266,6 +291,7 @@ export default (props: TreeProps) => {
                       type="link"
                       onClick={(e) => {
                         e.stopPropagation();
+                        updateOrg(nodeData.id);
                         setData({
                           ...nodeData,
                         });