Kaynağa Gözat

fix: 扫描

100011797 3 yıl önce
ebeveyn
işleme
036e4e2aa8

+ 5 - 2
src/components/ProTableCard/CardItems/DataCollect/device.tsx

@@ -29,8 +29,11 @@ export default (props: CollectorCardProps) => {
       status={props.state?.value}
       statusText={props.state?.text}
       statusNames={{
-        enabled: StatusColorEnum.success,
-        disabled: StatusColorEnum.error,
+        running: StatusColorEnum.success,
+        disabled: StatusColorEnum.processing,
+        partialError: StatusColorEnum.warning,
+        failed: StatusColorEnum.error,
+        stopped: StatusColorEnum.default,
       }}
     >
       <div className={'pro-table-card-item'}>

+ 27 - 28
src/pages/link/DataCollect/components/Channel/index.tsx

@@ -38,13 +38,6 @@ export default observer((props: Props) => {
     total: 0,
   });
 
-  const test = (value: string) => {
-    if (value === 'error') {
-      return '';
-    }
-    return value;
-  };
-
   const columns: ProColumns<ChannelItem>[] = [
     {
       title: '名称',
@@ -67,24 +60,40 @@ export default observer((props: Props) => {
     },
     {
       title: '状态',
-      dataIndex: 'runningState',
+      dataIndex: 'state',
       valueType: 'select',
       valueEnum: {
         enabled: {
           text: '正常',
-          status: 'running',
+          status: 'enabled',
         },
         disabled: {
           text: '禁用',
-          status: 'stopped',
-        },
-        error: {
-          text: '异常',
-          status: 'error',
+          status: 'disabled',
         },
       },
-      search: {
-        transform: test,
+    },
+    {
+      title: '运行状态',
+      dataIndex: 'runningState',
+      valueType: 'select',
+      valueEnum: {
+        running: {
+          text: '运行中',
+          status: 'running',
+        },
+        partialError: {
+          text: '部分错误',
+          status: 'partialError',
+        },
+        failed: {
+          text: '错误',
+          status: 'failed',
+        },
+        stopped: {
+          text: '已停止',
+          status: 'stopped',
+        },
       },
     },
     {
@@ -113,17 +122,7 @@ export default observer((props: Props) => {
   const getState = (record: Partial<ChannelItem>) => {
     if (record) {
       if (record?.state?.value === 'enabled') {
-        if (record?.runningState?.value === 'running') {
-          return {
-            text: '正常',
-            value: 'enabled',
-          };
-        } else {
-          return {
-            text: '异常',
-            value: 'error',
-          };
-        }
+        return { ...record?.runningState };
       } else {
         return {
           text: '禁用',
@@ -157,7 +156,7 @@ export default observer((props: Props) => {
                     <Col key={record.id} span={props.type ? 8 : 12}>
                       <ChannelCard
                         {...record}
-                        status={getState(record)}
+                        state={getState(record)}
                         actions={[
                           <PermissionButton
                             type={'link'}

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

@@ -78,6 +78,29 @@ export default observer((props: Props) => {
           },
         },
         {
+          title: '运行状态',
+          dataIndex: 'runningState',
+          valueType: 'select',
+          valueEnum: {
+            running: {
+              text: '运行中',
+              status: 'running',
+            },
+            partialError: {
+              text: '部分错误',
+              status: 'partialError',
+            },
+            failed: {
+              text: '错误',
+              status: 'failed',
+            },
+            stopped: {
+              text: '已停止',
+              status: 'stopped',
+            },
+          },
+        },
+        {
           title: '说明',
           dataIndex: 'description',
         },
@@ -103,6 +126,29 @@ export default observer((props: Props) => {
           },
         },
         {
+          title: '运行状态',
+          dataIndex: 'runningState',
+          valueType: 'select',
+          valueEnum: {
+            running: {
+              text: '运行中',
+              status: 'running',
+            },
+            partialError: {
+              text: '部分错误',
+              status: 'partialError',
+            },
+            failed: {
+              text: '错误',
+              status: 'failed',
+            },
+            stopped: {
+              text: '已停止',
+              status: 'stopped',
+            },
+          },
+        },
+        {
           title: '说明',
           dataIndex: 'description',
         },
@@ -133,6 +179,21 @@ export default observer((props: Props) => {
     handleSearch(param);
   }, [props.id]);
 
+  const getState = (record: Partial<CollectorItem>) => {
+    if (record) {
+      if (record?.state?.value === 'enabled') {
+        return { ...record?.runningState };
+      } else {
+        return {
+          text: '禁用',
+          value: 'disabled',
+        };
+      }
+    } else {
+      return {};
+    }
+  };
+
   return (
     <div>
       <SearchComponent<CollectorItem>
@@ -171,6 +232,7 @@ export default observer((props: Props) => {
                     <Col key={record.id} span={props.type ? 8 : 12}>
                       <CollectorCard
                         {...record}
+                        state={getState(record)}
                         actions={[
                           <PermissionButton
                             type={'link'}

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

@@ -27,7 +27,7 @@
       left: -12px;
       display: flex;
       justify-content: center;
-      width: 86px;
+      width: 100px;
       padding: 2px 0;
       background: rgba(0, 0, 0, 0.06);
       transform: skewX(-45deg);

+ 13 - 5
src/pages/link/DataCollect/components/Point/CollectorCard/index.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useState } from 'react';
-import { Ellipsis, PermissionButton } from '@/components';
+import { BadgeStatus, Ellipsis, PermissionButton } from '@/components';
 import './index.less';
-import { Badge, Popconfirm, Spin, Tooltip } from 'antd';
+import { Popconfirm, Spin, Tooltip } from 'antd';
 import {
   CheckOutlined,
   DeleteOutlined,
@@ -13,6 +13,7 @@ import service from '@/pages/link/DataCollect/service';
 import { onlyMessage } from '@/utils/util';
 import moment from 'moment';
 import classNames from 'classnames';
+import { StatusColorEnum } from '@/components/BadgeStatus';
 
 export interface PointCardProps {
   item: Partial<PointItem>;
@@ -51,9 +52,16 @@ const CollectorCard = (props: PointCardProps) => {
         <div className={'card-item-left'}>
           <div className={'card-item-status'}>
             <div className={'card-item-status-content'}>
-              <Badge
-                status={item.state?.value === 'enabled' ? 'success' : 'error'}
-                text={item.state?.text}
+              <BadgeStatus
+                status={item.status?.value !== undefined ? item.status?.value : ''}
+                text={item.status?.text}
+                statusNames={{
+                  running: StatusColorEnum.success,
+                  disabled: StatusColorEnum.processing,
+                  partialError: StatusColorEnum.warning,
+                  failed: StatusColorEnum.error,
+                  stopped: StatusColorEnum.default,
+                }}
               />
             </div>
           </div>

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

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

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

@@ -188,7 +188,7 @@ export default (props: Props) => {
                 pointKey: obj?.configuration?.nodeId,
               };
             });
-            const response = await service.savePointBatch(obj.collectorId, obj.collectorName, arr);
+            const response = await service.savePointBatch(arr);
             if (response && response?.status === 200) {
               onlyMessage('操作成功');
               props.reload();

+ 25 - 9
src/pages/link/DataCollect/components/Point/Save/components/MyInput.tsx

@@ -1,6 +1,6 @@
 import { Space, InputNumber, Checkbox } from 'antd';
-import { useState } from 'react';
 import { ArrayItems } from '@formily/antd';
+import { useForm } from '@formily/react';
 
 interface Props {
   value: any;
@@ -9,27 +9,43 @@ interface Props {
 }
 export default (props: Props) => {
   const index = ArrayItems.useIndex!();
-  const [edit, setEdit] = useState<boolean>(index > 0);
+  const form = useForm();
+  const list = form.getState().values?.array || [];
   return (
     <Space>
       <InputNumber
-        style={{ width: 150 }}
+        style={{ width: 120 }}
         defaultValue={3000}
         placeholder={props.placeholder}
-        value={props.value}
+        value={props.value?.value}
+        stringMode={true}
         onChange={(val) => {
-          props.onChange(val);
+          props.onChange({
+            ...props.value,
+            value: val,
+          });
         }}
-        readOnly={edit}
+        disabled={index > 0 && props.value?.check}
         addonAfter={'毫秒'}
       />
       {index > 0 && (
         <Checkbox
           style={{ width: 60 }}
-          onChange={() => {
-            setEdit(!edit);
+          onChange={(e) => {
+            if (e.target.checked) {
+              const item = list[index - 1];
+              props.onChange({
+                check: e.target.checked,
+                value: item?.configuration?.interval?.value,
+              });
+            } else {
+              props.onChange({
+                ...props.value,
+                check: e.target.checked,
+              });
+            }
           }}
-          checked={edit}
+          checked={props.value?.check}
         >
           同上
         </Checkbox>

+ 26 - 7
src/pages/link/DataCollect/components/Point/Save/components/MySelect.tsx

@@ -1,6 +1,6 @@
 import { Space, Select, Checkbox, SelectProps } from 'antd';
-import { useState } from 'react';
 import { ArrayItems } from '@formily/antd';
+import { useForm, useField } from '@formily/react';
 interface Props extends SelectProps {
   value: any;
   onChange: (data: any) => void;
@@ -9,17 +9,24 @@ interface Props extends SelectProps {
 }
 export default (props: Props) => {
   const { onChange, ...extra } = props;
-  const [edit, setEdit] = useState<boolean>(true);
+  const form = useForm();
+  const self = useField();
+  const list = form.getState().values?.array || [];
   const index = ArrayItems.useIndex!();
   return (
     <Space>
       <Select
-        style={{ minWidth: 170 }}
+        style={{ minWidth: 150 }}
         placeholder={props.placeholder}
         {...extra}
+        value={props.value?.value}
         onChange={(val) => {
-          props.onChange(val);
+          props.onChange({
+            ...props.value,
+            value: val,
+          });
         }}
+        disabled={index > 0 && props.value?.check}
       >
         {props.options.map((item) => (
           <Select.Option key={item.value} value={item.value}>
@@ -30,10 +37,22 @@ export default (props: Props) => {
       {index > 0 && (
         <Checkbox
           style={{ width: 60 }}
-          onChange={() => {
-            setEdit(!edit);
+          onChange={(e) => {
+            if (e.target.checked) {
+              const item = list[index - 1];
+              const str = String(self.props?.name);
+              props.onChange({
+                check: e.target.checked,
+                value: item[str].value,
+              });
+            } else {
+              props.onChange({
+                ...props.value,
+                check: e.target.checked,
+              });
+            }
           }}
-          checked={edit}
+          checked={props.value?.check}
         >
           同上
         </Checkbox>

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

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

+ 330 - 167
src/pages/link/DataCollect/components/Point/Save/scan.tsx

@@ -1,64 +1,62 @@
-import { Button, Empty, Modal, Popconfirm, Spin, Transfer, Tree } from 'antd';
-import type { TransferDirection } from 'antd/es/transfer';
+import { Button, Col, Modal, Row, Spin, Tree } from 'antd';
 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';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { ArrayTable, Editable, FormItem, Input, NumberPicker } from '@formily/antd';
+import MyInput from '@/pages/link/DataCollect/components/Point/Save/components/MyInput';
+import MySelect from '@/pages/link/DataCollect/components/Point/Save/components/MySelect';
+import {
+  createForm,
+  Field,
+  FormPath,
+  onFieldValueChange,
+  registerValidateRules,
+} from '@formily/core';
 
 interface Props {
   collector?: any;
   close: () => void;
   reload: () => void;
+  data: any[];
 }
 
-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);
+export default (props: Props) => {
+  const [checkedKeys, setCheckedKeys] = useState<any>();
+  const [treeData, setTreeData] = useState<any[]>([]);
+  const [loading, setLoading] = useState<boolean>(false);
+  const [spinning, setSpinning] = useState<boolean>(false);
+  const [dataSource, setDataSource] = useState<any[]>([]);
+  const [selectKeys, setSelectKeys] = useState<any[]>([]);
 
   useEffect(() => {
-    setTransferDataSource([...dataSource]);
-  }, [dataSource]);
-
-  const isChecked = (selectedKeys: (string | number)[], eventKey: string | number) =>
-    selectedKeys.includes(eventKey);
+    setSelectKeys(props.data.map((item) => item.pointKey));
+  }, [props.data]);
 
-  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);
-      }
+  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);
+        });
     }
-  };
-
-  const generateTargetTree = (treeNodes: any[] = []): any[] => {
-    const arr = treeNodes.map((item) => queryDataByID(transferDataSource, item));
-    return arr;
-  };
+  }, [props.collector?.channelId]);
 
   const updateTreeData = (list: any[], key: string, children: any[]): any[] => {
     const arr = list.map((node) => {
@@ -79,18 +77,14 @@ const TreeTransfer = ({
     return arr;
   };
 
-  useEffect(() => {
-    arrChange(generateTargetTree(targetKeys));
-  }, [targetKeys]);
-
   const onLoadData = (node: any) =>
     new Promise<void>(async (resolve) => {
-      if (node.children.length || !node?.folder) {
+      if ((node?.children && node.children?.length) || !node?.folder) {
         resolve();
         return;
       }
       const resp = await service.scanOpcUAList({
-        id: channelId,
+        id: props.collector?.channelId,
         nodeId: node.key,
       });
       if (resp.status === 200) {
@@ -103,113 +97,251 @@ const TreeTransfer = ({
             isLeaf: !item?.folder,
           };
         });
-        const arr = updateTreeData(transferDataSource, node.key, [...list]);
-        setTransferDataSource([...arr]);
+        setTreeData((origin) => updateTreeData(origin, node.key, [...list]));
       }
       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 }} />;
-          }
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      Input,
+      ArrayTable,
+      NumberPicker,
+      MyInput,
+      MySelect,
+    },
+  });
+
+  const form = createForm({
+    initialValues: { array: [] },
+    effects: () => {
+      onFieldValueChange('array.*.accessModes', (field, f) => {
+        const nextPath = FormPath.transform(field.path, /\d+/, (index) => {
+          return `array.${parseInt(index + 1)}.accessModes`;
+        });
+        const nextValue = (field.query(nextPath).take() as Field)?.value;
+        if (nextValue && nextValue.check) {
+          f.setFieldState(nextPath, (state) => {
+            state.value = {
+              ...field.value,
+              check: nextValue.check,
+            };
+          });
         }
-      }}
-    </Transfer>
-  );
-};
+      });
+      onFieldValueChange('array.*.configuration.interval', (field, f) => {
+        const nextPath = FormPath.transform(field.path, /\d+/, (index) => {
+          return `array.${parseInt(index + 1)}.configuration.interval`;
+        });
+        const nextValue = (field.query(nextPath).take() as Field)?.value;
+        if (nextValue && nextValue.check) {
+          f.setFieldState(nextPath, (state) => {
+            state.value = {
+              ...field.value,
+              check: nextValue.check,
+            };
+          });
+        }
+      });
+      onFieldValueChange('array.*.features', (field, f) => {
+        const nextPath = FormPath.transform(field.path, /\d+/, (index) => {
+          return `array.${parseInt(index + 1)}.features`;
+        });
+        const nextValue = (field.query(nextPath).take() as Field)?.value;
+        if (nextValue && nextValue.check) {
+          f.setFieldState(nextPath, (state) => {
+            state.value = {
+              ...field.value,
+              check: nextValue.check,
+            };
+          });
+        }
+      });
+    },
+  });
 
-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);
+  registerValidateRules({
+    checkLength(value) {
+      if (String(value.value).length > 64) {
+        return {
+          type: 'error',
+          message: '最多可输入64个字符',
+        };
+      }
+      if (!(Number(value.value) % 1 === 0) || Number(value.value) < 0) {
+        return {
+          type: 'error',
+          message: '请输入正整数',
+        };
+      }
+      return '';
+    },
+    checkAccessModes(value) {
+      if (value.value && value.value.length) {
+        return '';
+      } else {
+        return {
+          type: 'error',
+          message: '请选择访问类型',
+        };
+      }
+    },
+  });
+
+  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' },
+              properties: {
+                'configuration.nodeId': {
+                  type: 'string',
+                  'x-component': 'Input',
+                  'x-component-props': {
+                    readOnly: true,
+                  },
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '访问类型' },
+              properties: {
+                accessModes: {
+                  'x-component': 'MySelect',
+                  'x-component-props': {
+                    placeholder: '请选择访问类型',
+                    mode: 'multiple',
+                    options: [
+                      { label: '读', value: 'read' },
+                      { label: '写', value: 'write' },
+                      { label: '订阅', value: 'subscribe' },
+                    ],
+                  },
+                  'x-validator': [
+                    {
+                      checkAccessModes: true,
+                    },
+                  ],
+                },
+              },
+            },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '采集频率' },
+              properties: {
+                'configuration.interval': {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'MyInput',
+                  'x-component-props': {
+                    placeholder: '请输入采集频率',
+                  },
+                  'x-validator': [
+                    {
+                      checkLength: true,
+                    },
+                  ],
+                },
+              },
+            },
+            column5: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '只推送变化的数据' },
+              properties: {
+                features: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'MySelect',
+                  'x-component-props': {
+                    options: [
+                      {
+                        label: '是',
+                        value: true,
+                      },
+                      {
+                        label: '否',
+                        value: false,
+                      },
+                    ],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
   };
 
   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]);
+    const one = (dataSource || [])[0];
+    form.setValues({
+      array: (dataSource || []).map((item) => {
+        return {
+          ...one,
+          features: {
+            value: (one?.features || []).includes('changedOnly'),
+            check: true,
+          },
+          id: item.id,
+          name: item.name,
+          accessModes: {
+            value: one?.accessModes || [],
+            check: true,
+          },
+          configuration: {
+            ...one.configuration,
+            interval: {
+              value: one?.configuration?.interval || 3000,
+              check: true,
+            },
+            nodeId: item.id,
+          },
+        };
+      }),
+    });
+  }, [dataSource]);
 
   return (
     <Modal
@@ -217,7 +349,7 @@ export default (props: Props) => {
       maskClosable={false}
       visible
       onCancel={props.close}
-      width={700}
+      width={1300}
       footer={[
         <Button key={1} onClick={props.close}>
           取消
@@ -227,21 +359,28 @@ export default (props: Props) => {
           key={2}
           loading={spinning}
           onClick={async () => {
+            const data: any = await form.submit();
+            const arr = data?.array || [];
             if (!arr.length) {
               onlyMessage('请选择目标数据', 'error');
               return;
             }
-            const list = arr.map((item) => {
+            const list = arr.map((item: any) => {
               return {
-                id: item.key,
-                name: item.title,
-                type: item.type,
+                name: item.name,
+                provider: 'OPC_UA',
+                collectorId: props.collector?.id,
+                collectorName: props.collector?.name,
+                pointKey: item.id,
+                configuration: {
+                  interval: item.configuration?.interval?.value,
+                },
+                features: !item.features?.value ? [] : ['changedOnly'],
+                accessModes: item.accessModes?.value || [],
               };
             });
             setSpinning(true);
-            const resp = await service.savePointBatch(props.collector?.id, props.collector?.name, [
-              ...list,
-            ]);
+            const resp = await service.savePointBatch([...list]);
             if (resp.status === 200) {
               onlyMessage('操作成功');
               props.reload();
@@ -254,15 +393,39 @@ export default (props: Props) => {
       ]}
     >
       <Spin spinning={loading}>
-        <TreeTransfer
-          channelId={props.collector?.channelId}
-          dataSource={treeData}
-          targetKeys={targetKeys}
-          onChange={onChange}
-          arrChange={(li) => {
-            setArr(li);
-          }}
-        />
+        <Row gutter={24}>
+          <Col span={6}>
+            <div style={{ padding: '10px 0' }}>
+              <Tree
+                blockNode
+                checkable
+                checkStrictly
+                checkedKeys={checkedKeys}
+                height={500}
+                treeData={treeData}
+                loadData={onLoadData}
+                onCheck={(checkedKeysValue, info) => {
+                  setCheckedKeys(checkedKeysValue);
+                  setDataSource(info.checkedNodes);
+                }}
+                titleRender={(nodeData) => {
+                  return (
+                    <div style={{ color: selectKeys.includes(nodeData.key) ? '#1d39c4' : 'black' }}>
+                      {nodeData.name}
+                    </div>
+                  );
+                }}
+              />
+            </div>
+          </Col>
+          <Col span={18}>
+            <div>
+              <FormProvider form={form}>
+                <SchemaField schema={schema} />
+              </FormProvider>
+            </div>
+          </Col>
+        </Row>
       </Spin>
     </Modal>
   );

+ 85 - 13
src/pages/link/DataCollect/components/Point/index.tsx

@@ -39,6 +39,7 @@ const PointModel = model<{
   batch_visible: boolean;
   list: any[];
   selectKey: string[];
+  arr: any[];
 }>({
   m_visible: false,
   p_visible: false,
@@ -49,6 +50,7 @@ const PointModel = model<{
   batch_visible: false,
   list: [],
   selectKey: [],
+  arr: [],
 });
 
 const PointCard = observer((props: PointCardProps) => {
@@ -65,6 +67,21 @@ const PointCard = observer((props: PointCardProps) => {
     total: 0,
   });
 
+  const getState = (record: Partial<ChannelItem>) => {
+    if (record) {
+      if (record?.state?.value === 'enabled') {
+        return { ...record?.runningState };
+      } else {
+        return {
+          text: '禁用',
+          value: 'disabled',
+        };
+      }
+    } else {
+      return {};
+    }
+  };
+
   const columns: ProColumns<PointItem>[] = props.type
     ? [
         {
@@ -115,12 +132,35 @@ const PointCard = observer((props: PointCardProps) => {
               status: 'enabled',
             },
             disabled: {
-              text: '异常',
+              text: '禁用',
               status: 'disabled',
             },
           },
         },
         {
+          title: '运行状态',
+          dataIndex: 'runningState',
+          valueType: 'select',
+          valueEnum: {
+            running: {
+              text: '运行中',
+              status: 'running',
+            },
+            partialError: {
+              text: '部分错误',
+              status: 'partialError',
+            },
+            failed: {
+              text: '错误',
+              status: 'failed',
+            },
+            stopped: {
+              text: '已停止',
+              status: 'stopped',
+            },
+          },
+        },
+        {
           title: '说明',
           dataIndex: 'description',
         },
@@ -131,17 +171,21 @@ const PointCard = observer((props: PointCardProps) => {
           dataIndex: 'name',
         },
         {
-          title: '状态',
-          dataIndex: 'state',
+          title: '访问类型',
+          dataIndex: 'accessModes',
           valueType: 'select',
           valueEnum: {
-            enabled: {
-              text: '正常',
-              status: 'enabled',
+            read: {
+              text: '',
+              status: 'read',
             },
-            disabled: {
-              text: '异常',
-              status: 'disabled',
+            write: {
+              text: '写',
+              status: 'write',
+            },
+            subscribe: {
+              text: '订阅',
+              status: 'subscribe',
             },
           },
         },
@@ -155,12 +199,35 @@ const PointCard = observer((props: PointCardProps) => {
               status: 'enabled',
             },
             disabled: {
-              text: '异常',
+              text: '禁用',
               status: 'disabled',
             },
           },
         },
         {
+          title: '运行状态',
+          dataIndex: 'runningState',
+          valueType: 'select',
+          valueEnum: {
+            running: {
+              text: '运行中',
+              status: 'running',
+            },
+            partialError: {
+              text: '部分错误',
+              status: 'partialError',
+            },
+            failed: {
+              text: '错误',
+              status: 'failed',
+            },
+            stopped: {
+              text: '已停止',
+              status: 'stopped',
+            },
+          },
+        },
+        {
           title: '说明',
           dataIndex: 'description',
         },
@@ -236,9 +303,13 @@ const PointCard = observer((props: PointCardProps) => {
               <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
                 <PermissionButton
                   isPermission={permission.add}
-                  onClick={() => {
+                  onClick={async () => {
                     if (props.provider === 'OPC_UA') {
-                      PointModel.p_add_visible = true;
+                      const resp = await service.queryPointNoPaging({ paging: false });
+                      if (resp.status === 200) {
+                        PointModel.p_add_visible = true;
+                        PointModel.arr = resp.result;
+                      }
                     } else {
                       PointModel.m_visible = true;
                     }
@@ -289,7 +360,7 @@ const PointCard = observer((props: PointCardProps) => {
                       }}
                     >
                       <CollectorCard
-                        item={record}
+                        item={{ ...record, status: getState(record) }}
                         wsValue={propertyValue[record.id]}
                         reload={() => {
                           handleSearch(param);
@@ -389,6 +460,7 @@ export default observer((props: Props) => {
           close={() => {
             PointModel.p_add_visible = false;
           }}
+          data={PointModel.arr}
           collector={props.data}
           reload={() => {
             PointModel.p_add_visible = false;

+ 10 - 8
src/pages/link/DataCollect/service.ts

@@ -13,6 +13,11 @@ class Service {
       method: 'POST',
       data: params,
     });
+  public queryPointNoPaging = (params: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/_query/no-paging`, {
+      method: 'GET',
+      params,
+    });
   public queryPointCount = (params: any) =>
     request(`/${SystemConst.API_BASE}/data-collect/point/_count`, {
       method: 'POST',
@@ -142,14 +147,11 @@ class Service {
       method: 'GET',
     });
 
-  public savePointBatch = (collectorId: string, collectorName: string, params: any[]) =>
-    request(
-      `/${SystemConst.API_BASE}/data-collect/opc/point/_batch?collectorId=${collectorId}&collectorName=${collectorName}`,
-      {
-        method: 'POST',
-        data: params,
-      },
-    );
+  public savePointBatch = (params: any[]) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point`, {
+      method: 'PATCH',
+      data: params,
+    });
 
   public dashboard = (data?: any) =>
     request(`/${SystemConst.API_BASE}/dashboard/_multi`, {