Przeglądaj źródła

feat: modbus映射

Wzyyy98 3 lat temu
rodzic
commit
38a8a2ba02

+ 6 - 0
src/pages/device/Instance/Detail/MapChannel/index.less

@@ -0,0 +1,6 @@
+.top-button {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  margin-bottom: 10px;
+}

+ 439 - 0
src/pages/device/Instance/Detail/MapChannel/index.tsx

@@ -0,0 +1,439 @@
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import { createForm, Field, FormPath, onFieldReact } from '@formily/core';
+import { FormProvider, createSchemaField } from '@formily/react';
+import { Badge, Button, Card, Empty, Tooltip } from 'antd';
+import { useEffect, useState } from 'react';
+import { FormItem, ArrayTable, Editable, Select } from '@formily/antd';
+import PermissionButton from '@/components/PermissionButton';
+import { DisconnectOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import Service from './service';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import './index.less';
+
+interface Props {
+  type: 'MODBUS_TCP' | 'OPC_UA';
+  data: any;
+}
+export const service = new Service();
+
+const MapChannel = (props: Props) => {
+  const { data, type } = props;
+  const { minHeight } = useDomFullHeight('.metadataMap');
+  const [empty, setEmpty] = useState<boolean>(false);
+  const [reload, setReload] = useState<string>('');
+  const [properties, setProperties] = useState<any>([]);
+  const [channelList, setChannelList] = useState<any>([]);
+
+  const Render = (propsName: any) => {
+    const text = properties.find((item: any) => item.metadataId === propsName.value);
+    return <>{text?.metadataName}</>;
+  };
+  const StatusRender = (propsRender: any) => {
+    if (propsRender.value) {
+      return <Badge status="success" text={'已绑定'} />;
+    } else {
+      return <Badge status="error" text={'未绑定'} />;
+    }
+  };
+  const ActionButton = () => {
+    const record = ArrayTable.useRecord?.();
+    const index = ArrayTable.useIndex?.();
+    return (
+      <PermissionButton
+        isPermission={true}
+        style={{ padding: 0 }}
+        disabled={!record(index)?.id}
+        tooltip={{
+          title: '解绑',
+        }}
+        popConfirm={{
+          title: '确认解绑',
+          disabled: !record(index)?.id,
+          onConfirm: async () => {
+            // remove(record(index)?.id);
+          },
+        }}
+        key="unbind"
+        type="link"
+      >
+        <DisconnectOutlined />
+      </PermissionButton>
+    );
+  };
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field)?.then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.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.getCollector({
+      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.getPoint({
+      terms: [
+        {
+          terms: [
+            {
+              column: 'collectorId',
+              value: collectorId,
+            },
+          ],
+        },
+      ],
+    });
+  };
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      ArrayTable,
+      Select,
+      Render,
+      ActionButton,
+      StatusRender,
+    },
+  });
+
+  const form = createForm({
+    values: {
+      requestList: properties,
+    },
+    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 schema = {
+    type: 'object',
+    properties: {
+      requestList: {
+        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': { width: 200, title: '名称' },
+              properties: {
+                metadataId: {
+                  type: 'string',
+                  'x-component': 'Render',
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { width: 200, title: '通道' },
+              properties: {
+                channelId: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Select',
+                  'x-component-props': {
+                    placeholder: '请选择',
+                    showSearch: true,
+                    allowClear: true,
+                    showArrow: true,
+                    filterOption: (input: string, option: any) =>
+                      option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+                  },
+                  enum: channelList,
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                width: 200,
+                title: (
+                  <>
+                    采集器
+                    <Tooltip title="边缘网关代理的真实物理设备">
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </>
+                ),
+              },
+              properties: {
+                collectorId: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Select',
+                  'x-component-props': {
+                    placeholder: '请选择',
+                    showSearch: true,
+                    allowClear: true,
+                    showArrow: true,
+                    filterOption: (input: string, option: any) =>
+                      option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+                  },
+                  'x-reactions': ['{{useAsyncDataSource(getCollector)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择采集器',
+                    },
+                  ],
+                },
+              },
+            },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { width: 200, title: '点位' },
+              properties: {
+                pointId: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Select',
+                  'x-component-props': {
+                    placeholder: '请选择',
+                    showSearch: true,
+                    allowClear: true,
+                    showArrow: true,
+                    filterOption: (input: string, option: any) =>
+                      option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+                  },
+                  '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',
+                },
+              },
+            },
+            column6: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                width: 100,
+                title: '状态',
+                // sorter: (a: any, b: any) => a.state.value.length - b.state.value.length,
+              },
+              properties: {
+                id: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'StatusRender',
+                },
+              },
+            },
+            column7: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '操作',
+                dataIndex: 'operations',
+                width: 50,
+                fixed: 'right',
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ActionButton',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  useEffect(() => {
+    service
+      .getChannel({
+        paging: false,
+        terms: [
+          {
+            terms: [
+              {
+                column: 'provider',
+                value: type,
+              },
+            ],
+          },
+        ],
+      })
+      .then((res) => {
+        if (res.status === 200) {
+          const list = res.result?.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+            provider: item.provider,
+          }));
+          setChannelList(list);
+        }
+      });
+  }, []);
+
+  useEffect(() => {
+    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('device', data.id).then((res) => {
+        if (res.status === 200) {
+          // console.log(res.result)
+          const array = res.result.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.removeDevicePoint(data.id, delList);
+          }
+        }
+      });
+    } else {
+      setEmpty(true);
+    }
+  }, [reload]);
+
+  return (
+    <Card className="metadataMap" style={{ minHeight }}>
+      {empty ? (
+        <Empty description={'暂无数据,请配置物模型'} style={{ marginTop: '10%' }} />
+      ) : (
+        <>
+          <div className="top-button">
+            <Button style={{ marginRight: 10 }} onClick={async () => {}}>
+              批量映射
+            </Button>
+            <Button
+              type="primary"
+              onClick={async () => {
+                setReload('');
+                console.log(type);
+                const value: any = await form.submit();
+                if (value) {
+                  console.log(value);
+                }
+              }}
+            >
+              保存
+            </Button>
+          </div>
+          <FormProvider form={form}>
+            <SchemaField schema={schema} scope={{ useAsyncDataSource, getCollector, getPoint }} />
+          </FormProvider>
+        </>
+      )}
+    </Card>
+  );
+};
+export default MapChannel;

+ 46 - 0
src/pages/device/Instance/Detail/MapChannel/service.ts

@@ -0,0 +1,46 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<any> {
+  getChannel = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/channel/_query/no-paging`, {
+      method: 'POST',
+      data,
+    });
+  getCollector = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/collector/_query/no-paging`, {
+      method: 'POST',
+      data,
+    });
+  getPoint = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/data-collect/point/_query/no-paging`, {
+      method: 'POST',
+      data,
+    });
+  getMap = (thingType: string, thingId: any, params?: any) =>
+    request(`/${SystemConst.API_BASE}/things/collector/${thingType}/${thingId}/_query`, {
+      method: 'GET',
+      params,
+    });
+  removeMap = (thingType: string, thingId: any, data?: any) =>
+    request(`/${SystemConst.API_BASE}/things/collector/${thingType}/${thingId}/_delete`, {
+      method: 'POST',
+      data,
+    });
+  treeMap = (deviceId: string, data?: any) =>
+    request(
+      `/${SystemConst.API_BASE}/edge/operations/${deviceId}/data-collector-channel-tree/invoke`,
+      {
+        method: 'POST',
+        data,
+      },
+    );
+  saveMap = (thingType: string, thingId: any, data?: any) =>
+    request(`/${SystemConst.API_BASE}/things/collector/${thingType}/${thingId}/{provider}`, {
+      method: 'PATCH',
+      data: data,
+    });
+}
+
+export default Service;

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

@@ -13,8 +13,6 @@ import Running from '@/pages/device/Instance/Detail/Running';
 import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
 import Diagnose from '@/pages/device/Instance/Detail/Diagnose';
 // import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
-import Opcua from '@/pages/device/Instance/Detail/Opcua';
-import Modbus from '@/pages/device/Instance/Detail/Modbus';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
@@ -30,7 +28,7 @@ import useLocation from '@/hooks/route/useLocation';
 import { onlyMessage } from '@/utils/util';
 import Parsing from './Parsing';
 import EdgeMap from './EdgeMap';
-// import EdgeMap from './EdgeMap';
+import MapChannel from './MapChannel';
 
 export const deviceStatus = new Map();
 deviceStatus.set('online', <Badge status="success" text={'在线'} />);
@@ -196,14 +194,14 @@ const InstanceDetail = observer(() => {
         datalist.push({
           key: 'modbus',
           tab: 'Modbus',
-          component: <Modbus data={InstanceModel.detail} />,
+          component: <MapChannel data={InstanceModel.detail} type="MODBUS_TCP" />,
         });
       }
       if (response.result.protocol === 'opc-ua') {
         datalist.push({
           key: 'opcua',
           tab: 'OPC UA',
-          component: <Opcua data={InstanceModel.detail} />,
+          component: <MapChannel data={InstanceModel.detail} type="OPC_UA" />,
         });
       }
       if (response.result.deviceType?.value === 'gateway') {