wzyyy 3 年 前
コミット
41868bab4b

+ 66 - 33
src/pages/device/Instance/Detail/Modbus/index.tsx

@@ -1,5 +1,5 @@
 import { FormItem, ArrayTable, Editable, Select, NumberPicker } from '@formily/antd';
-import { createForm, Field, onFieldReact, FormPath } from '@formily/core';
+import { createForm, Field, onFieldReact, FormPath, onFieldChange } from '@formily/core';
 import { FormProvider, createSchemaField } from '@formily/react';
 import { Badge, Card, Input, Tooltip } from 'antd';
 import { action } from '@formily/reactive';
@@ -23,8 +23,19 @@ export default (props: Props) => {
   const [properties, setProperties] = useState<any>([]);
   const [filterList, setFilterList] = useState<any>([]);
   const [masterList, setMasterList] = useState<any>([]);
+  const [typeList, setTypeList] = useState<any>([]);
   const [reload, setReload] = useState<string>('');
 
+  //数据类型长度
+  const lengthMap = new Map();
+  lengthMap.set('int8', 1);
+  lengthMap.set('int16', 2);
+  lengthMap.set('int32', 4);
+  lengthMap.set('int64', 8);
+  lengthMap.set('ieee754_float', 4);
+  lengthMap.set('ieee754_double', 8);
+  lengthMap.set('hex', 1);
+
   const Render = (propsText: any) => {
     const text = properties.find((item: any) => item.metadataId === propsText.value);
     return <>{text?.metadataName}</>;
@@ -199,13 +210,22 @@ export default (props: Props) => {
           setMasterList(list);
         }
       });
+    service.dataType().then((res) => {
+      if (res.status === 200) {
+        console.log(res.result);
+        const items = res.result.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        setTypeList(items);
+      }
+    });
   }, []);
   useEffect(() => {
     const metadata = JSON.parse(data.metadata).properties?.map((item: any) => ({
       metadataId: item.id,
       metadataName: `${item.name}(${item.id})`,
       metadataType: 'property',
-      codec: 'number',
     }));
     service.getDevicePoint(data.id).then((res) => {
       if (res.status === 200) {
@@ -239,9 +259,6 @@ export default (props: Props) => {
   });
 
   const form = createForm({
-    // initialValues: {
-    //   array: properties
-    // },
     values: {
       array: filterList,
     },
@@ -253,7 +270,11 @@ export default (props: Props) => {
           /\d+/,
           (index) => `array.${parseInt(index)}.pointId`,
         );
-        // const path1 = FormPath.transform(field.path, /\d+/, (index) => `array.${parseInt(index)}.a4`);
+        const path1 = FormPath.transform(
+          field.path,
+          /\d+/,
+          (index) => `array.${parseInt(index)}.codec`,
+        );
         f.setFieldState(path, (state) => {
           if (value) {
             state.required = true;
@@ -263,15 +284,34 @@ export default (props: Props) => {
             form.validate();
           }
         });
-        // f.setFieldState(path1, (state) => {
-        //   if (value) {
-        //     state.required = true;
-        //     form.validate();
-        //   } else {
-        //     state.required = false;
-        //     form.validate();
-        //   }
-        // });
+        f.setFieldState(path1, (state) => {
+          if (value) {
+            state.required = true;
+            form.validate();
+          } else {
+            state.required = false;
+            form.validate();
+          }
+        });
+      });
+      onFieldChange('array.*.codec', (field: any) => {
+        const value = (field as Field).value;
+        const path = FormPath.transform(
+          field.path,
+          /\d+/,
+          (index) => `array.${parseInt(index)}.codecConfiguration.readIndex`,
+        );
+        if ((field as Field).modified) {
+          const readIndex = field.query(path).get('value');
+          const dataLength = field.query(path).get('dataSource')?.length * 2 - 1;
+          const length = lengthMap.get(value) + readIndex;
+          console.log(length, dataLength);
+          if (length > dataLength) {
+            field.selfErrors = '数据类型对应的长度和起始位置加起来不能超过数据长度';
+          } else {
+            field.selfErrors = '';
+          }
+        }
       });
     },
   });
@@ -389,7 +429,7 @@ export default (props: Props) => {
                 title: '数据类型',
               },
               properties: {
-                a4: {
+                codec: {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
@@ -401,16 +441,16 @@ export default (props: Props) => {
                     filterOption: (input: string, option: any) =>
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
-                  enum: [
-                    {
-                      label: '类型1',
-                      value: 'type1',
-                    },
-                    {
-                      label: '类型2',
-                      value: 'type2',
-                    },
-                  ],
+                  enum: typeList,
+                  // 'x-reactions': {
+                  //   dependencies: ['.codecConfiguration.readIndex'],
+                  //   fulfill: {
+                  //     state: {
+                  //       selfErrors:
+                  //         "{{$deps[0]<lengthMap.get($self.value)?'数据类型对应的长度和起始位置加起来不能超过 (数据长度 - 1)的长度':''}}"
+                  //     }
+                  //   }
+                  // }
                 },
               },
             },
@@ -454,13 +494,6 @@ export default (props: Props) => {
             },
           },
         },
-        // properties: {
-        //   add: {
-        //     type: 'void',
-        //     'x-component': 'ArrayTable.Addition',
-        //     title: '添加条目',
-        //   },
-        // },
       },
     },
   };

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

@@ -0,0 +1,121 @@
+import { FormItem, FormLayout, Radio, Select } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { Modal } from 'antd';
+import 'antd/lib/tree-select/style/index.less';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/Channel/Modbus';
+import SystemConst from '@/utils/const';
+import { downloadFile } from '@/utils/util';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+  masterId?: any;
+}
+
+const Export = (props: Props) => {
+  const { visible, close } = props;
+  const [list, setList] = useState<any[]>([]);
+  const SchemaField = createSchemaField({
+    components: {
+      Radio,
+      Select,
+      FormItem,
+      FormLayout,
+    },
+  });
+
+  useEffect(() => {
+    service.queryMaster({ paging: false }).then((resp) => {
+      if (resp.status === 200) {
+        const items = resp.result.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        setList(items);
+      }
+    });
+  }, []);
+
+  const form = createForm();
+
+  const schema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          // labelCol: 4,
+          // wrapperCol: 18,
+          // labelAlign: 'right',
+          layout: 'vertical',
+        },
+        properties: {
+          masterId: {
+            type: 'string',
+            title: '通道',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            enum: [...list],
+            'x-component-props': {
+              allowClear: true,
+              showSearch: true,
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+          },
+          fileType: {
+            title: '文件格式',
+            default: 'xlsx',
+            'x-decorator': 'FormItem',
+            'x-component': 'Radio.Group',
+            'x-component-props': {
+              optionType: 'button',
+              buttonStyle: 'solid',
+            },
+            enum: [
+              {
+                label: 'xlsx',
+                value: 'xlsx',
+              },
+              {
+                label: 'csv',
+                value: 'csv',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+  const downloadTemplate = async () => {
+    const values = (await form.submit()) as any;
+    if (values) {
+      downloadFile(
+        `/${SystemConst.API_BASE}/modbus/point/${values.masterId}/export.${values.fileType}`,
+      );
+      close();
+    }
+  };
+  return (
+    <Modal
+      maskClosable={false}
+      visible={visible}
+      onCancel={() => close()}
+      width="35vw"
+      title="导出"
+      onOk={downloadTemplate}
+    >
+      <div style={{ marginTop: '20px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+    </Modal>
+  );
+};
+export default Export;

+ 30 - 40
src/pages/link/Channel/Modbus/import/index.tsx

@@ -2,7 +2,7 @@ import { FormItem, FormLayout, Select } from '@formily/antd';
 import { createForm } from '@formily/core';
 import { createSchemaField, FormProvider } from '@formily/react';
 import { Badge, Button, Modal, Radio, Space, Upload } from 'antd';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 import { downloadFile, onlyMessage } from '@/utils/util';
@@ -12,7 +12,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
 interface Props {
   visible: boolean;
   close: () => void;
-  data?: any;
+  masterId: any;
 }
 const FileFormat = (props: any) => {
   const [data, setData] = useState<{ autoDeploy: boolean; fileType: 'xlsx' | 'csv' }>({
@@ -46,19 +46,20 @@ const NormalUpload = (props: any) => {
   const [flag, setFlag] = useState<boolean>(true);
   const [count, setCount] = useState<number>(0);
   const [errMessage, setErrMessage] = useState<string>('');
+  const [errorUrl, setErrorUrl] = useState<string>('');
 
   const submitData = async (fileUrl: string) => {
+    setErrorUrl(fileUrl);
     if (!!fileUrl) {
       setCount(0);
       setErrMessage('');
       setFlag(true);
-      const autoDeploy = !!props?.fileType?.autoDeploy || false;
       setImportLoading(true);
       let dt = 0;
       const source = new EventSourcePolyfill(
-        `/${SystemConst.API_BASE}/device/instance/${
-          props.product
-        }/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${Token.get()}`,
+        `/${SystemConst.API_BASE}/modbus/point/${
+          props.masterId
+        }/import?fileUrl=${fileUrl}&:X_Access_Token=${Token.get()}`,
       );
       source.onmessage = (e: any) => {
         const res = JSON.parse(e.data);
@@ -104,9 +105,8 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              const url = `/${SystemConst.API_BASE}/device-instance/${props.product}/template.xlsx`;
+              const url = `/${SystemConst.API_BASE}/modbus/point/template.xlsx`;
               downloadFile(url);
-              // downloadTemplate('xlsx', props.product);
             }}
           >
             .xlsx
@@ -114,9 +114,8 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              const url = `/${SystemConst.API_BASE}/device-instance/${props.product}/template.csv`;
+              const url = `/${SystemConst.API_BASE}/modbus/point/template.csv`;
               downloadFile(url);
-              // downloadTemplate('csv', props.product);
             }}
           >
             .csv
@@ -131,14 +130,29 @@ const NormalUpload = (props: any) => {
             <Badge status="success" text="已完成" />
           )}
           <span style={{ marginLeft: 15 }}>总数量:{count}</span>
-          <p style={{ color: 'red' }}>{errMessage}</p>
+          <div>
+            {errMessage && (
+              <>
+                <Badge status="error" text="失败" />
+                <span style={{ marginLeft: 15 }}>{errMessage}</span>
+                <a href={errorUrl} style={{ marginLeft: 15 }}>
+                  下载
+                </a>
+              </>
+            )}
+          </div>
         </div>
       )}
     </div>
   );
 };
 const Import = (props: Props) => {
-  const { visible, close } = props;
+  const { visible, close, masterId } = props;
+
+  useEffect(() => {
+    console.log(masterId);
+  }, []);
+
   const schema = {
     type: 'object',
     properties: {
@@ -164,6 +178,9 @@ const Import = (props: Props) => {
             // 'x-visible': false,
             'x-decorator': 'FormItem',
             'x-component': 'NormalUpload',
+            'x-component-props': {
+              masterId: masterId,
+            },
           },
         },
       },
@@ -179,34 +196,7 @@ const Import = (props: Props) => {
       NormalUpload,
     },
   });
-  const form = createForm({
-    // effects() {
-    //     onFieldValueChange('product', (field) => {
-    //         form.setFieldState('*(fileType, upload)', (state) => {
-    //             state.visible = !!field.value;
-    //         });
-    //         form.setFieldState('*(upload)', (state) => {
-    //             state.componentProps = {
-    //                 product: field.value,
-    //             };
-    //         });
-    //     });
-    //     onFieldValueChange('fileType', (field) => {
-    //         const product = form.getValuesIn('product') || '';
-    //         form.setFieldState('*(upload)', (state) => {
-    //             state.componentProps = {
-    //                 fileType: field.value,
-    //                 product,
-    //             };
-    //         });
-    //     });
-    //     onFieldValueChange('upload', (field) => {
-    //         if (!field.value) {
-    //             close();
-    //         }
-    //     });
-    // },
-  });
+  const form = createForm({});
   return (
     <Modal
       maskClosable={false}

+ 53 - 26
src/pages/link/Channel/Modbus/index.tsx

@@ -23,6 +23,9 @@ import SaveChannel from './saveChannel';
 import SavePoint from './savePoint';
 import Import from './import';
 import { onlyMessage } from '@/utils/util';
+import Export from './Export';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { map } from 'rxjs/operators';
 
 export const service = new Service('');
 
@@ -35,12 +38,17 @@ const NewModbus = () => {
   const [activeKey, setActiveKey] = useState<any>('');
   const [visible, setVisible] = useState<boolean>(false);
   const [visiblePoint, setVisiblePoint] = useState<boolean>(false);
+  const [exportVisible, setExportVisible] = useState<boolean>(false);
   const [current, setCurrent] = useState<any>({});
   const [pointDetail, setPointDetail] = useState<any>({});
   const [importVisible, setImportVisible] = useState<boolean>(false);
   const [masterList, setMasterList] = useState<any>([]);
   const [filterList, setFilterList] = useState([]);
   const masterId = useRef<string>('');
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const wsRef = useRef<any>();
+  const [pointList, setPointList] = useState<any>([]);
+  const [currentData, setCurrentData] = useState<any>({});
 
   const collectMap = new Map();
   collectMap.set('running', 'success');
@@ -51,14 +59,14 @@ const NewModbus = () => {
     <Menu>
       <Menu.Item key="1">
         <PermissionButton
-          isPermission={permission.export}
+          isPermission={permission.export || true}
           icon={<ExportOutlined />}
           type="default"
           onClick={() => {
-            // setExportVisible(true);
+            setExportVisible(true);
           }}
         >
-          批量导出设备
+          批量导出点位
         </PermissionButton>
       </Menu.Item>
       <Menu.Item key="2">
@@ -69,7 +77,7 @@ const NewModbus = () => {
             setImportVisible(true);
           }}
         >
-          批量导入设备
+          批量导入点位
         </PermissionButton>
       </Menu.Item>
     </Menu>
@@ -108,20 +116,18 @@ const NewModbus = () => {
       render: (record: any) => (
         <a
           onClick={() => {
-            console.log(record.number);
             Modal.info({
               title: '当前数据',
               content: (
                 <div>
                   <div>寄存器1:{record.number}</div>
-                  <div>寄存器2:{record.number}</div>
                 </div>
               ),
               onOk() {},
             });
           }}
         >
-          {record.number}
+          {currentData[record?.id] || '-'}
         </a>
       ),
     },
@@ -138,16 +144,18 @@ const NewModbus = () => {
                 status={collectMap.get(record.collectState?.value)}
                 text={record.collectState?.text}
               />
-              <SearchOutlined
-                style={{ color: '#1d39c4', marginLeft: 3 }}
-                onClick={() => {
-                  Modal.error({
-                    title: '失败原因',
-                    content: <div>111111</div>,
-                    onOk() {},
-                  });
-                }}
-              />
+              {record.collectState?.value === 'error' && (
+                <SearchOutlined
+                  style={{ color: '#1d39c4', marginLeft: 3 }}
+                  onClick={() => {
+                    Modal.error({
+                      title: '失败原因',
+                      content: <div>111111</div>,
+                      onOk() {},
+                    });
+                  }}
+                />
+              )}
             </>
           )}
         </>
@@ -286,8 +294,8 @@ const NewModbus = () => {
         if (res.status === 200) {
           setMasterList(res.result);
           setFilterList(res.result);
-          setActiveKey(res.result?.[0].id);
-          masterId.current = res.result?.[0].id;
+          setActiveKey(res.result?.[0]?.id);
+          masterId.current = res.result?.[0]?.id;
           console.log(masterId.current);
         }
       });
@@ -315,13 +323,28 @@ const NewModbus = () => {
   useEffect(() => {
     masterId.current = activeKey;
     actionRef.current?.reload();
-    // console.log(activeKey)
   }, [activeKey]);
 
   useEffect(() => {
     getMaster();
   }, []);
 
+  useEffect(() => {
+    const id = `collector-data-modbus`;
+    const topic = `/collector/MODBUS_TCP/${activeKey}/data`;
+    wsRef.current = subscribeTopic?.(id, topic, {
+      pointId: pointList.map((item: any) => item.id),
+    })
+      ?.pipe(map((res: any) => res.payload))
+      .subscribe((payload: any) => {
+        const { pointId, nativeData } = payload;
+        current[pointId] = nativeData;
+        setCurrentData({ ...current });
+        console.log(current);
+      });
+    return () => wsRef.current && wsRef.current?.unsubscribe();
+  }, [pointList]);
+
   return (
     <PageContainer>
       <Card className="modbus" style={{ minHeight }}>
@@ -480,6 +503,7 @@ const NewModbus = () => {
                     ...params,
                     sorts: [{ name: 'createTime', order: 'desc' }],
                   });
+                  setPointList(res.result.data);
                   return {
                     code: res.message,
                     result: {
@@ -503,11 +527,6 @@ const NewModbus = () => {
                   };
                 }
               }}
-              // request={async (params) =>
-              //   service.queryPoint(masterId.current,{
-              // ...params,
-              // sorts: [{ name: 'createTime', order: 'desc' }] })
-              // }
             />
           </div>
         </div>
@@ -533,13 +552,21 @@ const NewModbus = () => {
         />
       )}
       <Import
-        data={current}
+        masterId={activeKey}
         close={() => {
           setImportVisible(false);
           actionRef.current?.reload();
         }}
         visible={importVisible}
       />
+      <Export
+        masterId={activeKey}
+        close={() => {
+          setExportVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={exportVisible}
+      />
     </PageContainer>
   );
 };

+ 23 - 3
src/pages/link/Channel/Modbus/service.ts

@@ -48,23 +48,43 @@ class Service extends BaseService<any> {
       data,
     });
   //保存设备绑定点位映射配置
+  // saveDevicePoint = (deviceId: string, data: any) =>
+  //   request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collector/modbus`, {
+  //     method: 'PATCH',
+  //     data,
+  //   });
   saveDevicePoint = (deviceId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collector/modbus`, {
+    request(`/${SystemConst.API_BASE}/things/collector/device/${deviceId}/MODBUS_TCP`, {
       method: 'PATCH',
       data,
     });
   //查询设备点位映射配置
+  // getDevicePoint = (deviceId: string, param?: string) =>
+  //   request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collector/_query`, {
+  //     method: 'GET',
+  //     param,
+  //   });
   getDevicePoint = (deviceId: string, param?: string) =>
-    request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collector/_query`, {
+    request(`/${SystemConst.API_BASE}/things/collector/device/${deviceId}/_query`, {
       method: 'GET',
       param,
     });
   //设备解绑点位映射配置
+  // removeDevicePoint = (deviceId: string, data: any) =>
+  //   request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collectors/_delete`, {
+  //     method: 'POST',
+  //     data,
+  //   });
   removeDevicePoint = (deviceId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/collectors/_delete`, {
+    request(`/${SystemConst.API_BASE}/things/collector/device/${deviceId}/_delete`, {
       method: 'POST',
       data,
     });
+  //modbus数据类型
+  dataType = () =>
+    request(`/${SystemConst.API_BASE}/things/collector/codecs`, {
+      method: 'GET',
+    });
 }
 
 export default Service;

+ 225 - 125
src/pages/link/Channel/Opcua/index.tsx

@@ -14,60 +14,66 @@ import {
   SearchOutlined,
   StopOutlined,
 } from '@ant-design/icons';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { useIntl } from 'umi';
 import ChannelCard from '../channelCard';
 import { PageContainer } from '@ant-design/pro-layout';
 import Service from './service';
 import SaveChannel from './saveChannel';
 import SavePoint from './savePoint';
+// import Import from './import';
+import { onlyMessage } from '@/utils/util';
 
-export const service = new Service('');
+export const service = new Service('opc/client');
 
 const NewModbus = () => {
-  const { minHeight } = useDomFullHeight(`.opc`);
+  const { minHeight } = useDomFullHeight(`.modbus`);
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
-  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+  const { permission } = PermissionButton.usePermission('link/Channel/Modbus');
   const [param, setParam] = useState({});
   const [activeKey, setActiveKey] = useState<any>('');
   const [visible, setVisible] = useState<boolean>(false);
   const [visiblePoint, setVisiblePoint] = useState<boolean>(false);
   const [current, setCurrent] = useState<any>({});
   const [pointDetail, setPointDetail] = useState<any>({});
-  const data = [
-    {
-      id: 1,
-      status: 'connect',
-      state: {
-        text: '正常',
-        value: 'enabled',
-      },
-    },
-    {
-      id: 2,
-      status: 'disconnect',
-      state: {
-        text: '禁用',
-        value: 'disabled',
-      },
-    },
-  ];
-  const dataSoure = [
-    {
-      id: 1,
-      name: '111',
-      number: '0xF831',
-      collect: {
-        text: '采集失败',
-        value: 'collectError',
-      },
-      state: {
-        text: '禁用',
-        value: 'disabled',
-      },
-    },
-  ];
+  // const [importVisible, setImportVisible] = useState<boolean>(false);
+  const [masterList, setMasterList] = useState<any>([]);
+  const [filterList, setFilterList] = useState([]);
+  const masterId = useRef<string>('');
+
+  const collectMap = new Map();
+  collectMap.set('running', 'success');
+  collectMap.set('error', 'error');
+  collectMap.set('stopped', 'warning');
+
+  const menu = (
+    <Menu>
+      <Menu.Item key="1">
+        <PermissionButton
+          isPermission={permission.export}
+          icon={<ExportOutlined />}
+          type="default"
+          onClick={() => {
+            // setExportVisible(true);
+          }}
+        >
+          批量导出设备
+        </PermissionButton>
+      </Menu.Item>
+      <Menu.Item key="2">
+        <PermissionButton
+          isPermission={permission.import || true}
+          icon={<ImportOutlined />}
+          onClick={() => {
+            // setImportVisible(true);
+          }}
+        >
+          批量导入设备
+        </PermissionButton>
+      </Menu.Item>
+    </Menu>
+  );
 
   const columns: ProColumns<any>[] = [
     {
@@ -78,38 +84,44 @@ const NewModbus = () => {
       fixed: 'left',
     },
     {
-      title: '点位ID',
-      dataIndex: 'host',
+      title: '点位Id',
+      render: (record: any) => <>{record.function?.text}</>,
     },
     {
-      title: '数据模式',
-      dataIndex: 'port',
+      title: '数据类型',
+      dataIndex: 'unitId',
       search: false,
-      valueType: 'digit',
     },
     {
       title: '当前数据',
-      dataIndex: 'port',
       search: false,
-      valueType: 'digit',
+      render: (record: any) => <>{record.parameter?.quantity}</>,
     },
     {
       title: '采集状态',
-      // dataIndex: 'collect',
       search: false,
       render: (record: any) => (
         <>
-          {record.collect.text}
-          <SearchOutlined
-            style={{ color: '#1d39c4', marginLeft: 3 }}
-            onClick={() => {
-              Modal.error({
-                title: '失败原因',
-                content: <div>111111</div>,
-                onOk() {},
-              });
-            }}
-          />
+          {record.state.value === 'disabled' ? (
+            '-'
+          ) : (
+            <>
+              <Badge
+                status={collectMap.get(record.collectState?.value)}
+                text={record.collectState?.text}
+              />
+              <SearchOutlined
+                style={{ color: '#1d39c4', marginLeft: 3 }}
+                onClick={() => {
+                  Modal.error({
+                    title: '失败原因',
+                    content: <div>111111</div>,
+                    onOk() {},
+                  });
+                }}
+              />
+            </>
+          )}
         </>
       ),
     },
@@ -146,8 +158,8 @@ const NewModbus = () => {
           isPermission={permission.update}
           key="edit"
           onClick={() => {
-            // setVisible(true);
-            // setCurrent(record);
+            setPointDetail(record);
+            setVisiblePoint(true);
           }}
           type={'link'}
           style={{ padding: 0 }}
@@ -172,24 +184,24 @@ const NewModbus = () => {
               defaultMessage: '确认禁用?',
             }),
             onConfirm: async () => {
-              //   if (record.state.value === 'disabled') {
-              //     await service.edit({
-              //       ...record,
-              //       state: 'enabled',
-              //     });
-              //   } else {
-              //     await service.edit({
-              //       ...record,
-              //       state: 'disabled',
-              //     });
-              //   }
-              //   onlyMessage(
-              //     intl.formatMessage({
-              //       id: 'pages.data.option.success',
-              //       defaultMessage: '操作成功!',
-              //     }),
-              //   );
-              //   actionRef.current?.reload();
+              if (record.state.value === 'disabled') {
+                await service.editPoint(record.id, {
+                  ...record,
+                  state: 'enabled',
+                });
+              } else {
+                await service.editPoint(record.id, {
+                  ...record,
+                  state: 'disabled',
+                });
+              }
+              onlyMessage(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
             },
           }}
           isPermission={permission.action}
@@ -210,16 +222,16 @@ const NewModbus = () => {
             title: '确认删除',
             disabled: record.state.value === 'enabled',
             onConfirm: async () => {
-              //   const resp: any = await service.remove(record.id);
-              //   if (resp.status === 200) {
-              //     onlyMessage(
-              //       intl.formatMessage({
-              //         id: 'pages.data.option.success',
-              //         defaultMessage: '操作成功!',
-              //       }),
-              //     );
-              //     actionRef.current?.reload();
-              //   }
+              const resp: any = await service.deletePoint(record.id);
+              if (resp.status === 200) {
+                onlyMessage(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
             },
           }}
           key="delete"
@@ -231,33 +243,64 @@ const NewModbus = () => {
     },
   ];
 
-  const menu = (
-    <Menu>
-      <Menu.Item key="1">
-        <PermissionButton
-          isPermission={permission.export}
-          icon={<ExportOutlined />}
-          type="default"
-          onClick={() => {
-            // setExportVisible(true);
-          }}
-        >
-          批量导出设备
-        </PermissionButton>
-      </Menu.Item>
-      <Menu.Item key="2">
-        <PermissionButton
-          isPermission={permission.import}
-          icon={<ImportOutlined />}
-          onClick={() => {
-            // setImportVisible(true);
-          }}
-        >
-          批量导入设备
-        </PermissionButton>
-      </Menu.Item>
-    </Menu>
-  );
+  const getOpc = () => {
+    service
+      .noPagingOpcua({
+        paging: false,
+        sorts: [
+          {
+            name: 'createTime',
+            order: 'desc',
+          },
+        ],
+      })
+      .then((res: any) => {
+        if (res.status === 200) {
+          setMasterList(res.result);
+          setFilterList(res.result);
+          setActiveKey(res.result?.[0]?.id);
+          masterId.current = res.result?.[0]?.id;
+          console.log(masterId.current);
+        }
+      });
+  };
+
+  //启用
+  const _start = (id: string) => {
+    service.enable(id).then((res) => {
+      if (res.status === 200) {
+        onlyMessage('操作成功');
+        getOpc();
+      }
+    });
+  };
+  //禁用
+  const _stop = (id: string) => {
+    service.disable(id).then((res) => {
+      if (res.status === 200) {
+        onlyMessage('操作成功');
+        getOpc();
+      }
+    });
+  };
+
+  const removeOpc = (id: string) => {
+    service.remove(id).then((res: any) => {
+      if (res.status === 200) {
+        onlyMessage('删除成功');
+        getOpc();
+      }
+    });
+  };
+
+  useEffect(() => {
+    masterId.current = activeKey;
+    actionRef.current?.reload();
+  }, [activeKey]);
+
+  useEffect(() => {
+    getOpc();
+  }, []);
 
   return (
     <PageContainer>
@@ -269,7 +312,14 @@ const NewModbus = () => {
                 placeholder="请输入名称"
                 allowClear
                 onSearch={(value) => {
-                  console.log(value);
+                  const items = masterList.filter((item: any) => item.name.match(value));
+                  if (value) {
+                    setFilterList(items);
+                    setActiveKey(items?.[0].id);
+                  } else {
+                    setFilterList(masterList);
+                    setActiveKey(masterList?.[0].id);
+                  }
                 }}
               />
               <PermissionButton
@@ -281,12 +331,12 @@ const NewModbus = () => {
                 key="add"
                 icon={<PlusOutlined />}
                 type="default"
-                style={{ width: '100%', marginTop: 16 }}
+                style={{ width: '100%', marginTop: 16, marginBottom: 16 }}
               >
                 新增
               </PermissionButton>
               <div className="item-left-list">
-                {data.map((item) => (
+                {filterList.map((item: any) => (
                   <ChannelCard
                     active={activeKey === item.id}
                     data={item}
@@ -299,8 +349,8 @@ const NewModbus = () => {
                           isPermission={permission.update}
                           key="edit"
                           onClick={() => {
-                            // setVisible(true);
-                            // setCurrent(record);
+                            setVisible(true);
+                            setCurrent(item);
                           }}
                           type={'link'}
                           style={{ padding: 0 }}
@@ -321,7 +371,13 @@ const NewModbus = () => {
                               }.tips`,
                               defaultMessage: '确认禁用?',
                             }),
-                            onConfirm: async () => {},
+                            onConfirm: async () => {
+                              if (item.state.value === 'disabled') {
+                                _start(item.id);
+                              } else {
+                                _stop(item.id);
+                              }
+                            },
                           }}
                         >
                           {item.state.value === 'enabled' ? (
@@ -336,10 +392,15 @@ const NewModbus = () => {
                           isPermission={permission.delete}
                           style={{ padding: 0 }}
                           disabled={item.state.value === 'enabled'}
+                          tooltip={{
+                            title: item.state.value === 'enabled' ? '请先禁用该通道,再删除。' : '',
+                          }}
                           popConfirm={{
                             title: '确认删除',
                             disabled: item.state.value === 'enabled',
-                            onConfirm: async () => {},
+                            onConfirm: async () => {
+                              removeOpc(item.id);
+                            },
                           }}
                           key="delete"
                           type="link"
@@ -367,7 +428,7 @@ const NewModbus = () => {
               params={param}
               columns={columns}
               rowKey="id"
-              dataSource={dataSoure}
+              // dataSource={dataSoure}
               // scroll={{ x: 1000 }}
               search={false}
               headerTitle={
@@ -377,8 +438,7 @@ const NewModbus = () => {
                       setPointDetail({});
                       setVisiblePoint(true);
                     }}
-                    // isPermission={permission.add}
-                    isPermission={permission.add || true}
+                    isPermission={permission.add}
                     key="add"
                     icon={<PlusOutlined />}
                     type="primary"
@@ -394,8 +454,39 @@ const NewModbus = () => {
                   </Dropdown>
                 </>
               }
+              // request={async (params) => {
+              //   if (masterId.current) {
+              //     const res = await service.queryPoint(masterId.current, {
+              //       ...params,
+              //       sorts: [{ name: 'createTime', order: 'desc' }],
+              //     });
+              //     return {
+              //       code: res.message,
+              //       result: {
+              //         data: res.result.data,
+              //         pageIndex: res.result.pageIndex,
+              //         pageSize: res.result.pageSize,
+              //         total: res.result.total,
+              //       },
+              //       status: res.status,
+              //     };
+              //   } else {
+              //     return {
+              //       code: 200,
+              //       result: {
+              //         data: [],
+              //         pageIndex: 0,
+              //         pageSize: 0,
+              //         total: 0,
+              //       },
+              //       status: 200,
+              //     };
+              //   }
+              // }}
               // request={async (params) =>
-              //     service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+              //   service.queryPoint(masterId.current,{
+              // ...params,
+              // sorts: [{ name: 'createTime', order: 'desc' }] })
               // }
             />
           </div>
@@ -406,19 +497,28 @@ const NewModbus = () => {
           data={current}
           close={() => {
             setVisible(false);
-            actionRef.current?.reload();
+            getOpc();
           }}
         />
       )}
       {visiblePoint && (
         <SavePoint
           data={pointDetail}
+          opcId={activeKey}
           close={() => {
             setVisiblePoint(false);
             actionRef.current?.reload();
           }}
         />
       )}
+      {/* <Import
+        data={current}
+        close={() => {
+          setImportVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={importVisible}
+      /> */}
     </PageContainer>
   );
 };

+ 44 - 19
src/pages/link/Channel/Opcua/saveChannel.tsx

@@ -1,24 +1,40 @@
-import { createForm } from '@formily/core';
+import { createForm, Field } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import { Form, FormGrid, FormItem, Input, NumberPicker, Select } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
-// import { service } from '@/pages/link/Channel/Modbus';
+import { service } from '@/pages/link/Channel/Opcua';
 import { Modal } from '@/components';
-import { useEffect } from 'react';
-// import { onlyMessage } from '@/utils/util';
+import { onlyMessage } from '@/utils/util';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
 
 interface Props {
   data: any;
   close: () => void;
-  device?: any;
 }
 
 const SaveChannel = (props: Props) => {
   const form = createForm({
-    validateFirst: true,
-    initialValues: props.data,
+    initialValues: {
+      ...props.data,
+    },
   });
 
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field).then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
+          label: item,
+          value: item,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const getPolicies = () => service.policies();
+  const getModes = () => service.modes();
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -64,7 +80,7 @@ const SaveChannel = (props: Props) => {
               },
             ],
           },
-          'clientConfigs.endpoint': {
+          'configuration.endpoint': {
             title: '服务地址',
             'x-decorator-props': {
               gridSpan: 2,
@@ -93,7 +109,7 @@ const SaveChannel = (props: Props) => {
             name: 'endpoint',
             required: true,
           },
-          'clientConfigs.securityPolicy': {
+          'configuration.securityPolicy': {
             title: '安全策略',
             'x-decorator-props': {
               gridSpan: 2,
@@ -114,9 +130,9 @@ const SaveChannel = (props: Props) => {
                 message: '请选择安全策略',
               },
             ],
-            // 'x-reactions': ['{{useAsyncDataSource(getPolicies)}}'],
+            'x-reactions': ['{{useAsyncDataSource(getPolicies)}}'],
           },
-          'clientConfigs.securityMode': {
+          'configuration.securityMode': {
             title: '安全模式',
             'x-decorator-props': {
               gridSpan: 2,
@@ -134,9 +150,9 @@ const SaveChannel = (props: Props) => {
               },
             ],
             required: true,
-            // 'x-reactions': ['{{useAsyncDataSource(getModes)}}'],
+            'x-reactions': ['{{useAsyncDataSource(getModes)}}'],
           },
-          'clientConfigs.username': {
+          'configuration.username': {
             title: '用户名',
             type: 'string',
             'x-decorator': 'FormItem',
@@ -155,7 +171,7 @@ const SaveChannel = (props: Props) => {
               },
             ],
           },
-          'clientConfigs.password': {
+          'configuration.password': {
             title: '密码',
             type: 'string',
             'x-decorator': 'FormItem',
@@ -199,12 +215,21 @@ const SaveChannel = (props: Props) => {
 
   const save = async () => {
     const value = await form.submit<any>();
-    console.log(value);
+    if (props.data.id) {
+      const res = await service.editOpc(props.data.id, value);
+      if (res.status === 200) {
+        onlyMessage('保存成功');
+        props.close();
+      }
+    } else {
+      const res = await service.saveOpc(value);
+      if (res.status === 200) {
+        onlyMessage('保存成功');
+        props.close();
+      }
+    }
   };
 
-  useEffect(() => {
-    console.log(props.data.id);
-  }, []);
   return (
     <Modal
       title={props.data.id ? '编辑通道' : '新增通道'}
@@ -217,7 +242,7 @@ const SaveChannel = (props: Props) => {
       permission={['add', 'edit']}
     >
       <Form form={form} layout="vertical">
-        <SchemaField schema={schema} />
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getPolicies, getModes }} />
       </Form>
     </Modal>
   );

+ 6 - 4
src/pages/link/Channel/Opcua/savePoint.tsx

@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
 interface Props {
   data: any;
   close: Function;
+  opcId: string;
 }
 
 const SavePoint = (props: Props) => {
@@ -61,16 +62,17 @@ const SavePoint = (props: Props) => {
           <Col span={24}>
             <Form.Item
               label="点位ID"
-              name="opcID"
+              name="opcPointId"
               required
               rules={[
-                { required: true, message: '点位ID' },
+                { required: true, message: '点位ID必填' },
                 ({}) => ({
                   validator(_, value) {
-                    if (value !== 0 || /(^[1-9]\d*$)/.test(value)) {
+                    const item = value.substring(0, 2);
+                    if (item === 'i=' || item === 's=' || item === 'g=' || item === 'b=') {
                       return Promise.resolve();
                     }
-                    return Promise.reject(new Error('请输入非0正整数'));
+                    return Promise.reject(new Error('前两个字符必须为i=、s=、g=、b=中的一个'));
                   },
                 }),
               ]}

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

@@ -3,6 +3,16 @@ import { request } from 'umi';
 import SystemConst from '@/utils/const';
 
 class Service extends BaseService<OpaUa> {
+  saveOpc = (data: any) =>
+    request(`${SystemConst.API_BASE}/opc/client`, {
+      method: 'POST',
+      data,
+    });
+  editOpc = (id: string, data: any) =>
+    request(`${SystemConst.API_BASE}/opc/client/${id}`, {
+      method: 'PUT',
+      data,
+    });
   enable = (id: string) =>
     request(`${SystemConst.API_BASE}/opc/client/${id}/_enable`, {
       method: 'POST',