Prechádzať zdrojové kódy

fix: 修改数据采集

100011797 3 rokov pred
rodič
commit
0721eae0bf

+ 7 - 1
src/pages/link/DataCollect/Dashboard/index.tsx

@@ -90,16 +90,19 @@ const DeviceBoard = () => {
         return {
           limit: 24,
           interval: '1h',
+          format: 'HH:mm',
         };
       case 'week':
         return {
           limit: 7,
           interval: '1d',
+          format: 'MM-dd',
         };
       case 'hour':
         return {
           limit: 60,
           interval: '1m',
+          format: 'HH:mm',
         };
       default:
         const time = dt.end - dt.start;
@@ -109,16 +112,19 @@ const DeviceBoard = () => {
           return {
             limit: Math.abs(Math.ceil(time / (60 * 60))),
             interval: '1m',
+            format: 'HH:mm',
           };
         } else if (time > hour && time <= days) {
           return {
             limit: Math.abs(Math.ceil(time / hour)),
             interval: '1h',
+            format: 'HH:mm',
           };
         } else {
           return {
             limit: Math.abs(Math.ceil(dt / days)) + 1,
             interval: '1d',
+            format: 'MM-dd',
           };
         }
     }
@@ -137,7 +143,7 @@ const DeviceBoard = () => {
           from: data.time.start,
           to: data.time.end,
           interval: getParams(data.time).interval,
-          format: 'HH:mm',
+          format: getParams(data.time).format,
         },
       },
     ]);

+ 8 - 4
src/pages/link/DataCollect/components/Point/CollectorCard/WritePoint.tsx

@@ -1,7 +1,7 @@
 import { Modal } from 'antd';
-import { FormItem, Input } from '@formily/antd';
+import { FormItem, Input, Form } from '@formily/antd';
 import { createForm } from '@formily/core';
-import { createSchemaField, FormProvider } from '@formily/react';
+import { createSchemaField } from '@formily/react';
 import service from '../../../service';
 import { onlyMessage } from '@/utils/util';
 
@@ -17,6 +17,7 @@ const WritePoint = (props: Props) => {
     components: {
       Input,
       FormItem,
+      Form,
     },
   });
 
@@ -30,6 +31,9 @@ const WritePoint = (props: Props) => {
         required: true,
         'x-decorator': 'FormItem',
         'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入',
+        },
       },
     },
   };
@@ -64,9 +68,9 @@ const WritePoint = (props: Props) => {
       }}
     >
       <div style={{ marginTop: '30px' }}>
-        <FormProvider form={form}>
+        <Form form={form} layout="vertical">
           <SchemaField schema={schema} />
-        </FormProvider>
+        </Form>
       </div>
     </Modal>
   );

+ 37 - 0
src/pages/link/DataCollect/components/Point/CollectorCard/index.less

@@ -1,8 +1,18 @@
+@import '~antd/es/style/themes/default.less';
+
 .card-item {
   display: flex;
   width: 100%;
   overflow: hidden;
   border: 1px solid #e6e6e6;
+  &.active {
+    position: relative;
+    border: 1px solid #2f54eb;
+
+    .checked-icon {
+      display: block;
+    }
+  }
   .card-item-left {
     position: relative;
     display: flex;
@@ -113,4 +123,31 @@
       }
     }
   }
+
+  .checked-icon {
+    position: absolute;
+    right: -22px;
+    bottom: -22px;
+    z-index: 2;
+    display: none;
+    width: 44px;
+    height: 44px;
+    color: #fff;
+    background-color: red;
+    background-color: @primary-color-active;
+    transform: rotate(-45deg);
+
+    > div {
+      position: relative;
+      height: 100%;
+      transform: rotate(45deg);
+
+      > span {
+        position: absolute;
+        top: 6px;
+        left: 6px;
+        font-size: 12px;
+      }
+    }
+  }
 }

+ 70 - 68
src/pages/link/DataCollect/components/Point/CollectorCard/index.tsx

@@ -2,19 +2,24 @@ import { useEffect, useState } from 'react';
 import { Ellipsis, PermissionButton } from '@/components';
 import './index.less';
 import { Badge, Popconfirm, Spin, Tooltip } from 'antd';
-import { DeleteOutlined, EditOutlined, FormOutlined, RedoOutlined } from '@ant-design/icons';
-// import OpcSave from '../Save/opc-ua';
-// import ModbusSave from '../Save/modbus';
+import {
+  CheckOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  FormOutlined,
+  RedoOutlined,
+} from '@ant-design/icons';
 import service from '@/pages/link/DataCollect/service';
 import { onlyMessage } from '@/utils/util';
 import moment from 'moment';
-// import WritePoint from '@/pages/link/DataCollect/components/Point/CollectorCard/WritePoint';
+import classNames from 'classnames';
 
 export interface PointCardProps {
   item: Partial<PointItem>;
   reload: () => void;
   wsValue: any;
   update: (item: any, type?: boolean) => void;
+  activeStyle?: string;
 }
 
 const opcImage = require('/public/images/DataCollect/device-opcua.png');
@@ -22,9 +27,7 @@ const modbusImage = require('/public/images/DataCollect/device-modbus.png');
 
 const CollectorCard = (props: PointCardProps) => {
   const { item, wsValue } = props;
-  // const [editVisible, setEditVisible] = useState<boolean>(false);
   const [spinning, setSpinning] = useState<boolean>(false);
-  // const [writeVisible, setWriteVisible] = useState<boolean>(false);
   const { permission } = PermissionButton.usePermission('link/DataCollect/DataGathering');
   const [dataValue, setDataValue] = useState<any>(wsValue);
 
@@ -42,37 +45,9 @@ const CollectorCard = (props: PointCardProps) => {
       setSpinning(false);
     }
   };
-
-  // const saveComponent = () => {
-  //   if (item.provider === 'OPC_UA') {
-  //     return (
-  //       <OpcSave
-  //         close={() => {
-  //           setEditVisible(false);
-  //         }}
-  //         reload={() => {
-  //           setEditVisible(false);
-  //         }}
-  //         data={item}
-  //       />
-  //     );
-  //   }
-  //   return (
-  //     <ModbusSave
-  //       close={() => {
-  //         setEditVisible(false);
-  //       }}
-  //       collector={{}}
-  //       reload={() => {
-  //         setEditVisible(false);
-  //       }}
-  //       data={item}
-  //     />
-  //   );
-  // };
   return (
     <Spin spinning={spinning}>
-      <div className={'card-item'}>
+      <div className={classNames('card-item', props.activeStyle)}>
         <div className={'card-item-left'}>
           <div className={'card-item-status'}>
             <div className={'card-item-status-content'}>
@@ -101,7 +76,11 @@ const CollectorCard = (props: PointCardProps) => {
                 <Popconfirm
                   title={'确认删除'}
                   disabled={!permission.delete}
-                  onConfirm={async () => {
+                  onCancel={(e) => {
+                    e?.stopPropagation();
+                  }}
+                  onConfirm={async (e) => {
+                    e?.stopPropagation();
                     if (item.id) {
                       const resp = await service.removePoint(item.id);
                       if (resp.status === 200) {
@@ -112,14 +91,19 @@ const CollectorCard = (props: PointCardProps) => {
                   }}
                 >
                   <Tooltip title={!permission.delete ? '暂无权限,请联系管理员' : ''}>
-                    <DeleteOutlined style={{ marginRight: 10 }} />
+                    <DeleteOutlined
+                      style={{ marginRight: 10 }}
+                      onClick={(e) => {
+                        e?.stopPropagation();
+                      }}
+                    />
                   </Tooltip>
                 </Popconfirm>
                 <Tooltip title={!permission.update ? '暂无权限,请联系管理员' : ''}>
                   <FormOutlined
-                    onClick={() => {
+                    onClick={(e) => {
+                      e?.stopPropagation();
                       if (permission.update) {
-                        // setEditVisible(true);
                         props.update(item);
                       }
                     }}
@@ -135,18 +119,26 @@ const CollectorCard = (props: PointCardProps) => {
                       <Ellipsis title={`${dataValue?.parseData}(${dataValue?.dataType})`} />
                     </div>
                     <div className={'card-item-content-item-header-action'}>
-                      <EditOutlined
-                        style={{ marginRight: 5 }}
-                        onClick={() => {
-                          props.update(item, true);
-                          // setWriteVisible(true);
-                        }}
-                      />
-                      <RedoOutlined
-                        onClick={() => {
-                          read();
-                        }}
-                      />
+                      {item.accessModes?.length === 1 &&
+                        item.accessModes.map((i) => i.value)?.includes('write') && (
+                          <EditOutlined
+                            style={{ marginLeft: 15 }}
+                            onClick={(e) => {
+                              e?.stopPropagation();
+                              props.update(item, true);
+                            }}
+                          />
+                        )}
+                      {item.accessModes?.length === 1 &&
+                        item.accessModes.map((i) => i.value)?.includes('read') && (
+                          <RedoOutlined
+                            style={{ marginLeft: 15 }}
+                            onClick={(e) => {
+                              e?.stopPropagation();
+                              read();
+                            }}
+                          />
+                        )}
                     </div>
                   </div>
                   <div className={'card-item-content-item-text'}>
@@ -168,14 +160,28 @@ const CollectorCard = (props: PointCardProps) => {
                     <span className={'action'} style={{ fontWeight: 600, color: '#000' }}>
                       --
                     </span>
-                    <EditOutlined
-                      className={'action'}
-                      style={{ margin: '0 15px' }}
-                      onClick={() => {
-                        props.update(item, true);
-                      }}
-                    />
-                    <RedoOutlined className={'action'} onClick={read} />
+                    {item.accessModes?.length === 1 &&
+                      item.accessModes.map((i) => i.value)?.includes('write') && (
+                        <EditOutlined
+                          className={'action'}
+                          style={{ marginLeft: 15 }}
+                          onClick={(e) => {
+                            e?.stopPropagation();
+                            props.update(item, true);
+                          }}
+                        />
+                      )}
+                    {item.accessModes?.length === 1 &&
+                      item.accessModes.map((i) => i.value)?.includes('read') && (
+                        <RedoOutlined
+                          style={{ marginLeft: 15 }}
+                          className={'action'}
+                          onClick={(e) => {
+                            e?.stopPropagation();
+                            read();
+                          }}
+                        />
+                      )}
                   </div>
                 </div>
               )}
@@ -213,15 +219,11 @@ const CollectorCard = (props: PointCardProps) => {
             </div>
           </div>
         </div>
-        {/*{editVisible && saveComponent()}*/}
-        {/*{writeVisible && (*/}
-        {/*  <WritePoint*/}
-        {/*    data={item}*/}
-        {/*    onCancel={() => {*/}
-        {/*      setWriteVisible(false);*/}
-        {/*    }}*/}
-        {/*  />*/}
-        {/*)}*/}
+        <div className={'checked-icon'}>
+          <div>
+            <CheckOutlined />
+          </div>
+        </div>
       </div>
     </Spin>
   );

+ 221 - 0
src/pages/link/DataCollect/components/Point/Save/BatchUpdate-copy.tsx

@@ -0,0 +1,221 @@
+import { Button, Modal } from 'antd';
+import { FormItem, Input, ArrayTable, Editable, NumberPicker } from '@formily/antd';
+import MyInput from './components/MyInput';
+import MySelect from './components/MySelect';
+import { createForm, registerValidateRules } from '@formily/core';
+import { FormProvider, createSchemaField } from '@formily/react';
+import { useEffect } from 'react';
+
+interface Props {
+  data?: any[];
+  close: () => void;
+  reload: () => void;
+}
+
+export default (props: Props) => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      Input,
+      ArrayTable,
+      NumberPicker,
+      MyInput,
+      MySelect,
+    },
+  });
+
+  const form = createForm({
+    initialValues: { array: [] },
+  });
+
+  registerValidateRules({
+    checkLength(value) {
+      if (String(value).length > 64) {
+        return {
+          type: 'error',
+          message: '最多可输入64个字符',
+        };
+      }
+      if (!(value % 1 === 0)) {
+        return {
+          type: 'error',
+          message: '请输入非0正整数',
+        };
+      }
+      return '';
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      array: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: { pageSize: 10 },
+          scroll: { x: '100%' },
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '名称' },
+              properties: {
+                name: {
+                  type: 'string',
+                  'x-component': 'Input',
+                  'x-component-props': {
+                    placeholder: '请输入点位名称',
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请输入点位名称',
+                    },
+                    {
+                      max: 64,
+                      message: '最多可输入64个字符',
+                    },
+                  ],
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: 'nodeId', width: 150 },
+              properties: {
+                'configuration.nodeId': {
+                  type: 'string',
+                  'x-component': 'Input',
+                  'x-component-props': {
+                    readOnly: true,
+                  },
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '访问类型', width: 200 },
+              properties: {
+                accessModes: {
+                  type: 'string',
+                  'x-component': 'MySelect',
+                  'x-component-props': {
+                    placeholder: '请选择访问类型',
+                    mode: 'multiple',
+                    options: [
+                      { label: '读', value: 'read' },
+                      { label: '写', value: 'write' },
+                      { label: '订阅', value: 'subscribe' },
+                    ],
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择访问类型',
+                    },
+                  ],
+                },
+              },
+            },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '采集频率' },
+              properties: {
+                'configuration.interval': {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'MyInput',
+                  'x-component-props': {
+                    placeholder: '请输入采集频率',
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请输入采集频率',
+                    },
+                    {
+                      checkLength: true,
+                    },
+                  ],
+                },
+              },
+            },
+            column5: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { width: 210, title: '只推送变化的数据' },
+              properties: {
+                features: {
+                  type: 'array',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'MySelect',
+                  'x-component-props': {
+                    options: [
+                      {
+                        label: '是',
+                        value: true,
+                      },
+                      {
+                        label: '否',
+                        value: false,
+                      },
+                    ],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  useEffect(() => {
+    const one = (props?.data || [])[0];
+    form.setValues({
+      array: (props?.data || []).map((item) => {
+        return {
+          ...one,
+          features: one.features.includes('changedOnly'),
+          id: item.id,
+          name: item.name,
+          configuration: {
+            ...one.configuration,
+            nodeId: item.configuration.nodeId,
+          },
+        };
+      }),
+    });
+  }, [props.data]);
+
+  return (
+    <Modal
+      title={'批量编辑'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={1200}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button type="primary" key={2} onClick={() => {}}>
+          确定
+        </Button>,
+      ]}
+    >
+      <FormProvider form={form}>
+        <SchemaField schema={schema} />
+      </FormProvider>
+    </Modal>
+  );
+};

+ 137 - 159
src/pages/link/DataCollect/components/Point/Save/BatchUpdate.tsx

@@ -1,13 +1,13 @@
 import { Button, Modal } from 'antd';
-import { FormItem, Input, ArrayTable, Editable, NumberPicker } from '@formily/antd';
-import MyInput from './components/MyInput';
-import MySelect from './components/MySelect';
+import { FormItem, Checkbox, NumberPicker, FormGrid, Form } from '@formily/antd';
 import { createForm, registerValidateRules } from '@formily/core';
-import { FormProvider, createSchemaField } from '@formily/react';
-import { useEffect } from 'react';
+import { createSchemaField } from '@formily/react';
+import RadioCard from '@/components/RadioCard';
+import service from '@/pages/link/DataCollect/service';
+import { onlyMessage } from '@/utils/util';
 
 interface Props {
-  data?: any[];
+  data: any[];
   close: () => void;
   reload: () => void;
 }
@@ -15,40 +15,29 @@ interface Props {
 export default (props: Props) => {
   const SchemaField = createSchemaField({
     components: {
+      Form,
       FormItem,
-      Editable,
-      Input,
-      ArrayTable,
+      Checkbox,
+      FormGrid,
+      RadioCard,
       NumberPicker,
-      MyInput,
-      MySelect,
     },
   });
 
   const form = createForm({
-    initialValues: { array: [] },
-    effects: () => {
-      // onFieldValueChange('array.*.accessModes', (field, form1) => {
-      //   if (field.modified) {
-      //     const value = field.value;
-      //     console.log(value)
-      //     // form1.setFieldState('description', (state) => {
-      //     //   state.value = '';
-      //     // });
-      //   }
-      // });
-    },
+    initialValues: {},
   });
 
   registerValidateRules({
     checkLength(value) {
+      if (!value) return '';
       if (String(value).length > 64) {
         return {
           type: 'error',
           message: '最多可输入64个字符',
         };
       }
-      if (!(value % 1 === 0)) {
+      if (!(Number(value) % 1 === 0) || Number(value) <= 0) {
         return {
           type: 'error',
           message: '请输入非0正整数',
@@ -61,172 +50,161 @@ export default (props: Props) => {
   const schema = {
     type: 'object',
     properties: {
-      array: {
-        type: 'array',
-        'x-decorator': 'FormItem',
-        'x-component': 'ArrayTable',
+      layout: {
+        type: 'void',
+        'x-component': 'FormGrid',
         'x-component-props': {
-          pagination: { pageSize: 10 },
-          scroll: { x: '100%' },
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
         },
-        items: {
-          type: 'object',
-          properties: {
-            column1: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: '名称' },
-              properties: {
-                name: {
-                  type: 'string',
-                  'x-component': 'Input',
-                  'x-component-props': {
-                    placeholder: '请输入点位名称',
-                  },
-                  'x-validator': [
-                    {
-                      required: true,
-                      message: '请输入点位名称',
-                    },
-                    {
-                      max: 64,
-                      message: '最多可输入64个字符',
-                    },
-                  ],
-                },
-              },
+        properties: {
+          accessModes: {
+            title: '访问类型',
+            type: 'array',
+            'x-component': 'RadioCard',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
             },
-            column2: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: 'nodeId', width: 150 },
-              properties: {
-                'configuration.nodeId': {
-                  type: 'string',
-                  'x-component': 'Input',
-                  'x-component-props': {
-                    readOnly: true,
-                  },
-                },
+            'x-component-props': {
+              placeholder: '请选择访问类型',
+              model: 'multiple',
+              itemStyle: {
+                display: 'flex',
+                flexDirection: 'column',
+                justifyContent: 'space-around',
+                minWidth: '130px',
+                height: '50px',
               },
+              options: [
+                { label: '读', value: 'read' },
+                { label: '写', value: 'write' },
+                { label: '订阅', value: 'subscribe' },
+              ],
             },
-            column3: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: '访问类型', width: 200 },
-              properties: {
-                accessModes: {
-                  type: 'string',
-                  'x-component': 'MySelect',
-                  'x-component-props': {
-                    placeholder: '请选择访问类型',
-                    mode: 'multiple',
-                    options: [
-                      { label: '读', value: 'read' },
-                      { label: '写', value: 'write' },
-                      { label: '订阅', value: 'subscribe' },
-                    ],
-                  },
-                  'x-validator': [
-                    {
-                      required: true,
-                      message: '请选择访问类型',
-                    },
-                  ],
-                },
-              },
+            // 'x-validator': [
+            //   {
+            //     required: true,
+            //     message: '请选择访问类型',
+            //   },
+            // ],
+          },
+          interval: {
+            title: '采集频率',
+            'x-component': 'NumberPicker',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+              tooltip: '采集频率为0时不执行轮询任务',
             },
-            column4: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: '采集频率' },
-              properties: {
-                'configuration.interval': {
-                  type: 'string',
-                  'x-decorator': 'FormItem',
-                  'x-component': 'MyInput',
-                  'x-component-props': {
-                    placeholder: '请输入采集频率',
-                  },
-                  'x-validator': [
-                    {
-                      required: true,
-                      message: '请输入采集频率',
-                    },
-                    {
-                      checkLength: true,
-                    },
-                  ],
-                },
+            // 'x-reactions': {
+            //   dependencies: ['..accessModes'],
+            //   fulfill: {
+            //     state: {
+            //       visible: '{{($deps[0] || []).includes("subscribe")}}',
+            //     },
+            //   },
+            // },
+            'x-component-props': {
+              placeholder: '请输入采集频率',
+              addonAfter: '毫秒',
+              stringMode: true,
+              style: {
+                width: '100%',
               },
             },
-            column5: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': { width: 210, title: '只推送变化的数据' },
-              properties: {
-                features: {
-                  type: 'array',
-                  'x-decorator': 'FormItem',
-                  'x-component': 'MySelect',
-                  'x-component-props': {
-                    options: [
-                      {
-                        label: '是',
-                        value: true,
-                      },
-                      {
-                        label: '否',
-                        value: false,
-                      },
-                    ],
-                  },
-                },
+            'x-validator': [
+              {
+                min: 0,
+                message: '请输入正整数',
+              },
+              {
+                checkLength: true,
               },
+            ],
+          },
+          features: {
+            title: '',
+            type: 'array',
+            'x-component': 'Checkbox.Group',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
             },
+            // 'x-reactions': {
+            //   dependencies: ['.accessModes'],
+            //   fulfill: {
+            //     state: {
+            //       visible: '{{($deps[0] || []).includes("subscribe")}}',
+            //     },
+            //   },
+            // },
+            enum: [
+              {
+                label: '只推送变化的数据',
+                value: 'changedOnly',
+              },
+            ],
           },
         },
       },
     },
   };
 
-  useEffect(() => {
-    const one = (props?.data || [])[0];
-    form.setValues({
-      array: (props?.data || []).map((item) => {
-        return {
-          ...one,
-          features: one.features.includes('changedOnly'),
-          id: item.id,
-          name: item.name,
-          configuration: {
-            ...one.configuration,
-            nodeId: item.configuration.nodeId,
-          },
-        };
-      }),
-    });
-  }, [props.data]);
-
   return (
     <Modal
       title={'批量编辑'}
       maskClosable={false}
       visible
       onCancel={props.close}
-      width={1200}
+      width={600}
       footer={[
         <Button key={1} onClick={props.close}>
           取消
         </Button>,
-        <Button type="primary" key={2} onClick={() => {}}>
+        <Button
+          type="primary"
+          key={2}
+          onClick={async () => {
+            const value = await form.submit<any>();
+            const obj = props.data[0];
+            if (value.accessModes.length) {
+              obj.accessModes = value.accessModes;
+            }
+            if (value.features.length) {
+              obj.features = value.features;
+            }
+            if (Number(value.interval) >= 0) {
+              obj.configuration = {
+                ...obj.configuration,
+                interval: Number(value.interval),
+              };
+            }
+            const arr = props.data.map((item) => {
+              return {
+                ...item,
+                ...obj,
+                pointKey: obj?.configuration?.nodeId,
+              };
+            });
+            const response = await service.savePointBatch(obj.collectorId, obj.collectorName, arr);
+            if (response && response?.status === 200) {
+              onlyMessage('操作成功');
+              props.reload();
+            }
+          }}
+        >
           确定
         </Button>,
       ]}
     >
-      <FormProvider form={form}>
+      <div style={{ margin: '5px 0 20px 0' }}>
+        将批量修改{(props?.data || []).length}条数据的访问类型、采集频率、只推送变化的数据
+      </div>
+      <Form form={form} layout="vertical">
         <SchemaField schema={schema} />
-      </FormProvider>
+      </Form>
     </Modal>
   );
 };

+ 57 - 36
src/pages/link/DataCollect/components/Point/Save/modbus.tsx

@@ -194,39 +194,39 @@ export default (props: Props) => {
               },
             ],
           },
-          'configuration.codec.configuration.readIndex': {
-            title: '起始位置',
-            'x-component': 'NumberPicker',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 2,
-            },
-            'x-component-props': {
-              placeholder: '请输入起始位置',
-              stringMode: true,
-            },
-            'x-reactions': {
-              dependencies: ['...function'],
-              fulfill: {
-                state: {
-                  visible: '{{$deps[0] === "HoldingRegisters"}}',
-                },
-              },
-            },
-            'x-validator': [
-              {
-                required: true,
-                message: '请输入起始位置',
-              },
-              {
-                min: 1,
-                message: '请输入非0正整数',
-              },
-              {
-                checkLength: true,
-              },
-            ],
-          },
+          // 'configuration.codec.configuration.readIndex': {
+          //   title: '起始位置',
+          //   'x-component': 'NumberPicker',
+          //   'x-decorator': 'FormItem',
+          //   'x-decorator-props': {
+          //     gridSpan: 2,
+          //   },
+          //   'x-component-props': {
+          //     placeholder: '请输入起始位置',
+          //     stringMode: true,
+          //   },
+          //   'x-reactions': {
+          //     dependencies: ['...function'],
+          //     fulfill: {
+          //       state: {
+          //         visible: '{{$deps[0] === "HoldingRegisters"}}',
+          //       },
+          //     },
+          //   },
+          //   'x-validator': [
+          //     {
+          //       required: true,
+          //       message: '请输入起始位置',
+          //     },
+          //     {
+          //       min: 1,
+          //       message: '请输入非0正整数',
+          //     },
+          //     {
+          //       checkLength: true,
+          //     },
+          //   ],
+          // },
           'configuration.parameter.quantity': {
             title: '寄存器数量',
             'x-component': 'NumberPicker',
@@ -238,6 +238,16 @@ export default (props: Props) => {
               placeholder: '请输入寄存器数量',
               stringMode: true,
             },
+            // 'x-reactions': [
+            //   {
+            //     dependencies: ['configuration.codec.provider'],
+            //     fulfill: {
+            //       state: {
+            //         selfErrors: '{{$deps[0] && $self.value && {"int8:": 1, "int16": 2, "int32": 4, "int64": 8, "ieee754_float": 4, "ieee754_double": 8, "hex": 1}[$deps[0]] > $self.value * 2 ? "数据类型长度需 <= 寄存器数量 * 2" : ""}}',
+            //       },
+            //     },
+            //   },
+            // ],
             default: 1,
             'x-validator': [
               {
@@ -266,10 +276,12 @@ export default (props: Props) => {
             'x-reactions': [
               '{{useAsyncDataSource(getCodecProvider)}}',
               {
-                dependencies: ['..function'],
+                dependencies: ['..function', 'configuration.parameter.quantity'],
                 fulfill: {
                   state: {
                     visible: '{{$deps[0] === "HoldingRegisters"}}',
+                    selfErrors:
+                      '{{$deps[1] && $self.value && {"int8:": 1, "int16": 2, "int32": 4, "int64": 8, "ieee754_float": 4, "ieee754_double": 8, "hex": 1}[$self.value] > $deps[1] * 2 ? "数据类型长度需 <= 寄存器数量 * 2" : ""}}',
                   },
                 },
               },
@@ -413,8 +425,17 @@ export default (props: Props) => {
       collectorId: props?.collector?.id,
     };
     const response: any = props.data?.id
-      ? await service.updatePoint(props.data?.id, { ...props.data, ...value })
-      : await service.savePoint({ ...obj, ...props.data, ...value });
+      ? await service.updatePoint(props.data?.id, {
+          ...props.data,
+          ...value,
+          pointKey: value?.configuration?.parameter?.address,
+        })
+      : await service.savePoint({
+          ...obj,
+          ...props.data,
+          ...value,
+          pointKey: value?.configuration?.parameter?.address,
+        });
     if (response && response?.status === 200) {
       onlyMessage('操作成功');
       props.reload();

+ 16 - 35
src/pages/link/DataCollect/components/Point/Save/opc-ua.tsx

@@ -126,24 +126,6 @@ export default (props: Props) => {
               },
             ],
           },
-          // 'configuration.codec.provider': {
-          //   title: '数据类型',
-          //   'x-component': 'Select',
-          //   'x-decorator': 'FormItem',
-          //   'x-decorator-props': {
-          //     gridSpan: 2,
-          //   },
-          //   'x-component-props': {
-          //     placeholder: '请选择数据类型',
-          //   },
-          //   'x-reactions': '{{useAsyncDataSource(getCodecProvider)}}',
-          //   'x-validator': [
-          //     {
-          //       required: true,
-          //       message: '请选择数据类型',
-          //     },
-          //   ],
-          // },
           accessModes: {
             title: '访问类型',
             type: 'array',
@@ -183,14 +165,14 @@ export default (props: Props) => {
               gridSpan: 2,
             },
             default: 3000,
-            'x-reactions': {
-              dependencies: ['..accessModes'],
-              fulfill: {
-                state: {
-                  visible: '{{($deps[0] || []).includes("subscribe")}}',
-                },
-              },
-            },
+            // 'x-reactions': {
+            //   dependencies: ['..accessModes'],
+            //   fulfill: {
+            //     state: {
+            //       visible: '{{($deps[0] || []).includes("subscribe")}}',
+            //     },
+            //   },
+            // },
             'x-component-props': {
               placeholder: '请输入采集频率',
               addonAfter: '毫秒',
@@ -216,14 +198,14 @@ export default (props: Props) => {
             'x-decorator-props': {
               gridSpan: 2,
             },
-            'x-reactions': {
-              dependencies: ['.accessModes'],
-              fulfill: {
-                state: {
-                  visible: '{{($deps[0] || []).includes("subscribe")}}',
-                },
-              },
-            },
+            // 'x-reactions': {
+            //   dependencies: ['.accessModes'],
+            //   fulfill: {
+            //     state: {
+            //       visible: '{{($deps[0] || []).includes("subscribe")}}',
+            //     },
+            //   },
+            // },
             enum: [
               {
                 label: '只推送变化的数据',
@@ -252,7 +234,6 @@ export default (props: Props) => {
 
   const saveData = async () => {
     const value = await form.submit<PointItem>();
-    console.log(value);
     const response: any = props.data?.id
       ? await service.updatePoint(props.data?.id, { ...props.data, ...value })
       : await service.savePoint({ ...props.data, ...value });

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

@@ -0,0 +1,269 @@
+import { Button, Empty, Modal, Popconfirm, Spin, Transfer, Tree } from 'antd';
+import type { TransferDirection } from 'antd/es/transfer';
+import { useEffect, useState } from 'react';
+import service from '@/pages/link/DataCollect/service';
+import './scan.less';
+import { CloseOutlined } from '@ant-design/icons';
+import { Ellipsis } from '@/components';
+import { onlyMessage } from '@/utils/util';
+
+interface Props {
+  collector?: any;
+  close: () => void;
+  reload: () => void;
+}
+
+interface TreeTransferProps {
+  dataSource: any[];
+  targetKeys: string[];
+  onChange: (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void;
+  channelId?: string;
+  arrChange: (arr: any[]) => void;
+}
+
+const TreeTransfer = ({
+  dataSource,
+  targetKeys,
+  channelId,
+  arrChange,
+  ...restProps
+}: TreeTransferProps) => {
+  const [transferDataSource, setTransferDataSource] = useState<any[]>(dataSource);
+
+  useEffect(() => {
+    setTransferDataSource([...dataSource]);
+  }, [dataSource]);
+
+  const isChecked = (selectedKeys: (string | number)[], eventKey: string | number) =>
+    selectedKeys.includes(eventKey);
+
+  const generateTree = (treeNodes: any[] = [], checkedKeys: string[] = []): any[] =>
+    treeNodes.map(({ children, ...props }) => ({
+      ...props,
+      disabled: checkedKeys.includes(props.key as string) || props?.folder,
+      children: generateTree(children, checkedKeys),
+    }));
+
+  const queryDataByID = (list: any[] = [], key: string): any => {
+    for (let i = 0; i < list.length; i++) {
+      if (list[i].key === key) {
+        return list[i];
+      }
+      if (list[i].children) {
+        return queryDataByID(list[i].children, key);
+      }
+    }
+  };
+
+  const generateTargetTree = (treeNodes: any[] = []): any[] => {
+    const arr = treeNodes.map((item) => queryDataByID(transferDataSource, item));
+    return arr;
+  };
+
+  const updateTreeData = (list: any[], key: string, children: any[]): any[] => {
+    const arr = list.map((node) => {
+      if (node.key === key) {
+        return {
+          ...node,
+          children,
+        };
+      }
+      if (node?.children && node.children.length) {
+        return {
+          ...node,
+          children: updateTreeData(node.children, key, children),
+        };
+      }
+      return node;
+    });
+    return arr;
+  };
+
+  useEffect(() => {
+    arrChange(generateTargetTree(targetKeys));
+  }, [targetKeys]);
+
+  const onLoadData = (node: any) =>
+    new Promise<void>(async (resolve) => {
+      if (node.children.length || !node?.folder) {
+        resolve();
+        return;
+      }
+      const resp = await service.scanOpcUAList({
+        id: channelId,
+        nodeId: node.key,
+      });
+      if (resp.status === 200) {
+        const list = resp.result.map((item: any) => {
+          return {
+            ...item,
+            key: item.id,
+            title: item.name,
+            disabled: item?.folder,
+            isLeaf: !item?.folder,
+          };
+        });
+        const arr = updateTreeData(transferDataSource, node.key, [...list]);
+        setTransferDataSource([...arr]);
+      }
+      resolve();
+    });
+
+  return (
+    <Transfer
+      {...restProps}
+      targetKeys={targetKeys}
+      dataSource={transferDataSource}
+      render={(item) => item?.title}
+      showSelectAll={false}
+      titles={['源数据', '目标数据']}
+      oneWay
+    >
+      {({ direction, onItemSelect, selectedKeys, onItemRemove }) => {
+        if (direction === 'left') {
+          const checkedKeys = [...selectedKeys, ...targetKeys];
+          return (
+            <div style={{ margin: '10px' }}>
+              <Tree
+                blockNode
+                checkable
+                checkStrictly
+                checkedKeys={checkedKeys}
+                height={250}
+                treeData={generateTree(transferDataSource, targetKeys)}
+                onCheck={(_, { node: { key } }) => {
+                  onItemSelect(key as string, !isChecked(checkedKeys, key));
+                }}
+                onSelect={(_, { node: { key } }) => {
+                  onItemSelect(key as string, !isChecked(checkedKeys, key));
+                }}
+                loadData={onLoadData}
+              />
+            </div>
+          );
+        } else {
+          if (targetKeys.length) {
+            return (
+              <div>
+                {generateTargetTree(targetKeys).map((item) => {
+                  return (
+                    <div className={'right-item'} key={item.key}>
+                      <div style={{ width: 'calc(100% - 30px)' }}>
+                        <Ellipsis title={item?.title || item.key} />
+                      </div>
+                      <div style={{ width: 20, marginLeft: 10 }}>
+                        <Popconfirm
+                          title={'确认删除?'}
+                          onConfirm={() => {
+                            if (onItemRemove) {
+                              onItemRemove([item.key]);
+                            }
+                          }}
+                        >
+                          <CloseOutlined />
+                        </Popconfirm>
+                      </div>
+                    </div>
+                  );
+                })}
+              </div>
+            );
+          } else {
+            return <Empty style={{ marginTop: 10 }} />;
+          }
+        }
+      }}
+    </Transfer>
+  );
+};
+
+export default (props: Props) => {
+  const [targetKeys, setTargetKeys] = useState<string[]>([]);
+  const [treeData, setTreeData] = useState<any[]>([]);
+  const [loading, setLoading] = useState<boolean>(false);
+  const [spinning, setSpinning] = useState<boolean>(false);
+  const [arr, setArr] = useState<any[]>([]);
+  const onChange = (keys: any[]) => {
+    setTargetKeys(keys);
+  };
+
+  useEffect(() => {
+    if (props.collector?.channelId) {
+      setLoading(true);
+      service
+        .scanOpcUAList({
+          id: props.collector?.channelId,
+        })
+        .then((resp) => {
+          if (resp.status === 200) {
+            const list = resp.result.map((item: any) => {
+              return {
+                ...item,
+                key: item.id,
+                title: item.name,
+                disabled: item?.folder,
+              };
+            });
+            setTreeData(list);
+          }
+          setLoading(false);
+        });
+    }
+  }, [props.collector?.channelId]);
+
+  return (
+    <Modal
+      title={'扫描'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      width={700}
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          loading={spinning}
+          onClick={async () => {
+            if (!arr.length) {
+              onlyMessage('请选择目标数据', 'error');
+              return;
+            }
+            const list = arr.map((item) => {
+              return {
+                id: item.key,
+                name: item.title,
+                type: item.type,
+              };
+            });
+            setSpinning(true);
+            const resp = await service.savePointBatch(props.collector?.id, props.collector?.name, [
+              ...list,
+            ]);
+            if (resp.status === 200) {
+              onlyMessage('操作成功');
+              props.reload();
+            }
+            setSpinning(false);
+          }}
+        >
+          确定
+        </Button>,
+      ]}
+    >
+      <Spin spinning={loading}>
+        <TreeTransfer
+          channelId={props.collector?.channelId}
+          dataSource={treeData}
+          targetKeys={targetKeys}
+          onChange={onChange}
+          arrChange={(li) => {
+            setArr(li);
+          }}
+        />
+      </Spin>
+    </Modal>
+  );
+};

+ 62 - 3
src/pages/link/DataCollect/components/Point/index.tsx

@@ -15,6 +15,7 @@ import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import OpcSave from '@/pages/link/DataCollect/components/Point/Save/opc-ua';
 import WritePoint from '@/pages/link/DataCollect/components/Point/CollectorCard/WritePoint';
 import BatchUpdate from './Save/BatchUpdate';
+import { onlyMessage } from '@/utils/util';
 interface Props {
   type: boolean; // true: 综合查询  false: 数据采集
   data?: Partial<CollectorItem>;
@@ -37,6 +38,7 @@ const PointModel = model<{
   reload: boolean;
   batch_visible: boolean;
   list: any[];
+  selectKey: string[];
 }>({
   m_visible: false,
   p_visible: false,
@@ -46,6 +48,7 @@ const PointModel = model<{
   reload: false,
   batch_visible: false,
   list: [],
+  selectKey: [],
 });
 
 const PointCard = observer((props: PointCardProps) => {
@@ -84,6 +87,25 @@ const PointCard = observer((props: PointCardProps) => {
           },
         },
         {
+          title: '访问类型',
+          dataIndex: 'accessModes',
+          valueType: 'select',
+          valueEnum: {
+            read: {
+              text: '读',
+              status: 'read',
+            },
+            write: {
+              text: '写',
+              status: 'write',
+            },
+            subscribe: {
+              text: '订阅',
+              status: 'subscribe',
+            },
+          },
+        },
+        {
           title: '状态',
           dataIndex: 'state',
           valueType: 'select',
@@ -124,6 +146,21 @@ const PointCard = observer((props: PointCardProps) => {
           },
         },
         {
+          title: '状态',
+          dataIndex: 'state',
+          valueType: 'select',
+          valueEnum: {
+            enabled: {
+              text: '正常',
+              status: 'enabled',
+            },
+            disabled: {
+              text: '异常',
+              status: 'disabled',
+            },
+          },
+        },
+        {
           title: '说明',
           dataIndex: 'description',
         },
@@ -162,7 +199,6 @@ const PointCard = observer((props: PointCardProps) => {
       .then((resp) => {
         if (resp.status === 200) {
           setDataSource(resp.result);
-          PointModel.list = resp.result?.data || [];
           subscribeProperty((resp.result?.data || []).map((item: any) => item.id));
         }
         setLoading(false);
@@ -218,7 +254,11 @@ const PointCard = observer((props: PointCardProps) => {
                     style={{ marginLeft: 15 }}
                     isPermission={permission.update}
                     onClick={() => {
-                      PointModel.batch_visible = true;
+                      if (PointModel.list.length) {
+                        PointModel.batch_visible = true;
+                      } else {
+                        onlyMessage('请选择点位', 'error');
+                      }
                     }}
                     key="batch"
                   >
@@ -231,13 +271,30 @@ const PointCard = observer((props: PointCardProps) => {
               <>
                 <Row gutter={[18, 18]} style={{ marginTop: 10 }}>
                   {(dataSource?.data || []).map((record: any) => (
-                    <Col key={record.id} span={12}>
+                    <Col
+                      key={record.id}
+                      span={12}
+                      onClick={() => {
+                        if (props?.provider === 'OPC_UA') {
+                          if (PointModel.selectKey.includes(record.id)) {
+                            PointModel.selectKey = PointModel.selectKey.filter(
+                              (i) => i !== record.id,
+                            );
+                            PointModel.list = PointModel.list.filter((i) => i.id !== record.id);
+                          } else {
+                            PointModel.selectKey.push(record.id);
+                            PointModel.list.push(record);
+                          }
+                        }
+                      }}
+                    >
                       <CollectorCard
                         item={record}
                         wsValue={propertyValue[record.id]}
                         reload={() => {
                           handleSearch(param);
                         }}
+                        activeStyle={PointModel.selectKey.includes(record.id) ? 'active' : ''}
                         update={(item, flag) => {
                           if (flag) {
                             PointModel.writeVisible = true;
@@ -356,6 +413,8 @@ export default observer((props: Props) => {
           reload={() => {
             PointModel.batch_visible = false;
             PointModel.reload = !PointModel.reload;
+            PointModel.list = [];
+            PointModel.selectKey = [];
           }}
         />
       )}

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

@@ -25,6 +25,7 @@ type PointItem = {
   description?: string;
   provider: string;
   collectorId: string;
+  pointKey: string;
   circuitBreaker: {
     type: 'Ignore' | 'Break' | 'LowerFrequency';
     maxConsecutiveErrors?: string;