Procházet zdrojové kódy

feat: 边缘端映射及新增子设备

hear před 3 roky
rodič
revize
82042754c4

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