Wzyyy98 3 лет назад
Родитель
Сommit
3366773c08

+ 161 - 0
src/pages/device/Instance/Detail/ChildDevice/SaveChild/index.tsx

@@ -0,0 +1,161 @@
+import TitleComponent from '@/components/TitleComponent';
+import { Button, Col, Form, Input, Row, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '../../EdgeMap';
+import MapTable from '../../EdgeMap/mapTable';
+
+interface Props {
+  data: any;
+  close: () => void;
+  childData: any;
+}
+
+const SaveChild = (props: Props) => {
+  const [form] = Form.useForm();
+  const [productList, setProductList] = useState<any>([]);
+  const [metaData, setMetaData] = useState<any>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const getProductList = async () => {
+    const res = await service.getProductListNoPage({
+      terms: [{ column: 'accessProvider', value: 'edge-child-device' }],
+    });
+    if (res.status === 200) {
+      setProductList(res.result);
+    }
+  };
+
+  useEffect(() => {
+    getProductList();
+    if (props.childData?.id) {
+      setVisible(true);
+    }
+  }, []);
+
+  useEffect(() => {
+    if (props.childData.id) {
+      const product = productList.filter((i: any) => i.id === props.childData.productId)[0];
+      // console.log(JSON.parse(item.metadata || []));
+      if (product && product.metadata) {
+        const metadata = JSON.parse(product?.metadata || {})?.properties?.map((item: any) => ({
+          metadataId: item.id,
+          metadataName: `${item.name}(${item.id})`,
+          metadataType: 'property',
+        }));
+        if (metadata && metadata.length !== 0) {
+          service
+            .getMap(props.data.id, {
+              deviceId: props.childData.id,
+              query: {},
+            })
+            .then((res) => {
+              if (res.status === 200) {
+                // console.log(res.result)
+                //合并物模型
+                const array = res.result[0]?.reduce((x: any, y: any) => {
+                  const metadataId = metadata.find((item: any) => item.metadataId === y.metadataId);
+                  if (metadataId) {
+                    Object.assign(metadataId, y);
+                  } else {
+                    x.push(y);
+                  }
+                  return x;
+                }, metadata);
+                //删除物模型
+                const items = array.filter((item: any) => item.metadataName);
+                setMetaData(items);
+                const delList = array.filter((a: any) => !a.metadataName).map((b: any) => b.id);
+                //删除后解绑
+                if (delList && delList.length !== 0) {
+                  service.removeMap(props.data.id, {
+                    deviceId: props.childData.id,
+                    idList: [...delList],
+                  });
+                }
+              }
+            });
+        }
+      }
+    }
+  }, [productList]);
+
+  return (
+    <>
+      <TitleComponent
+        data={
+          <>
+            基本信息
+            <Button
+              onClick={() => {
+                props.close();
+              }}
+              style={{ marginLeft: 10 }}
+            >
+              返回
+            </Button>
+          </>
+        }
+      />
+      <Form layout="vertical" form={form} initialValues={props.childData}>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="设备名称"
+              name="name"
+              rules={[{ required: true, message: '请输入设备名称' }]}
+            >
+              <Input />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="产品名称"
+              name="productId"
+              rules={[{ required: true, message: '请选择产品名称' }]}
+            >
+              <Select
+                disabled={props.childData.id}
+                onChange={(e) => {
+                  if (e) {
+                    setVisible(true);
+                  }
+                  const item = productList.filter((i: any) => i.id === e)[0];
+                  const array = JSON.parse(item.metadata || [])?.properties?.map((i: any) => ({
+                    metadataType: 'property',
+                    metadataName: `${i.name}(${i.id})`,
+                    metadataId: i.id,
+                  }));
+                  setMetaData(array);
+                  console.log(array);
+                }}
+              >
+                {productList.map((item: any) => (
+                  <Select.Option key={item.id} value={item.id}>
+                    {item.name}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+        {visible && (
+          <Row>
+            <MapTable
+              metaData={metaData}
+              edgeId={props.data.id}
+              deviceId={props.childData.id}
+              title={'点位映射'}
+              productList={productList}
+              close={() => {
+                props.close();
+              }}
+              formRef={form}
+            />
+          </Row>
+        )}
+      </Form>
+    </>
+  );
+};
+
+export default SaveChild;

+ 120 - 72
src/pages/device/Instance/Detail/ChildDevice/index.tsx

@@ -2,7 +2,7 @@ import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import type { LogItem } from '@/pages/device/Instance/Detail/Log/typings';
 import { Badge, Button, Card, Popconfirm, Tooltip } from 'antd';
-import { DisconnectOutlined, SearchOutlined } from '@ant-design/icons';
+import { DisconnectOutlined, EditOutlined, SearchOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useRef, useState } from 'react';
@@ -13,19 +13,26 @@ import { Link } from 'umi';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { useDomFullHeight } from '@/hooks';
 import { onlyMessage } from '@/utils/util';
+import SaveChild from './SaveChild';
 
 const statusMap = new Map();
 statusMap.set('online', 'success');
 statusMap.set('offline', 'error');
 statusMap.set('notActive', 'warning');
 
-const ChildDevice = () => {
+interface Props {
+  data: any;
+}
+
+const ChildDevice = (props: Props) => {
+  console.log(props.data);
   const intl = useIntl();
   const [visible, setVisible] = useState<boolean>(false);
-
   const actionRef = useRef<ActionType>();
   const [searchParams, setSearchParams] = useState<any>({});
   const [bindKeys, setBindKeys] = useState<any[]>([]);
+  const [childVisible, setChildVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
 
   const { minHeight } = useDomFullHeight(`.device-detail-childDevice`);
 
@@ -132,84 +139,125 @@ const ChildDevice = () => {
             </Tooltip>
           </Popconfirm>
         </a>,
+        <>
+          {props.data.accessProvider === 'official-edge-gateway' && (
+            <a
+              onClick={() => {
+                setCurrent(record);
+                setChildVisible(true);
+              }}
+            >
+              <Tooltip title={'编辑'} key={'edit'}>
+                <EditOutlined />
+              </Tooltip>
+            </a>
+          )}
+        </>,
       ],
     },
   ];
 
   return (
     <Card className={'device-detail-childDevice'} style={{ minHeight }}>
-      <SearchComponent<LogItem>
-        field={[...columns]}
-        target="child-device"
-        enableSave={false}
-        // pattern={'simple'}
-        defaultParam={[
-          { column: 'parentId', value: InstanceModel?.detail?.id || '', termType: 'eq' },
-        ]}
-        onSearch={(param) => {
-          actionRef.current?.reset?.();
-          setSearchParams(param);
-        }}
-        // onReset={() => {
-        //   // 重置分页及搜索参数
-        //   actionRef.current?.reset?.();
-        //   setSearchParams({});
-        // }}
-      />
-      <ProTable<LogItem>
-        search={false}
-        columns={columns}
-        size="small"
-        scroll={{ x: 1366 }}
-        actionRef={actionRef}
-        params={searchParams}
-        rowKey="id"
-        columnEmptyText={''}
-        rowSelection={{
-          selectedRowKeys: bindKeys,
-          onChange: (selectedRowKeys, selectedRows) => {
-            setBindKeys(selectedRows.map((item) => item.id));
-          },
-        }}
-        toolBarRender={() => [
-          <Button
-            onClick={() => {
-              setVisible(true);
+      {childVisible ? (
+        <SaveChild
+          data={props.data}
+          childData={current}
+          close={() => {
+            setChildVisible(false);
+          }}
+        />
+      ) : (
+        <>
+          <SearchComponent<LogItem>
+            field={[...columns]}
+            target="child-device"
+            enableSave={false}
+            // pattern={'simple'}
+            defaultParam={[
+              { column: 'parentId', value: InstanceModel?.detail?.id || '', termType: 'eq' },
+            ]}
+            onSearch={(param) => {
               actionRef.current?.reset?.();
+              setSearchParams(param);
             }}
-            key="bind"
-            type="primary"
-          >
-            绑定
-          </Button>,
-          <Popconfirm
-            key="unbind"
-            onConfirm={async () => {
-              const resp = await service.unbindBatchDevice(InstanceModel.detail.id!, bindKeys);
-              if (resp.status === 200) {
-                onlyMessage('操作成功!');
-                setBindKeys([]);
-                actionRef.current?.reset?.();
-              }
+            // onReset={() => {
+            //   // 重置分页及搜索参数
+            //   actionRef.current?.reset?.();
+            //   setSearchParams({});
+            // }}
+          />
+          <ProTable<LogItem>
+            search={false}
+            columns={columns}
+            size="small"
+            scroll={{ x: 1366 }}
+            actionRef={actionRef}
+            params={searchParams}
+            rowKey="id"
+            columnEmptyText={''}
+            rowSelection={{
+              selectedRowKeys: bindKeys,
+              onChange: (selectedRowKeys, selectedRows) => {
+                setBindKeys(selectedRows.map((item) => item.id));
+              },
             }}
-            title={'确认解绑吗?'}
-          >
-            <Button>批量解绑</Button>
-          </Popconfirm>,
-        ]}
-        pagination={{
-          pageSize: 10,
-        }}
-        request={(params) => service.query(params)}
-      />
-      {visible && (
-        <BindChildDevice
-          data={{}}
-          onCancel={() => {
-            setVisible(false);
-            actionRef.current?.reload?.();
-          }}
-        />
+            toolBarRender={() => [
+              <>
+                {props.data.accessProvider === 'official-edge-gateway' && (
+                  <Button
+                    onClick={() => {
+                      // actionRef.current?.reset?.();
+                      setCurrent({});
+                      setChildVisible(true);
+                    }}
+                    key="save"
+                    type="primary"
+                  >
+                    新增并绑定
+                  </Button>
+                )}
+              </>,
+              <Button
+                onClick={() => {
+                  setVisible(true);
+                  actionRef.current?.reset?.();
+                }}
+                key="bind"
+                type="primary"
+              >
+                绑定
+              </Button>,
+              <Popconfirm
+                key="unbind"
+                onConfirm={async () => {
+                  const resp = await service.unbindBatchDevice(InstanceModel.detail.id!, bindKeys);
+                  if (resp.status === 200) {
+                    onlyMessage('操作成功!');
+                    setBindKeys([]);
+                    actionRef.current?.reset?.();
+                  }
+                }}
+                title={'确认解绑吗?'}
+              >
+                <Button>批量解绑</Button>
+              </Popconfirm>,
+            ]}
+            pagination={{
+              pageSize: 10,
+            }}
+            request={(params) => service.query(params)}
+          />
+          {visible && (
+            <BindChildDevice
+              data={{}}
+              onCancel={() => {
+                setVisible(false);
+                actionRef.current?.reload?.();
+              }}
+            />
+          )}
+        </>
       )}
     </Card>
   );

+ 73 - 3
src/pages/device/Instance/Detail/EdgeMap/index.tsx

@@ -1,12 +1,82 @@
 import useDomFullHeight from '@/hooks/document/useDomFullHeight';
-import { Card } from 'antd';
+import { Card, Empty } from 'antd';
+import { useEffect, useState } from 'react';
 import MapTable from './mapTable';
+import Service from './service';
 
-const EdgeMap = () => {
+interface Props {
+  data: any;
+}
+
+export const service = new Service();
+
+const EdgeMap = (props: Props) => {
+  const { data } = props;
   const { minHeight } = useDomFullHeight('.metadataMap');
+  const [properties, setProperties] = useState<any>([]);
+  const [empty, setEmpty] = useState<boolean>(false);
+  const [reload, setReload] = useState<string>('');
+
+  useEffect(() => {
+    setReload('');
+    const metadata = JSON.parse(data.metadata).properties?.map((item: any) => ({
+      metadataId: item.id,
+      metadataName: `${item.name}(${item.id})`,
+      metadataType: 'property',
+    }));
+    if (metadata && metadata.length !== 0) {
+      service
+        .getMap(data.parentId, {
+          deviceId: data.id,
+          query: {},
+        })
+        .then((res) => {
+          if (res.status === 200) {
+            // console.log(res.result)
+            //合并物模型
+            const array = res.result[0]?.reduce((x: any, y: any) => {
+              const metadataId = metadata.find((item: any) => item.metadataId === y.metadataId);
+              if (metadataId) {
+                Object.assign(metadataId, y);
+              } else {
+                x.push(y);
+              }
+              return x;
+            }, metadata);
+            //删除物模型
+            const items = array.filter((item: any) => item.metadataName);
+            setProperties(items);
+            const delList = array.filter((a: any) => !a.metadataName).map((b: any) => b.id);
+            //删除后解绑
+            if (delList && delList.length !== 0) {
+              service.removeMap(data.parentId, {
+                deviceId: data.id,
+                idList: [...delList],
+              });
+            }
+          }
+        });
+    } else {
+      setEmpty(true);
+    }
+    setProperties(metadata);
+    console.log(metadata);
+  }, [reload]);
+
   return (
     <Card className="metadataMap" style={{ minHeight }}>
-      <MapTable metaData={[]} />
+      {empty ? (
+        <Empty description={'暂无数据,请配置物模型'} style={{ marginTop: '10%' }} />
+      ) : (
+        <MapTable
+          metaData={properties}
+          deviceId={data.id}
+          edgeId={data.parentId}
+          reload={(param: string) => {
+            setReload(param);
+          }}
+        />
+      )}
     </Card>
   );
 };

+ 279 - 19
src/pages/device/Instance/Detail/EdgeMap/mapTable/index.tsx

@@ -1,25 +1,53 @@
 import PermissionButton from '@/components/PermissionButton';
+import TitleComponent from '@/components/TitleComponent';
 import { DisconnectOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import { FormItem, ArrayTable, Editable, Select, NumberPicker } from '@formily/antd';
-import { createForm } from '@formily/core';
+import { createForm, Field, FormPath, onFieldReact } from '@formily/core';
+import type { Response } from '@/utils/typings';
 import { FormProvider, createSchemaField } from '@formily/react';
+import { action } from '@formily/reactive';
 import { Badge, Button, Tooltip } from 'antd';
+import { useEffect, useState } from 'react';
+import MapTree from '../mapTree';
 import './index.less';
+import { service } from '..';
+import { onlyMessage } from '@/utils/util';
 
 interface Props {
   metaData: Record<string, string>[];
+  deviceId?: string;
+  edgeId: string;
+  reload?: any;
+  close?: any;
+  title?: string;
+  formRef?: any;
+  productList?: any;
 }
 
 const MapTable = (props: Props) => {
-  const { metaData } = props;
+  const { metaData, deviceId, reload, edgeId, productList } = props;
+  const [visible, setVisible] = useState<boolean>(false);
+  const [channelList, setChannelList] = useState<any>([]);
+  const [deviceData, seyDeviceData] = useState<any>({});
+
+  const remove = async (params: any) => {
+    const res = await service.removeMap(edgeId, {
+      deviceId: deviceId,
+      idList: [params],
+    });
+    if (res.status === 200) {
+      onlyMessage('解绑成功');
+      if (props.formRef) {
+        props.close();
+      } else {
+        reload('save');
+      }
+    }
+  };
 
   const Render = (propsName: any) => {
     const text = metaData.find((item: any) => item.metadataId === propsName.value);
-    return (
-      <>
-        {text?.metadataName}({text?.metadataId})
-      </>
-    );
+    return <>{text?.metadataName}</>;
   };
   const StatusRender = (propsRender: any) => {
     if (propsRender.value) {
@@ -43,8 +71,7 @@ const MapTable = (props: Props) => {
           title: '确认解绑',
           disabled: !record(index)?.id,
           onConfirm: async () => {
-            // deteleMaster(item.id)
-            // remove([record(index)?.id]);
+            remove(record(index)?.id);
           },
         }}
         key="unbind"
@@ -54,6 +81,63 @@ const MapTable = (props: Props) => {
       </PermissionButton>
     );
   };
+
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field)?.then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.[0]?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const getCollector = async (field: Field) => {
+    const path = FormPath.transform(
+      field.path,
+      /\d+/,
+      (index) => `requestList.${parseInt(index)}.channelId`,
+    );
+    const channelId = field.query(path).get('value');
+    if (!channelId) return [];
+    return service.edgeCollector(edgeId, {
+      terms: [
+        {
+          terms: [
+            {
+              column: 'channelId',
+              value: channelId,
+            },
+          ],
+        },
+      ],
+    });
+  };
+  const getPoint = async (field: Field) => {
+    const path = FormPath.transform(
+      field.path,
+      /\d+/,
+      (index) => `requestList.${parseInt(index)}.collectorId`,
+    );
+    const collectorId = field.query(path).get('value');
+    if (!collectorId) return [];
+    return service.edgePoint(edgeId, {
+      terms: [
+        {
+          terms: [
+            {
+              column: 'collectorId',
+              value: collectorId,
+            },
+          ],
+        },
+      ],
+    });
+  };
+
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -66,7 +150,99 @@ const MapTable = (props: Props) => {
       StatusRender,
     },
   });
-  const form = createForm({});
+
+  const save = async (item: any) => {
+    const res = await service.saveMap(edgeId, item);
+    if (res.status === 200) {
+      onlyMessage('保存成功');
+      if (props.formRef) {
+        props.close();
+      } else {
+        reload('save');
+      }
+    }
+  };
+
+  const form = createForm({
+    values: {
+      requestList: metaData,
+    },
+    effects: () => {
+      onFieldReact('requestList.*.channelId', async (field, f) => {
+        const value = (field as Field).value;
+        // console.log(field, 'provider')
+        if (value) {
+          const param = channelList.find((item: any) => item.value === value);
+          const providerPath = FormPath.transform(
+            field.path,
+            /\d+/,
+            (index) => `requestList.${parseInt(index)}.provider`,
+          );
+          f.setFieldState(providerPath, (state) => {
+            state.value = param?.provider;
+          });
+        }
+        const path = FormPath.transform(
+          field.path,
+          /\d+/,
+          (index) => `requestList.${parseInt(index)}.collectorId`,
+        );
+        const path1 = FormPath.transform(
+          field.path,
+          /\d+/,
+          (index) => `requestList.${parseInt(index)}.pointId`,
+        );
+        f.setFieldState(path, (state) => {
+          if (value) {
+            state.required = true;
+            form.validate();
+          } else {
+            state.required = false;
+            form.validate();
+          }
+        });
+        f.setFieldState(path1, (state) => {
+          if (value) {
+            state.required = true;
+            form.validate();
+          } else {
+            state.required = false;
+            form.validate();
+          }
+        });
+      });
+    },
+  });
+  const add = async () => {
+    const value = await props.formRef.validateFields();
+    const mapValue: any = await form.submit();
+    // console.log(value)
+    if (value && mapValue) {
+      if (mapValue.requestList.length === 0) {
+        onlyMessage('请配置物模型', 'warning');
+      } else {
+        const formData = {
+          ...value,
+          productName: productList.find((item: any) => item.id === value.productId).name,
+          parentId: edgeId,
+          id: deviceId ? deviceId : undefined,
+        };
+        const res = deviceId
+          ? await service.editDevice(formData)
+          : await service.addDevice(formData);
+        if (res.status === 200) {
+          const array = mapValue.requestList.filter((item: any) => item.channelId);
+          const submitData = {
+            deviceId: deviceId ? deviceId : res.result.id,
+            provider: array?.[0]?.provider,
+            requestList: array,
+          };
+          save(submitData);
+        }
+      }
+      // console.log(value, mapValue);
+    }
+  };
 
   const schema = {
     type: 'object',
@@ -87,7 +263,7 @@ const MapTable = (props: Props) => {
             column1: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
-              'x-component-props': { width: 120, title: '名称' },
+              'x-component-props': { width: 200, title: '名称' },
               properties: {
                 metadataId: {
                   type: 'string',
@@ -100,7 +276,7 @@ const MapTable = (props: Props) => {
               'x-component': 'ArrayTable.Column',
               'x-component-props': { width: 200, title: '通道' },
               properties: {
-                collectorId: {
+                channelId: {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
@@ -112,7 +288,7 @@ const MapTable = (props: Props) => {
                     filterOption: (input: string, option: any) =>
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
-                  // enum: masterList,
+                  enum: channelList,
                 },
               },
             },
@@ -131,7 +307,7 @@ const MapTable = (props: Props) => {
                 ),
               },
               properties: {
-                collectors: {
+                collectorId: {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
@@ -143,7 +319,13 @@ const MapTable = (props: Props) => {
                     filterOption: (input: string, option: any) =>
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
-                  'x-reactions': ['{{useAsyncDataSource(getName)}}'],
+                  'x-reactions': ['{{useAsyncDataSource(getCollector)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择采集器',
+                    },
+                  ],
                 },
               },
             },
@@ -164,7 +346,20 @@ const MapTable = (props: Props) => {
                     filterOption: (input: string, option: any) =>
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
-                  'x-reactions': ['{{useAsyncDataSource(getName)}}'],
+                  'x-reactions': ['{{useAsyncDataSource(getPoint)}}'],
+                },
+              },
+            },
+            column5: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { width: 0 },
+              properties: {
+                provider: {
+                  type: 'string',
+                  'x-hidden': true,
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
                 },
               },
             },
@@ -212,17 +407,82 @@ const MapTable = (props: Props) => {
     },
   };
 
+  useEffect(() => {
+    service.edgeChannel(edgeId).then((res) => {
+      if (res.status === 200) {
+        const list = res.result?.[0].map((item: any) => ({
+          label: item.name,
+          value: item.id,
+          provider: item.provider,
+        }));
+        setChannelList(list);
+      }
+    });
+  }, []);
+
   return (
     <div>
       <div className="top-button">
-        <Button style={{ marginRight: 10 }}>批量映射</Button>
-        <Button type="primary">保存</Button>
+        {props.title && <TitleComponent data={props.title} />}
+        <Button
+          style={{ marginRight: 10 }}
+          onClick={async () => {
+            const value = await props.formRef.validateFields();
+            if (value) {
+              const formData = {
+                ...value,
+                productName: productList.find((item: any) => item.id === value.productId).name,
+                parentId: edgeId,
+                // id: deviceId ? deviceId : undefined,
+              };
+              seyDeviceData(formData);
+              setVisible(true);
+            }
+          }}
+        >
+          批量映射
+        </Button>
+        <Button
+          type="primary"
+          onClick={async () => {
+            if (props.formRef) {
+              add();
+            } else {
+              const value: any = await form.submit();
+              const array = value.requestList.filter((item: any) => item.channelId);
+              const submitData = {
+                deviceId: deviceId,
+                provider: array[0].provider,
+                requestList: array,
+              };
+              save(submitData);
+            }
+          }}
+        >
+          保存
+        </Button>
       </div>
       <div>
         <FormProvider form={form}>
-          <SchemaField schema={schema} scope={{}} />
+          <SchemaField schema={schema} scope={{ useAsyncDataSource, getCollector, getPoint }} />
         </FormProvider>
       </div>
+      {visible && (
+        <MapTree
+          close={() => {
+            setVisible(false);
+            if (props.formRef) {
+              props.close();
+            } else {
+              reload('map');
+            }
+          }}
+          deviceId={deviceId || ''}
+          edgeId={edgeId}
+          metaData={metaData}
+          addDevice={deviceData}
+        />
+      )}
     </div>
   );
 };

+ 21 - 0
src/pages/device/Instance/Detail/EdgeMap/mapTree/index.less

@@ -0,0 +1,21 @@
+.map-tree {
+  .map-tree-top {
+    color: #000000b5;
+    background-color: #f6f6f6;
+  }
+
+  .map-tree-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 10px;
+
+    .ant-card-body {
+      padding: 12px !important;
+    }
+    .map-tree-card {
+      min-width: 350px;
+      min-height: 300px;
+    }
+  }
+}

+ 192 - 0
src/pages/device/Instance/Detail/EdgeMap/mapTree/index.tsx

@@ -0,0 +1,192 @@
+import { onlyMessage } from '@/utils/util';
+import { DeleteOutlined } from '@ant-design/icons';
+import { Button, Card, Modal, Tree, List, Popconfirm } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '..';
+import './index.less';
+
+interface Props {
+  close: any;
+  deviceId: string;
+  edgeId: string;
+  metaData: any;
+  addDevice?: any;
+}
+
+const MapTree = (props: Props) => {
+  const { deviceId, edgeId, close, metaData } = props;
+  const [data, setData] = useState<any>([]);
+  const [checked, setChecked] = useState<any>([]);
+  const filterRef = useRef<any>([]);
+  const [expandedKey, setExpandedKey] = useState<any>();
+  const [list, setList] = useState<any>([]);
+
+  const filterTree = (nodes: any[], lists: any[]) => {
+    if (!nodes?.length) {
+      return nodes;
+    }
+    return nodes.filter((item) => {
+      if (lists.indexOf(item.id) > -1) {
+        filterRef.current.push(item);
+        // console.log(filterRef.current, 'filterRef.current');
+        return false;
+      }
+      // 符合条件的保留,并且需要递归处理其子节点
+      item.collectors = filterTree(item.collectors, lists);
+      return true;
+    });
+  };
+
+  const pushTree = (node: any) => {
+    const newTree = data.map((item: any) => {
+      if (item.id === node.parentId) {
+        item.collectors.push(node);
+      }
+      return item;
+    });
+    setData(newTree);
+    filterRef.current = filterRef.current.filter((element: any) => element.id !== node.id);
+  };
+
+  const save = async () => {
+    // console.log(list,'list')
+    const params: any[] = [];
+    const metadataId = metaData.map((item: any) => item.metadataId);
+    list.forEach((item: any) => {
+      const array = item.points.map((element: any) => ({
+        channelId: item.parentId,
+        collectorId: element.collectorId,
+        pointId: element.id,
+        metadataType: 'property',
+        metadataId: metadataId.find((i: any) => i === element.id),
+        provider: data.find((it: any) => it.id === item.parentId).provider,
+      }));
+      params.push(...array);
+    });
+    const filterParms = params.filter((item) => !!item.metadataId);
+    if (deviceId) {
+      if (filterParms && filterParms.length !== 0) {
+        const res = await service.saveMap(edgeId, {
+          deviceId: deviceId,
+          provider: filterParms[0].provider,
+          requestList: filterParms,
+        });
+        if (res.status === 200) {
+          onlyMessage('保存成功');
+          close();
+        }
+      } else {
+        onlyMessage('暂无属性映射', 'warning');
+      }
+    } else {
+      const res = await service.addDevice(props.addDevice);
+      if (res.status === 200) {
+        const resp = await service.saveMap(edgeId, {
+          deviceId: res.result.id,
+          provider: filterParms[0].provider,
+          requestList: filterParms,
+        });
+        if (resp.status === 200) {
+          onlyMessage('保存成功');
+          close();
+        }
+      }
+    }
+  };
+
+  useEffect(() => {
+    service.treeMap(edgeId).then((res) => {
+      if (res.status === 200) {
+        console.log(res.result?.[0], 'data');
+        setData(res.result?.[0]);
+        setExpandedKey([res.result?.[0].id]);
+      }
+    });
+  }, []);
+
+  useEffect(() => {
+    setList(filterRef.current);
+  }, [filterRef.current]);
+
+  return (
+    <Modal
+      title="批量映射"
+      visible
+      onCancel={() => {
+        close();
+      }}
+      onOk={() => {
+        save();
+      }}
+      width="900px"
+    >
+      <div className="map-tree">
+        <div className="map-tree-top">
+          采集器的点位名称与属性名称一致时将自动映射绑定;有多个采集器点位名称与属性名称一致时以第1个采集器的点位数据进行绑定
+        </div>
+        <div className="map-tree-content">
+          <Card title="源数据" className="map-tree-card">
+            <Tree
+              key={'id'}
+              checkable
+              selectable={false}
+              expandedKeys={expandedKey}
+              onExpand={(expandedKeys) => {
+                setExpandedKey(expandedKeys);
+              }}
+              onCheck={(checkeds) => {
+                setChecked(checkeds);
+              }}
+            >
+              {data?.map((item: any) => (
+                <Tree.TreeNode key={item.id} title={item.name} checkable={false}>
+                  {(item?.collectors || []).map((collector: any) => (
+                    <Tree.TreeNode key={collector.id} title={collector.name}>
+                      {(collector?.points || []).map((i: any) => (
+                        <Tree.TreeNode checkable={false} key={i.id} title={i.name}></Tree.TreeNode>
+                      ))}
+                    </Tree.TreeNode>
+                  ))}
+                </Tree.TreeNode>
+              ))}
+            </Tree>
+          </Card>
+          <div>
+            <Button
+              disabled={checked && checked.length === 0}
+              onClick={() => {
+                const item = filterTree(data, checked);
+                setData(item);
+              }}
+            >
+              加入右侧
+            </Button>
+          </div>
+          <Card title="采集器" className="map-tree-card">
+            <List
+              size="small"
+              dataSource={list}
+              renderItem={(item: any) => (
+                <List.Item
+                  actions={[
+                    <Popconfirm
+                      title="确定删除?"
+                      onConfirm={() => {
+                        pushTree(item);
+                      }}
+                    >
+                      <DeleteOutlined />
+                    </Popconfirm>,
+                  ]}
+                >
+                  {item.name}
+                </List.Item>
+              )}
+            />
+          </Card>
+        </div>
+      </div>
+    </Modal>
+  );
+};
+export default MapTree;

+ 38 - 0
src/pages/device/Instance/Detail/EdgeMap/service.ts

@@ -32,6 +32,44 @@ class Service extends BaseService<any> {
         data,
       },
     );
+  getMap = (deviceId: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/edge/operations/${deviceId}/device-collector-list/invoke`, {
+      method: 'POST',
+      data,
+    });
+  removeMap = (deviceId: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/edge/operations/${deviceId}/device-collector-delete/invoke`, {
+      method: 'POST',
+      data,
+    });
+  treeMap = (deviceId: string, data?: any) =>
+    request(
+      `/${SystemConst.API_BASE}/edge/operations/${deviceId}/data-collector-channel-tree/invoke`,
+      {
+        method: 'POST',
+        data,
+      },
+    );
+  saveMap = (deviceId: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/edge/operations/${deviceId}/device-collector-save/invoke`, {
+      method: 'POST',
+      data,
+    });
+  getProductListNoPage = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging?paging=false`, {
+      method: 'POST',
+      data: params,
+    });
+  addDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance`, {
+      method: 'POST',
+      data: params,
+    });
+  editDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance`, {
+      method: 'PATCH',
+      data: params,
+    });
 }
 
 export default Service;

+ 9 - 8
src/pages/device/Instance/Detail/index.tsx

@@ -29,6 +29,7 @@ import Service from '@/pages/device/Instance/service';
 import useLocation from '@/hooks/route/useLocation';
 import { onlyMessage } from '@/utils/util';
 import Parsing from './Parsing';
+import EdgeMap from './EdgeMap';
 // import EdgeMap from './EdgeMap';
 
 export const deviceStatus = new Map();
@@ -210,16 +211,16 @@ const InstanceDetail = observer(() => {
         datalist.push({
           key: 'child-device',
           tab: '子设备',
-          component: <ChildDevice />,
+          component: <ChildDevice data={InstanceModel.detail} />,
+        });
+      }
+      if (response.result.accessProvider === 'edge-child-device' && response.result.parentId) {
+        datalist.push({
+          key: 'edge-map',
+          tab: '边缘端映射',
+          component: <EdgeMap data={InstanceModel.detail} />,
         });
       }
-      // if(response.result){
-      //   datalist.push({
-      //     key: 'edge-map',
-      //     tab: '边缘端映射',
-      //     component: <EdgeMap />,
-      //   })
-      // }
       setList(datalist);
       // 写入物模型数据
       const metadata: DeviceMetadata = JSON.parse(response.result?.metadata || '{}');

+ 5 - 0
src/pages/device/Instance/service.ts

@@ -14,6 +14,11 @@ class Service extends BaseService<DeviceInstance> {
       method: 'GET',
       params,
     });
+  public getProductListNoPage = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging?paging=false`, {
+      method: 'POST',
+      data: params,
+    });
 
   // 批量删除设备
   public batchDeleteDevice = (params: any) =>

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

@@ -4,6 +4,7 @@ import { model } from '@formily/reactive';
 import Point from '../components/Point';
 import Device from '../components/Device';
 import Channel from '../components/Channel';
+import { useEffect } from 'react';
 
 const dataModel = model<{
   tab: string;
@@ -29,6 +30,11 @@ export default observer(() => {
       component: <Point type={true} />,
     },
   ];
+
+  useEffect(() => {
+    dataModel.tab = 'channel';
+  }, []);
+
   return (
     <PageContainer
       tabList={list}

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

@@ -188,6 +188,7 @@ export default (props: Props) => {
             ...value,
             provider: resp.result.provider,
             channelId: props.channelId,
+            channelName: resp.result.name,
             configuration: {
               ...value.configuration,
             },

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

@@ -254,6 +254,7 @@ export default observer((props: Props) => {
                                       defaultMessage: '操作成功!',
                                     }),
                                   );
+                                  handleSearch(param);
                                 } else {
                                   onlyMessage(
                                     intl.formatMessage({ id: 'pages.device.instance.deleteTip' }),

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

@@ -51,12 +51,13 @@
     }
     .card-item-content {
       display: flex;
+      overflow: hidden;
       .card-item-content-item {
         flex-direction: row-reverse;
         align-items: flex-start;
         justify-content: space-around;
-        //min-width: calc(50% - 40px);
-        width: calc(50% - 20px);
+        min-width: calc(50% - 40px);
+        max-width: calc(50% - 20px);
 
         .card-item-content-item-empty {
           margin-top: 10px;

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

@@ -1,30 +1,36 @@
-import { useState } from 'react';
+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 OpcSave from '../Save/opc-ua';
+// import ModbusSave from '../Save/modbus';
 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 WritePoint from '@/pages/link/DataCollect/components/Point/CollectorCard/WritePoint';
 
 export interface PointCardProps {
   item: Partial<PointItem>;
   reload: () => void;
   wsValue: any;
+  update: (item: any, type?: boolean) => void;
 }
 
 const opcImage = require('/public/images/DataCollect/device-opcua.png');
 const modbusImage = require('/public/images/DataCollect/device-modbus.png');
 
-export default (props: PointCardProps) => {
+const CollectorCard = (props: PointCardProps) => {
   const { item, wsValue } = props;
-  const [editVisible, setEditVisible] = useState<boolean>(false);
+  // const [editVisible, setEditVisible] = useState<boolean>(false);
   const [spinning, setSpinning] = useState<boolean>(false);
-  const [writeVisible, setWriteVisible] = useState<boolean>(false);
+  // const [writeVisible, setWriteVisible] = useState<boolean>(false);
   const { permission } = PermissionButton.usePermission('link/DataCollect/DataGathering');
+  const [dataValue, setDataValue] = useState<any>(wsValue);
+
+  useEffect(() => {
+    setDataValue(wsValue);
+  }, [wsValue]);
 
   const read = async () => {
     if (item?.collectorId && item?.id) {
@@ -37,33 +43,33 @@ export default (props: PointCardProps) => {
     }
   };
 
-  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}
-      />
-    );
-  };
+  // 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'}>
@@ -113,7 +119,8 @@ export default (props: PointCardProps) => {
                   <FormOutlined
                     onClick={() => {
                       if (permission.update) {
-                        setEditVisible(true);
+                        // setEditVisible(true);
+                        props.update(item);
                       }
                     }}
                   />
@@ -121,17 +128,18 @@ export default (props: PointCardProps) => {
               </div>
             </div>
             <div className={'card-item-content'}>
-              {wsValue ? (
+              {dataValue ? (
                 <div className={'card-item-content-item'}>
                   <div className={'card-item-content-item-header'}>
                     <div className={'card-item-content-item-header-title'}>
-                      <Ellipsis title={`${wsValue?.parseData}(${wsValue?.dataType})`} />
+                      <Ellipsis title={`${dataValue?.parseData}(${dataValue?.dataType})`} />
                     </div>
                     <div className={'card-item-content-item-header-action'}>
                       <EditOutlined
                         style={{ marginRight: 5 }}
                         onClick={() => {
-                          setWriteVisible(true);
+                          props.update(item, true);
+                          // setWriteVisible(true);
                         }}
                       />
                       <RedoOutlined
@@ -142,13 +150,13 @@ export default (props: PointCardProps) => {
                     </div>
                   </div>
                   <div className={'card-item-content-item-text'}>
-                    <Ellipsis title={wsValue?.hex || ''} />
+                    <Ellipsis title={dataValue?.hex || ''} />
                   </div>
                   <div className={'card-item-content-item-text'}>
                     <Ellipsis
                       title={
-                        wsValue?.timestamp
-                          ? moment(wsValue.timestamp).format('YYYY-MM-DD HH:mm:ss')
+                        dataValue?.timestamp
+                          ? moment(dataValue.timestamp).format('YYYY-MM-DD HH:mm:ss')
                           : ''
                       }
                     />
@@ -164,15 +172,10 @@ export default (props: PointCardProps) => {
                       className={'action'}
                       style={{ margin: '0 15px' }}
                       onClick={() => {
-                        setWriteVisible(true);
-                      }}
-                    />
-                    <RedoOutlined
-                      className={'action'}
-                      onClick={() => {
-                        read();
+                        props.update(item, true);
                       }}
                     />
+                    <RedoOutlined className={'action'} onClick={read} />
                   </div>
                 </div>
               )}
@@ -210,16 +213,18 @@ export default (props: PointCardProps) => {
             </div>
           </div>
         </div>
-        {editVisible && saveComponent()}
-        {writeVisible && (
-          <WritePoint
-            data={item}
-            onCancel={() => {
-              setWriteVisible(false);
-            }}
-          />
-        )}
+        {/*{editVisible && saveComponent()}*/}
+        {/*{writeVisible && (*/}
+        {/*  <WritePoint*/}
+        {/*    data={item}*/}
+        {/*    onCancel={() => {*/}
+        {/*      setWriteVisible(false);*/}
+        {/*    }}*/}
+        {/*  />*/}
+        {/*)}*/}
       </div>
     </Spin>
   );
 };
+
+export default CollectorCard;

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

@@ -0,0 +1,232 @@
+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: [] },
+    effects: () => {
+      // onFieldValueChange('array.*.accessModes', (field, form1) => {
+      //   if (field.modified) {
+      //     const value = field.value;
+      //     console.log(value)
+      //     // form1.setFieldState('description', (state) => {
+      //     //   state.value = '';
+      //     // });
+      //   }
+      // });
+    },
+  });
+
+  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>
+  );
+};

+ 39 - 0
src/pages/link/DataCollect/components/Point/Save/components/MyInput.tsx

@@ -0,0 +1,39 @@
+import { Space, InputNumber, Checkbox } from 'antd';
+import { useState } from 'react';
+import { ArrayItems } from '@formily/antd';
+
+interface Props {
+  value: any;
+  onChange: (data: any) => void;
+  placeholder?: string;
+}
+export default (props: Props) => {
+  const index = ArrayItems.useIndex!();
+  const [edit, setEdit] = useState<boolean>(index > 0);
+  return (
+    <Space>
+      <InputNumber
+        style={{ width: 150 }}
+        defaultValue={3000}
+        placeholder={props.placeholder}
+        value={props.value}
+        onChange={(val) => {
+          props.onChange(val);
+        }}
+        readOnly={edit}
+        addonAfter={'毫秒'}
+      />
+      {index > 0 && (
+        <Checkbox
+          style={{ width: 60 }}
+          onChange={() => {
+            setEdit(!edit);
+          }}
+          checked={edit}
+        >
+          同上
+        </Checkbox>
+      )}
+    </Space>
+  );
+};

+ 43 - 0
src/pages/link/DataCollect/components/Point/Save/components/MySelect.tsx

@@ -0,0 +1,43 @@
+import { Space, Select, Checkbox, SelectProps } from 'antd';
+import { useState } from 'react';
+import { ArrayItems } from '@formily/antd';
+interface Props extends SelectProps {
+  value: any;
+  onChange: (data: any) => void;
+  placeholder?: string;
+  options: any[];
+}
+export default (props: Props) => {
+  const { onChange, ...extra } = props;
+  const [edit, setEdit] = useState<boolean>(true);
+  const index = ArrayItems.useIndex!();
+  return (
+    <Space>
+      <Select
+        style={{ minWidth: 170 }}
+        placeholder={props.placeholder}
+        {...extra}
+        onChange={(val) => {
+          props.onChange(val);
+        }}
+      >
+        {props.options.map((item) => (
+          <Select.Option key={item.value} value={item.value}>
+            {item.label}
+          </Select.Option>
+        ))}
+      </Select>
+      {index > 0 && (
+        <Checkbox
+          style={{ width: 60 }}
+          onChange={() => {
+            setEdit(!edit);
+          }}
+          checked={edit}
+        >
+          同上
+        </Checkbox>
+      )}
+    </Space>
+  );
+};

+ 151 - 42
src/pages/link/DataCollect/components/Point/index.tsx

@@ -12,24 +12,43 @@ import ModbusSave from '@/pages/link/DataCollect/components/Point/Save/modbus';
 import Scan from '@/pages/link/DataCollect/components/Point/Save/scan';
 import { map } from 'rxjs/operators';
 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';
 interface Props {
   type: boolean; // true: 综合查询  false: 数据采集
   data?: Partial<CollectorItem>;
   provider?: 'OPC_UA' | 'MODBUS_TCP';
 }
 
+interface PointCardProps {
+  type: boolean; // true: 综合查询  false: 数据采集
+  data?: Partial<CollectorItem>;
+  provider?: 'OPC_UA' | 'MODBUS_TCP';
+  reload: boolean; // 变化时刷新
+}
+
 const PointModel = model<{
   m_visible: boolean;
+  p_visible: boolean;
   p_add_visible: boolean;
+  writeVisible: boolean;
   current: Partial<PointItem>;
+  reload: boolean;
+  batch_visible: boolean;
+  list: any[];
 }>({
   m_visible: false,
+  p_visible: false,
   p_add_visible: false,
   current: {},
+  writeVisible: false,
+  reload: false,
+  batch_visible: false,
+  list: [],
 });
 
-export default observer((props: Props) => {
+const PointCard = observer((props: PointCardProps) => {
   const [subscribeTopic] = useSendWebsocketMessage();
   const { minHeight } = useDomFullHeight(`.data-collect-point`, 24);
   const [param, setParam] = useState({ pageSize: 12, terms: [] });
@@ -43,46 +62,72 @@ export default observer((props: Props) => {
     total: 0,
   });
 
-  const columns: ProColumns<PointItem>[] = [
-    {
-      title: '名称',
-      dataIndex: 'name',
-    },
-    {
-      title: '通讯协议',
-      dataIndex: 'provider',
-      valueType: 'select',
-      valueEnum: {
-        OPC_UA: {
-          text: 'OPC_UA',
-          status: 'OPC_UA',
+  const columns: ProColumns<PointItem>[] = props.type
+    ? [
+        {
+          title: '名称',
+          dataIndex: 'name',
+        },
+        {
+          title: '通讯协议',
+          dataIndex: 'provider',
+          valueType: 'select',
+          valueEnum: {
+            OPC_UA: {
+              text: 'OPC_UA',
+              status: 'OPC_UA',
+            },
+            MODBUS_TCP: {
+              text: 'MODBUS_TCP',
+              status: 'MODBUS_TCP',
+            },
+          },
+        },
+        {
+          title: '状态',
+          dataIndex: 'state',
+          valueType: 'select',
+          valueEnum: {
+            enabled: {
+              text: '正常',
+              status: 'enabled',
+            },
+            disabled: {
+              text: '异常',
+              status: 'disabled',
+            },
+          },
+        },
+        {
+          title: '说明',
+          dataIndex: 'description',
         },
-        MODBUS_TCP: {
-          text: 'MODBUS_TCP',
-          status: 'MODBUS_TCP',
+      ]
+    : [
+        {
+          title: '名称',
+          dataIndex: 'name',
         },
-      },
-    },
-    {
-      title: '状态',
-      dataIndex: 'state',
-      valueType: 'select',
-      valueEnum: {
-        enabled: {
-          text: '正常',
-          status: 'enabled',
+        {
+          title: '状态',
+          dataIndex: 'state',
+          valueType: 'select',
+          valueEnum: {
+            enabled: {
+              text: '正常',
+              status: 'enabled',
+            },
+            disabled: {
+              text: '异常',
+              status: 'disabled',
+            },
+          },
         },
-        disabled: {
-          text: '异常',
-          status: 'disabled',
+        {
+          title: '说明',
+          dataIndex: 'description',
         },
-      },
-    },
-    {
-      title: '说明',
-      dataIndex: 'description',
-    },
-  ];
+      ];
 
   const subRef = useRef<any>(null);
 
@@ -117,7 +162,7 @@ export default observer((props: Props) => {
       .then((resp) => {
         if (resp.status === 200) {
           setDataSource(resp.result);
-          console.log(resp.result);
+          PointModel.list = resp.result?.data || [];
           subscribeProperty((resp.result?.data || []).map((item: any) => item.id));
         }
         setLoading(false);
@@ -126,7 +171,7 @@ export default observer((props: Props) => {
 
   useEffect(() => {
     handleSearch(param);
-  }, [props.data?.id]);
+  }, [props.data?.id, props.reload]);
 
   useEffect(() => {
     return () => {
@@ -168,6 +213,18 @@ export default observer((props: Props) => {
                 >
                   {props?.provider === 'OPC_UA' ? '扫描' : '新增'}
                 </PermissionButton>
+                {props.provider === 'OPC_UA' && (
+                  <PermissionButton
+                    style={{ marginLeft: 15 }}
+                    isPermission={permission.update}
+                    onClick={() => {
+                      PointModel.batch_visible = true;
+                    }}
+                    key="batch"
+                  >
+                    批量编辑
+                  </PermissionButton>
+                )}
               </div>
             )}
             {dataSource?.data.length ? (
@@ -181,6 +238,18 @@ export default observer((props: Props) => {
                         reload={() => {
                           handleSearch(param);
                         }}
+                        update={(item, flag) => {
+                          if (flag) {
+                            PointModel.writeVisible = true;
+                          } else {
+                            if (item.provider === 'MODBUS_TCP') {
+                              PointModel.m_visible = true;
+                            } else {
+                              PointModel.p_visible = true;
+                            }
+                          }
+                          PointModel.current = item;
+                        }}
                       />
                     </Col>
                   ))}
@@ -225,6 +294,14 @@ export default observer((props: Props) => {
           </div>
         </div>
       </Card>
+    </div>
+  );
+});
+
+export default observer((props: Props) => {
+  return (
+    <div>
+      <PointCard {...props} reload={PointModel.reload} />
       {PointModel.m_visible && (
         <ModbusSave
           data={PointModel.current}
@@ -234,8 +311,20 @@ export default observer((props: Props) => {
           }}
           reload={() => {
             PointModel.m_visible = false;
-            handleSearch(param);
+            PointModel.reload = !PointModel.reload;
+          }}
+        />
+      )}
+      {PointModel.p_visible && (
+        <OpcSave
+          close={() => {
+            PointModel.p_visible = false;
+          }}
+          reload={() => {
+            PointModel.p_visible = false;
+            PointModel.reload = !PointModel.reload;
           }}
+          data={PointModel.current}
         />
       )}
       {PointModel.p_add_visible && (
@@ -246,7 +335,27 @@ export default observer((props: Props) => {
           collector={props.data}
           reload={() => {
             PointModel.p_add_visible = false;
-            handleSearch(param);
+            PointModel.reload = !PointModel.reload;
+          }}
+        />
+      )}
+      {PointModel.writeVisible && (
+        <WritePoint
+          data={PointModel.current}
+          onCancel={() => {
+            PointModel.writeVisible = false;
+          }}
+        />
+      )}
+      {PointModel.batch_visible && (
+        <BatchUpdate
+          close={() => {
+            PointModel.batch_visible = false;
+          }}
+          data={PointModel.list}
+          reload={() => {
+            PointModel.batch_visible = false;
+            PointModel.reload = !PointModel.reload;
           }}
         />
       )}

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

@@ -111,10 +111,10 @@ export default observer((props: Props) => {
                       </div>
                       <div>
                         <Space className={styles.iconColor}>
-                          <Tooltip title={!permission.edit ? '暂无权限,请联系管理员' : ''}>
+                          <Tooltip title={!permission.update ? '暂无权限,请联系管理员' : ''}>
                             <FormOutlined
                               onClick={() => {
-                                if (permission.edit) {
+                                if (permission.update) {
                                   TreeModel.current = item;
                                   TreeModel.visible = true;
                                 }

+ 99 - 2
src/pages/system/Menu/Setting/baseMenu.ts

@@ -1380,6 +1380,7 @@ export default [
                 url: '/iot/link/DataCollect/Dashboard',
                 icon: 'icon-shujumoni',
                 showPage: [
+                  'dashboard',
                   'data-collect-channel',
                   'data-collect-opc',
                   'data-collector',
@@ -1411,6 +1412,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['save'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save'],
+                      },
                     ],
                   },
                   {
@@ -1419,7 +1432,19 @@ export default [
                     permissions: [
                       {
                         permission: 'data-collect-channel',
-                        actions: ['add', 'query'],
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save', 'query'],
                       },
                     ],
                   },
@@ -1431,6 +1456,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['save', 'query'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save', 'query'],
+                      },
                     ],
                   },
                   {
@@ -1441,6 +1478,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['delete'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['delete'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['delete'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['delete'],
+                      },
                     ],
                   },
                 ],
@@ -1468,6 +1517,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['save'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save'],
+                      },
                     ],
                   },
                   {
@@ -1476,7 +1537,19 @@ export default [
                     permissions: [
                       {
                         permission: 'data-collect-channel',
-                        actions: ['add', 'query'],
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save', 'query'],
                       },
                     ],
                   },
@@ -1488,6 +1561,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['save', 'query'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['save', 'query'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['save', 'query'],
+                      },
                     ],
                   },
                   {
@@ -1498,6 +1583,18 @@ export default [
                         permission: 'data-collect-channel',
                         actions: ['delete'],
                       },
+                      {
+                        permission: 'data-collector',
+                        actions: ['delete'],
+                      },
+                      {
+                        permission: 'data-collect-opc',
+                        actions: ['delete'],
+                      },
+                      {
+                        permission: 'things-collector',
+                        actions: ['delete'],
+                      },
                     ],
                   },
                 ],