Jelajahi Sumber

feat(modbus): modbus

wzyyy 3 tahun lalu
induk
melakukan
08586354a4

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

+ 1 - 1
src/pages/account/Center/index.tsx

@@ -141,7 +141,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="注册时间">

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

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

@@ -0,0 +1,370 @@
+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: '读取起始位置',
+      dataIndex: 'address',
+    },
+    {
+      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="请输入属性"
+                          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.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;

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

@@ -0,0 +1,172 @@
+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;
+}
+
+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;

+ 168 - 33
src/pages/link/Channel/Opcua/Access/addPoint/index.tsx

@@ -1,29 +1,115 @@
-import { Col, Form, Input, Modal, Row, Select, InputNumber, Radio } from 'antd';
-import { useEffect } from 'react';
+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();
-    console.log(formData);
+    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.deviceId);
+    console.log(props.data);
     service.deviceDetail(props.deviceId).then((res) => {
-      console.log(res);
+      if (res.result.metadata) {
+        const item = JSON.parse(res.result?.metadata);
+        setProperty(item.properties);
+      }
     });
+    if (props.data.enableCalculate) {
+      setEnableCalculate(true);
+    }
   }, []);
   return (
     <Modal
-      title="编辑"
+      title={props.data.id ? '编辑' : '新增'}
       visible
       width="40vw"
       destroyOnClose
@@ -32,7 +118,15 @@ const AddPoint = (props: Props) => {
         props.close();
       }}
     >
-      <Form form={form} layout="vertical" initialValues={{}}>
+      <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
@@ -41,10 +135,10 @@ const AddPoint = (props: Props) => {
               name="opcPointId"
               rules={[
                 { type: 'string', max: 64 },
-                { required: true, message: '姓名必填' },
+                { required: true, message: '请输入OPC点位ID' },
               ]}
             >
-              <Input placeholder="请输入姓名" />
+              <Input placeholder="请输入OPC点位ID" />
             </Form.Item>
           </Col>
         </Row>
@@ -56,55 +150,96 @@ const AddPoint = (props: Props) => {
               required
               rules={[{ required: true, message: '属性必选' }]}
             >
-              <Select>
-                <Select.Option value="demo">Demo</Select.Option>
+              <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">
-              <Select>
-                <Select.Option value="demo">Demo</Select.Option>
+            <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">
-              <Select>
-                <Select.Option value="demo">Demo</Select.Option>
+            <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">
-              <InputNumber />
+            <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">
-              <Radio.Group buttonStyle="solid">
+            <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>
-        <Row gutter={[24, 24]}>
-          <Col span={12}>
-            <Form.Item label="初始值" name="initialValue">
-              <Input placeholder="请输入姓名" />
-            </Form.Item>
-          </Col>
-          <Col span={12}>
-            <Form.Item label="倍数" name="multiple">
-              <InputNumber />
-            </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">

+ 219 - 36
src/pages/link/Channel/Opcua/Access/index.tsx

@@ -1,67 +1,208 @@
 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 } from 'antd';
+import { Badge, Card, Popconfirm, message, Tabs, Empty, Input } from 'antd';
 import { useIntl, useLocation } from 'umi';
 import { useEffect, useRef, useState } from 'react';
-import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
+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 = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const location = useLocation<string>();
-  // const [param, setParam] = useState({});
+  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<OpaUa>[] = [
+  const columns: ProColumns<any>[] = [
     {
       title: '属性ID',
-      dataIndex: 'name',
+      dataIndex: 'property',
     },
     {
-      title: '功能码',
-      dataIndex: '1',
+      title: '名称',
+      dataIndex: 'name',
     },
     {
-      title: '读取起始位置',
-      dataIndex: '2',
+      title: 'OPC点位ID',
+      dataIndex: 'opcPointId',
     },
     {
-      title: '读取长度',
-      dataIndex: '3',
+      title: '数据类型',
+      dataIndex: 'dataType',
     },
     {
       title: '值',
-      dataIndex: '4',
+      // dataIndex: '4',
+      render: (record: any) => <>{propertyValue[record.property]}</>,
     },
     {
       title: '状态',
       dataIndex: 'state',
       renderText: (state) => (
-        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+        <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');
@@ -98,6 +239,11 @@ const Access = () => {
             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) => (
@@ -130,34 +276,69 @@ const Access = () => {
                   </div>
                 }
               >
-                <ProTable<OpaUa>
+                <ProTable
                   actionRef={actionRef}
-                  // params={param}
+                  params={param}
                   columns={columns}
                   rowKey="id"
                   search={false}
                   headerTitle={
-                    <PermissionButton
-                      onClick={() => {
-                        setPointVisiable(true);
-                        // setMode('add');
-                        // setVisible(true);
-                        // setCurrent({});
-                      }}
-                      isPermission={permission.add}
-                      key="add"
-                      icon={<PlusOutlined />}
-                      type="primary"
-                    >
-                      {intl.formatMessage({
-                        id: 'pages.data.option.add',
-                        defaultMessage: '新增',
-                      })}
-                    </PermissionButton>
+                    <>
+                      <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) =>
-                  //   service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
-                  // }
+                  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>
             ))}
@@ -184,9 +365,11 @@ const Access = () => {
       {pointVisiable && (
         <AddPoint
           deviceId={deviceId}
-          data={{}}
+          opcUaId={opcUaId}
+          data={current}
           close={() => {
             setPointVisiable(false);
+            actionRef.current?.reload();
           }}
         />
       )}

+ 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 () => {

+ 32 - 3
src/pages/link/Channel/Opcua/service.ts

@@ -22,18 +22,18 @@ class Service extends BaseService<OpaUa> {
       params,
     });
   getDevice = (params?: any) =>
-    request(`${SystemConst.API_BASE}/device-instance/_query`, {
+    request(`/${SystemConst.API_BASE}/device-instance/_query`, {
       method: 'POST',
       data: params,
     });
   //绑定设备
   bind = (params: any) =>
-    request(`${SystemConst.API_BASE}/opc/device-bind/batch/_create`, {
+    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`, {
+    request(`/${SystemConst.API_BASE}/opc/device-bind/device-details/_query/no-paging`, {
       method: 'GET',
       params,
     });
@@ -46,6 +46,35 @@ class Service extends BaseService<OpaUa> {
     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,
+    });
 }
 
 export default Service;

+ 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

@@ -36,7 +36,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',