Quellcode durchsuchen

feat(merge): merge sc

feat: 告警日志
Lind vor 3 Jahren
Ursprung
Commit
90cff70142
39 geänderte Dateien mit 1701 neuen und 103 gelöschten Zeilen
  1. BIN
      public/images/alarm/background.png
  2. BIN
      public/images/alarm/device.png
  3. BIN
      public/images/alarm/log.png
  4. BIN
      public/images/alarm/org.png
  5. BIN
      public/images/alarm/other.png
  6. BIN
      public/images/alarm/product.png
  7. BIN
      public/images/metadata-map.png
  8. 13 13
      src/components/BaseCrud/index.tsx
  9. 5 5
      src/components/Metadata/ArrayParam/index.tsx
  10. 4 4
      src/components/PermissionButton/index.tsx
  11. 4 4
      src/components/ProTableCard/CardItems/AccessConfig/index.tsx
  12. 4 4
      src/hooks/permission/index.ts
  13. 8 8
      src/pages/device/Category/Save/index.tsx
  14. 4 4
      src/pages/device/Instance/Detail/Diagnose/index.tsx
  15. 12 8
      src/pages/device/Instance/Detail/Info/index.tsx
  16. 23 12
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  17. 160 0
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  18. 88 0
      src/pages/device/Instance/Detail/Reation/index.tsx
  19. 7 7
      src/pages/device/Instance/Save/index.tsx
  20. 14 0
      src/pages/device/Instance/service.ts
  21. 1 0
      src/pages/device/Instance/typings.d.ts
  22. 31 25
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  23. 1 1
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  24. 3 2
      src/pages/link/AccessConfig/service.ts
  25. 49 0
      src/pages/rule-engine/Alarm/Log/Detail/Info.tsx
  26. 133 0
      src/pages/rule-engine/Alarm/Log/Detail/index.tsx
  27. 53 0
      src/pages/rule-engine/Alarm/Log/SolveComponent/index.tsx
  28. 95 0
      src/pages/rule-engine/Alarm/Log/SolveLog/index.tsx
  29. 135 0
      src/pages/rule-engine/Alarm/Log/TabComponent/index.less
  30. 305 0
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  31. 19 5
      src/pages/rule-engine/Alarm/Log/index.tsx
  32. 32 1
      src/pages/rule-engine/Alarm/Log/model.ts
  33. 35 0
      src/pages/rule-engine/Alarm/Log/service.ts
  34. 38 0
      src/pages/rule-engine/Alarm/Log/typings.d.ts
  35. 251 0
      src/pages/system/Relationship/Save/index.tsx
  36. 146 0
      src/pages/system/Relationship/index.tsx
  37. 12 0
      src/pages/system/Relationship/service.ts
  38. 12 0
      src/pages/system/Relationship/typings.d.ts
  39. 4 0
      src/utils/menu/router.ts

BIN
public/images/alarm/background.png


BIN
public/images/alarm/device.png


BIN
public/images/alarm/log.png


BIN
public/images/alarm/org.png


BIN
public/images/alarm/other.png


BIN
public/images/alarm/product.png


BIN
public/images/metadata-map.png


+ 13 - 13
src/components/BaseCrud/index.tsx

@@ -1,22 +1,22 @@
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {Button, Tooltip} from 'antd';
-import type {ActionType, ProColumns, RequestData} from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Button, Tooltip } from 'antd';
+import type { ActionType, ProColumns, RequestData } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 
-import {PlusOutlined} from '@ant-design/icons';
+import { PlusOutlined } from '@ant-design/icons';
 import type BaseService from '@/utils/BaseService';
 import * as React from 'react';
-import {useRef, useState} from 'react';
+import { useRef, useState } from 'react';
 import Save from '@/components/BaseCrud/save';
-import type {ISchema} from '@formily/json-schema';
-import {CurdModel} from '@/components/BaseCrud/model';
-import type {ISchemaFieldProps} from '@formily/react/lib/types';
-import type {ModalProps} from 'antd/lib/modal/Modal';
-import type {TablePaginationConfig} from 'antd/lib/table/interface';
-import type {Form} from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import { CurdModel } from '@/components/BaseCrud/model';
+import type { ISchemaFieldProps } from '@formily/react/lib/types';
+import type { ModalProps } from 'antd/lib/modal/Modal';
+import type { TablePaginationConfig } from 'antd/lib/table/interface';
+import type { Form } from '@formily/core';
 import SearchComponent from '@/components/SearchComponent';
-import type {ProFormInstance} from '@ant-design/pro-form';
-import type {SearchConfig} from '@ant-design/pro-form/lib/components/Submitter';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import type { SearchConfig } from '@ant-design/pro-form/lib/components/Submitter';
 
 export type Option = {
   model: 'edit' | 'preview' | 'add';

+ 5 - 5
src/components/Metadata/ArrayParam/index.tsx

@@ -1,9 +1,9 @@
-import {createSchemaField} from '@formily/react';
-import {Editable, FormItem, FormLayout, Input, NumberPicker, Select} from '@formily/antd';
-import type {ISchema} from '@formily/json-schema';
+import { createSchemaField } from '@formily/react';
+import { Editable, FormItem, FormLayout, Input, NumberPicker, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
 import './index.less';
-import {DataTypeList, DateTypeList, FileTypeList} from '@/pages/device/data';
-import {Store} from 'jetlinks-store';
+import { DataTypeList, DateTypeList, FileTypeList } from '@/pages/device/data';
+import { Store } from 'jetlinks-store';
 import JsonParam from '@/components/Metadata/JsonParam';
 import EnumParam from '@/components/Metadata/EnumParam';
 import BooleanEnum from '@/components/Metadata/BooleanParam';

+ 4 - 4
src/components/PermissionButton/index.tsx

@@ -1,8 +1,8 @@
-import type {ButtonProps, PopconfirmProps, TooltipProps} from 'antd';
-import {Button, Popconfirm, Tooltip} from 'antd';
+import type { ButtonProps, PopconfirmProps, TooltipProps } from 'antd';
+import { Button, Popconfirm, Tooltip } from 'antd';
 import usePermissions from '@/hooks/permission';
-import {useCallback} from 'react';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { useCallback } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 interface PermissionButtonProps extends ButtonProps {
   tooltip?: TooltipProps;

+ 4 - 4
src/components/ProTableCard/CardItems/AccessConfig/index.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
-import {StatusColorEnum} from '@/components/BadgeStatus';
-import {TableCard} from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
 import '@/style/common.less';
-import {Badge, Tooltip} from 'antd';
-import type {AccessItem} from '@/pages/link/AccessConfig/typings';
+import { Badge, Tooltip } from 'antd';
+import type { AccessItem } from '@/pages/link/AccessConfig/typings';
 import './index.less';
 import classNames from 'classnames';
 

+ 4 - 4
src/hooks/permission/index.ts

@@ -1,7 +1,7 @@
-import {useEffect, useState} from 'react';
-import type {BUTTON_PERMISSION, MENUS_CODE_TYPE} from '@/utils/menu/router';
-import {BUTTON_PERMISSION_ENUM} from '@/utils/menu/router';
-import {MENUS_BUTTONS_CACHE} from '@/utils/menu';
+import { useEffect, useState } from 'react';
+import type { BUTTON_PERMISSION, MENUS_CODE_TYPE } from '@/utils/menu/router';
+import { BUTTON_PERMISSION_ENUM } from '@/utils/menu/router';
+import { MENUS_BUTTONS_CACHE } from '@/utils/menu';
 
 export type permissionKeyType = keyof typeof BUTTON_PERMISSION_ENUM;
 export type permissionType = Record<permissionKeyType, boolean>;

+ 8 - 8
src/pages/device/Category/Save/index.tsx

@@ -13,16 +13,16 @@ import {
   Upload,
 } from '@formily/antd';
 import React from 'react';
-import {createForm} from '@formily/core';
-import {createSchemaField} from '@formily/react';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
 import FUpload from '@/components/Upload';
 import * as ICONS from '@ant-design/icons';
-import {message, Modal} from 'antd';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import type {ISchema} from '@formily/json-schema';
-import type {CategoryItem} from '@/pages/visualization/Category/typings';
-import {service, state} from '@/pages/device/Category';
-import type {Response} from '@/utils/typings';
+import { message, Modal } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ISchema } from '@formily/json-schema';
+import type { CategoryItem } from '@/pages/visualization/Category/typings';
+import { service, state } from '@/pages/device/Category';
+import type { Response } from '@/utils/typings';
 
 interface Props {
   visible: boolean;

+ 4 - 4
src/pages/device/Instance/Detail/Diagnose/index.tsx

@@ -1,11 +1,11 @@
-import {Badge, Card, Col, Row} from 'antd';
-import type {ReactNode} from 'react';
-import {useEffect, useState} from 'react';
+import { Badge, Card, Col, Row } from 'antd';
+import type { ReactNode } from 'react';
+import { useEffect, useState } from 'react';
 import Message from './Message';
 import Status from './Status';
 import './index.less';
 import classNames from 'classnames';
-import {Store} from 'jetlinks-store';
+import { Store } from 'jetlinks-store';
 
 interface ListProps {
   key: string;

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

@@ -1,15 +1,16 @@
-import {Card, Descriptions} from 'antd';
-import {InstanceModel} from '@/pages/device/Instance';
+import { Card, Descriptions } from 'antd';
+import { InstanceModel } from '@/pages/device/Instance';
 import moment from 'moment';
-import {observer} from '@formily/react';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { observer } from '@formily/react';
+import { useIntl } from '@@/plugin-locale/localeExports';
 import Config from '@/pages/device/Instance/Detail/Config';
+import Reation from '@/pages/device/Instance/Detail/Reation';
 import Save from '../../Save';
-import {useState} from 'react';
-import type {DeviceInstance} from '../../typings';
-import {EditOutlined} from '@ant-design/icons';
+import { useState } from 'react';
+import type { DeviceInstance } from '../../typings';
+import { EditOutlined } from '@ant-design/icons';
 import Tags from '@/pages/device/Instance/Detail/Tags';
-import {PermissionButton} from '@/components';
+import { PermissionButton } from '@/components';
 
 const Info = observer(() => {
   const intl = useIntl();
@@ -115,6 +116,9 @@ const Info = observer(() => {
         </Descriptions>
         <Config />
         {InstanceModel.detail?.tags && InstanceModel.detail?.tags.length > 0 && <Tags />}
+        {InstanceModel.detail?.relations && InstanceModel.detail?.relations.length > 0 && (
+          <Reation />
+        )}
       </Card>
       <Save
         model={'edit'}

+ 23 - 12
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -27,6 +27,7 @@ interface EditableCellProps {
   dataIndex: string;
   record: any;
   list: any[];
+  properties: any[];
   handleSave: (record: any) => void;
 }
 
@@ -37,15 +38,17 @@ const EditableCell = ({
   dataIndex,
   record,
   list,
+  properties,
   handleSave,
   ...restProps
 }: EditableCellProps) => {
   const form: any = useContext(EditableContext);
+  const [temp, setTemp] = useState<any>({});
 
   const save = async () => {
     try {
       const values = await form.validateFields();
-      handleSave({ ...record, metadataId: values?.metadataId });
+      handleSave({ ...record, originalId: values?.originalId });
     } catch (errInfo) {
       console.log('Save failed:', errInfo);
     }
@@ -54,6 +57,7 @@ const EditableCell = ({
   useEffect(() => {
     if (record) {
       form.setFieldsValue({ [dataIndex]: record[dataIndex] });
+      setTemp(properties.find((i) => i.id === record.originalId));
     }
   }, [record]);
 
@@ -71,6 +75,11 @@ const EditableCell = ({
           }
         >
           <Select.Option value={record.metadataId}>使用原始属性</Select.Option>
+          {record.originalId !== record.metadataId && (
+            <Select.Option value={record.originalId}>
+              {temp?.name}({temp?.id})
+            </Select.Option>
+          )}
           {list.length > 0 &&
             list.map((item: any) => (
               <Select.Option key={item?.id} value={item?.id}>
@@ -98,7 +107,7 @@ const EditableTable = (props: Props) => {
     },
     {
       title: '设备上报属性',
-      dataIndex: 'metadataId',
+      dataIndex: 'originalId',
       width: '30%',
       editable: true,
     },
@@ -136,7 +145,7 @@ const EditableTable = (props: Props) => {
     },
   };
 
-  const initData = async () => {
+  const initData = async (lists: any[]) => {
     let resp = null;
     if (props.type === 'device') {
       resp = await service.queryDeviceMetadata(props.data.id);
@@ -147,10 +156,10 @@ const EditableTable = (props: Props) => {
       const data = resp.result;
       const obj: any = {};
       data.map((i: any) => {
-        obj[i?.originalId] = i;
+        obj[i?.metadataId] = i;
       });
-      if (protocolMetadata.length > 0) {
-        setPmList(protocolMetadata.filter((i) => !_.map(data, 'metadataId').includes(i.id)));
+      if (lists.length > 0) {
+        setPmList(lists.filter((i) => !_.map(data, 'originalId').includes(i.id)));
       } else {
         setPmList([]);
       }
@@ -184,8 +193,9 @@ const EditableTable = (props: Props) => {
         )
         .then((resp) => {
           if (resp.status === 200) {
-            setProtocolMetadata(JSON.parse(resp.result || '{}')?.properties || []);
-            initData();
+            const list = JSON.parse(resp.result || '{}')?.properties || [];
+            setProtocolMetadata(list);
+            initData(list);
           }
         });
     }
@@ -195,21 +205,21 @@ const EditableTable = (props: Props) => {
     const newData = [...dataSource.data];
     const index = newData.findIndex((item) => row.id === item.id);
     const item = newData[index];
-    if (item?.metadataId !== row?.metadataId) {
+    if (item?.originalId !== row?.originalId) {
       const resp = await service[
         props.type === 'device' ? 'saveDeviceMetadata' : 'saveProductMetadata'
       ](props.data?.id, [
         {
           metadataType: 'property',
-          metadataId: row.metadataId === row.id ? row.metadataId : row.id,
-          originalId: row.metadataId === row.id ? row.id : '',
+          metadataId: row.metadataId,
+          originalId: row.metadataId !== row.originalId ? row.originalId : '',
           others: {},
         },
       ]);
       if (resp.status === 200) {
         message.success('操作成功!');
         // 刷新
-        initData();
+        initData(protocolMetadata);
       }
     }
   };
@@ -253,6 +263,7 @@ const EditableTable = (props: Props) => {
         dataIndex: col.dataIndex,
         title: col.title,
         list: pmList,
+        properties: protocolMetadata,
         handleSave: handleSave,
       }),
     };

+ 160 - 0
src/pages/device/Instance/Detail/Reation/Edit.tsx

@@ -0,0 +1,160 @@
+import type { Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import type { ISchema } from '@formily/json-schema';
+import { Form, FormGrid, FormItem, Select, PreviewText } from '@formily/antd';
+import { useParams } from 'umi';
+import { Button, Drawer, message, Space } from 'antd';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  close: () => void;
+  data: any[];
+}
+
+const Edit = (props: Props) => {
+  const { data } = props;
+  const params = useParams<{ id: string }>();
+  const id = InstanceModel.detail?.id || params?.id;
+  const [initData, setInitData] = useState<any>({});
+
+  const getUsers = () => service.queryUserListNopaging();
+
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field).then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
+          ...item,
+          label: item.name,
+          value: JSON.stringify({
+            id: item.id,
+            name: item.name,
+          }),
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: initData,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Select,
+      FormGrid,
+      PreviewText,
+    },
+  });
+
+  const configToSchema = (list: any[]) => {
+    const config = {};
+    list.forEach((item) => {
+      config[item.relation] = {
+        type: 'string',
+        title: item.relationName,
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          placeholder: '请选择关联方',
+          showSearch: true,
+          showArrow: true,
+          mode: 'multiple',
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
+        'x-reactions': ['{{useAsyncDataSource(getUsers)}}'],
+      };
+    });
+    return config;
+  };
+
+  const renderConfigCard = () => {
+    const itemSchema: ISchema = {
+      type: 'object',
+      properties: {
+        grid: {
+          type: 'void',
+          'x-component': 'FormGrid',
+          'x-component-props': {
+            minColumns: [1],
+            maxColumns: [1],
+          },
+          properties: configToSchema(data),
+        },
+      },
+    };
+
+    return (
+      <>
+        <PreviewText.Placeholder value="-">
+          <Form form={form} layout="vertical">
+            <SchemaField schema={itemSchema} scope={{ useAsyncDataSource, getUsers }} />
+          </Form>
+        </PreviewText.Placeholder>
+      </>
+    );
+  };
+
+  useEffect(() => {
+    const obj: any = {};
+    (props?.data || []).map((item: any) => {
+      obj[item.relation] = [...(item?.related || []).map((i: any) => JSON.stringify(i))];
+    });
+    setInitData(obj);
+  }, [props.data]);
+
+  return (
+    <Drawer
+      title="编辑"
+      placement="right"
+      onClose={() => {
+        props.close();
+      }}
+      visible
+      extra={
+        <Space>
+          <Button
+            type="primary"
+            onClick={async () => {
+              const values = (await form.submit()) as any;
+              if (Object.keys(values).length > 0) {
+                const param: any[] = [];
+                Object.keys(values).forEach((key) => {
+                  const item = data.find((i) => i.relation === key);
+                  const items = (values[key] || []).map((i: string) => JSON.parse(i));
+                  if (item) {
+                    param.push({
+                      relatedType: 'user',
+                      relation: item.relation,
+                      description: '',
+                      related: [...items],
+                    });
+                  }
+                });
+                const resp = await service.saveRelations(id || '', param);
+                if (resp.status === 200) {
+                  message.success('操作成功!');
+                  props.close();
+                }
+              }
+            }}
+          >
+            保存
+          </Button>
+        </Space>
+      }
+    >
+      {renderConfigCard()}
+    </Drawer>
+  );
+};
+
+export default Edit;

+ 88 - 0
src/pages/device/Instance/Detail/Reation/index.tsx

@@ -0,0 +1,88 @@
+import { Descriptions, Tooltip } from 'antd';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
+import { history, useParams } from 'umi';
+import { EditOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import Edit from './Edit';
+import { PermissionButton } from '@/components';
+import _ from 'lodash';
+
+const Reation = () => {
+  const params = useParams<{ id: string }>();
+  useEffect(() => {
+    const id = InstanceModel.current?.id || params.id;
+    if (id) {
+      service.getConfigMetadata(id).then((response) => {
+        InstanceModel.config = response?.result;
+      });
+    } else {
+      history.goBack();
+    }
+  }, []);
+
+  const [data, setData] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+  const { permission } = PermissionButton.usePermission('device/Instance');
+
+  const id = InstanceModel.detail?.id || params?.id;
+
+  const getDetail = () => {
+    service.detail(id || '').then((resp) => {
+      if (resp.status === 200) {
+        InstanceModel.detail = { id, ...resp.result };
+      }
+    });
+  };
+
+  useEffect(() => {
+    if (id) {
+      setData(InstanceModel.detail?.relations || []);
+    }
+  }, [id]);
+
+  return (
+    <div style={{ width: '100%', marginTop: '20px' }}>
+      <Descriptions
+        style={{ marginBottom: 20 }}
+        bordered
+        column={3}
+        size="small"
+        title={
+          <span>
+            关系信息
+            <PermissionButton
+              isPermission={permission.update}
+              type="link"
+              onClick={async () => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+              <Tooltip title={`管理设备与其他业务的关联关系,关系来源于关系配置`}>
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </PermissionButton>
+          </span>
+        }
+      >
+        {(data || [])?.map((item: any) => (
+          <Descriptions.Item span={1} label={item.relationName} key={item.objectId}>
+            {_.map(item?.related || [], 'name').join(',')}
+          </Descriptions.Item>
+        ))}
+      </Descriptions>
+      {visible && (
+        <Edit
+          data={data || []}
+          close={() => {
+            setVisible(false);
+            getDetail();
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+export default Reation;

+ 7 - 7
src/pages/device/Instance/Save/index.tsx

@@ -1,10 +1,10 @@
-import {Col, Form, Input, message, Modal, Row, Select} from 'antd';
-import {service} from '@/pages/device/Instance';
-import type {DeviceInstance} from '../typings';
-import {useEffect, useState} from 'react';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {UploadImage} from '@/components';
-import {debounce} from 'lodash';
+import { Col, Form, Input, message, Modal, Row, Select } from 'antd';
+import { service } from '@/pages/device/Instance';
+import type { DeviceInstance } from '../typings';
+import { useEffect, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { UploadImage } from '@/components';
+import { debounce } from 'lodash';
 
 interface Props {
   visible: boolean;

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

@@ -251,6 +251,20 @@ class Service extends BaseService<DeviceInstance> {
 
   //接入方式
   public queryGatewayList = () => request(`/${SystemConst.API_BASE}/gateway/device/providers`);
+  // 保存设备关系
+  public saveRelations = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device/instance/${id}/relations`, {
+      method: 'PATCH',
+      data,
+    });
+  // 查询用户
+  public queryUserListNopaging = () =>
+    request(`/${SystemConst.API_BASE}/user/_query/no-paging`, {
+      method: 'POST',
+      data: {
+        paging: false,
+      },
+    });
 }
 
 export default Service;

+ 1 - 0
src/pages/device/Instance/typings.d.ts

@@ -29,6 +29,7 @@ export type DeviceInstance = {
   orgId: string;
   orgName: string;
   configuration: Record<string, any>;
+  relations?: any[];
   cachedConfiguration: any;
   transport: string;
   protocol: string;

+ 31 - 25
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -42,6 +42,7 @@ const Access = (props: Props) => {
   const [config, setConfig] = useState<any>();
   const networkPermission = PermissionButton.usePermission('link/Type').permission;
   const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+  const [steps, setSteps] = useState<string[]>(['网络组件', '消息协议', '完成']);
 
   const MetworkTypeMapping = new Map();
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
@@ -69,7 +70,7 @@ const Access = (props: Props) => {
     });
   };
 
-  const queryProcotolList = (id: string, params?: any) => {
+  const queryProcotolList = (id?: string, params?: any) => {
     service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
       if (resp.status === 200) {
         setProcotolList(resp.result);
@@ -79,25 +80,39 @@ const Access = (props: Props) => {
 
   useEffect(() => {
     if (props.provider?.id && !props.data?.id) {
-      queryNetworkList(props.provider?.id, {
-        include: networkCurrent || '',
-      });
-      setCurrent(0);
+      if (props.provider?.id !== 'child-device') {
+        setSteps(['网络组件', '消息协议', '完成']);
+        queryNetworkList(props.provider?.id, {
+          include: networkCurrent || '',
+        });
+        setCurrent(0);
+      } else {
+        setSteps(['消息协议', '完成']);
+        setCurrent(1);
+        queryProcotolList(props.provider?.id);
+      }
     }
   }, [props.provider]);
 
   useEffect(() => {
     if (props.data?.id) {
       setProcotolCurrent(props.data?.protocol);
-      setNetworkCurrent(props.data?.channelId);
       form.setFieldsValue({
         name: props.data?.name,
         description: props.data?.description,
       });
-      setCurrent(0);
-      queryNetworkList(props.data?.provider, {
-        include: props.data?.channelId,
-      });
+      if (props.data?.provider !== 'child-device') {
+        setCurrent(0);
+        setSteps(['网络组件', '消息协议', '完成']);
+        setNetworkCurrent(props.data?.channelId);
+        queryNetworkList(props.data?.provider, {
+          include: props.data?.channelId,
+        });
+      } else {
+        setSteps(['消息协议', '完成']);
+        setCurrent(1);
+        queryProcotolList(props.data?.provider);
+      }
     }
   }, [props.data]);
 
@@ -130,18 +145,6 @@ const Access = (props: Props) => {
     setCurrent(current - 1);
   };
 
-  const steps = [
-    {
-      title: '网络组件',
-    },
-    {
-      title: '消息协议',
-    },
-    {
-      title: '完成',
-    },
-  ];
-
   const columnsMQTT: any[] = [
     {
       title: '分组',
@@ -525,7 +528,10 @@ const Access = (props: Props) => {
                               description: values.description,
                               provider: props.provider.id,
                               protocol: procotolCurrent,
-                              transport: ProcotoleMapping.get(props.provider.id),
+                              transport:
+                                props.provider?.id === 'child-device'
+                                  ? 'Gateway'
+                                  : ProcotoleMapping.get(props.provider.id),
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                             })
@@ -648,13 +654,13 @@ const Access = (props: Props) => {
         <div className={styles.steps}>
           <Steps size="small" current={current}>
             {steps.map((item) => (
-              <Steps.Step key={item.title} title={item.title} />
+              <Steps.Step key={item} title={item} />
             ))}
           </Steps>
         </div>
         <div className={styles.content}>{renderSteps(current)}</div>
         <div className={styles.action}>
-          {current === 1 && (
+          {current === 1 && props.provider.id !== 'child-device' && (
             <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
               上一步
             </Button>

+ 1 - 1
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -50,7 +50,7 @@ const Provider = (props: Props) => {
                     <div className={styles.images}>{item.name}</div>
                     <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
                       <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description}</div>
+                      <div className={styles.desc}>{item.description || '--'}</div>
                     </div>
                   </div>
                   <div style={{ width: '70px' }}>

+ 3 - 2
src/pages/link/AccessConfig/service.ts

@@ -31,11 +31,12 @@ class Service extends BaseService<AccessItem> {
       method: 'GET',
       params,
     });
-  public getProtocolList = (transport: string, params?: any) =>
-    request(`/${SystemConst.API_BASE}/protocol/supports/${transport}`, {
+  public getProtocolList = (transport?: string, params?: any) => {
+    return request(`/${SystemConst.API_BASE}/protocol/supports/${transport ? transport : ''}`, {
       method: 'GET',
       params,
     });
+  };
   public getConfigView = (id: string, transport: string) =>
     request(`/${SystemConst.API_BASE}/protocol/${id}/transport/${transport}`, {
       method: 'GET',

+ 49 - 0
src/pages/rule-engine/Alarm/Log/Detail/Info.tsx

@@ -0,0 +1,49 @@
+import { Descriptions, Modal } from 'antd';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<AlarmLogHistoryItem>;
+  close: () => void;
+}
+
+const Info = (props: Props) => {
+  const [data, setDada] = useState<Partial<AlarmLogHistoryItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Descriptions bordered column={2}>
+        {data.targetType === 'device' && (
+          <>
+            <Descriptions.Item label="告警设备" span={1}>
+              {data?.targetName}
+            </Descriptions.Item>
+            <Descriptions.Item label="设备ID" span={1}>
+              {data?.targetId}
+            </Descriptions.Item>
+          </>
+        )}
+        <Descriptions.Item label="告警名称" span={1}>
+          {data?.alarmConfigName}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警时间" span={1}>
+          {moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警级别" span={1}>
+          {data?.level}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警说明" span={1}>
+          {data?.description}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警流水" span={2}>
+          {data?.alarmInfo}
+        </Descriptions.Item>
+      </Descriptions>
+    </Modal>
+  );
+};
+export default Info;

+ 133 - 0
src/pages/rule-engine/Alarm/Log/Detail/index.tsx

@@ -0,0 +1,133 @@
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '@/pages/rule-engine/Alarm/Log';
+import { PageContainer } from '@ant-design/pro-layout';
+import { useParams } from 'umi';
+import { AlarmLogModel } from '../model';
+import { observer } from '@formily/reactive-react';
+import { SearchOutlined } from '@ant-design/icons';
+import Info from './Info';
+import { Button } from 'antd';
+import moment from 'moment';
+
+const Detail = observer(() => {
+  const params = useParams<{ id: string }>();
+
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<AlarmLogHistoryItem>>({});
+
+  const [param, setParam] = useState<any>({
+    terms: [
+      {
+        column: 'alarmRecordId',
+        termType: 'eq$not',
+        value: params.id || AlarmLogModel.current?.id,
+        type: 'and',
+      },
+    ],
+    sorts: [
+      {
+        name: 'name',
+        order: 'asc',
+      },
+    ],
+  });
+  const actionRef = useRef<ActionType>();
+
+  const initColumns: ProColumns<AlarmLogHistoryItem>[] = [
+    {
+      dataIndex: 'alarmTime',
+      title: '告警时间',
+      render: (text: any) => <span>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</span>,
+    },
+    {
+      dataIndex: 'alarmConfigName',
+      title: '告警名称',
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+      render: (record: any) => (
+        <Button type="link">
+          <SearchOutlined
+            onClick={() => {
+              setVisible(true);
+              setCurrent(record);
+            }}
+          />
+        </Button>
+      ),
+    },
+  ];
+
+  useEffect(() => {
+    service.detail(params.id).then((resp) => {
+      if (resp.status === 200) {
+        AlarmLogModel.current = resp.result;
+        if (resp.result.targetType === 'device') {
+          initColumns.splice(2, 0, {
+            dataIndex: 'targetName',
+            title: '告警设备',
+          });
+        }
+        AlarmLogModel.columns = initColumns;
+      }
+    });
+  }, [params.id]);
+
+  return (
+    <PageContainer>
+      <SearchComponent<AlarmLogHistoryItem>
+        field={AlarmLogModel.columns}
+        target="alarm-log-detail"
+        enableSave={false}
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          const terms = [
+            {
+              column: 'alarmRecordId',
+              termType: 'eq$not',
+              value: params.id || AlarmLogModel.current?.id,
+              type: 'and',
+            },
+          ];
+          setParam({
+            ...param,
+            terms: data?.terms ? [...data?.terms, ...terms] : [...terms],
+          });
+        }}
+      />
+      <ProTable<AlarmLogHistoryItem>
+        actionRef={actionRef}
+        params={param}
+        columns={AlarmLogModel.columns}
+        search={false}
+        headerTitle={'记录列表'}
+        request={async (data) => {
+          return service.queryHistoryList({
+            ...data,
+            sorts: [{ name: 'alarmTime', order: 'desc' }],
+          });
+        }}
+        rowKey="id"
+      />
+      {visible && (
+        <Info
+          close={() => {
+            setVisible(false);
+            setCurrent({});
+          }}
+          data={current}
+        />
+      )}
+    </PageContainer>
+  );
+});
+
+export default Detail;

+ 53 - 0
src/pages/rule-engine/Alarm/Log/SolveComponent/index.tsx

@@ -0,0 +1,53 @@
+import { service } from '@/pages/rule-engine/Alarm/Log';
+import { Form, Input, message, Modal } from 'antd';
+
+interface Props {
+  close: () => void;
+  reload: () => void;
+  data: Partial<AlarmLogItem>;
+}
+
+const SolveComponent = (props: Props) => {
+  const { data } = props;
+  const [form] = Form.useForm();
+
+  return (
+    <Modal
+      title="告警处理"
+      visible
+      onOk={form.submit}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        name="basic"
+        layout="vertical"
+        form={form}
+        onFinish={async (values: any) => {
+          const resp = await service.handleLog(data?.id || '', {
+            describe: values.describe,
+            type: 'user',
+            state: 'normal',
+          });
+          if (resp.status === 200) {
+            message.success('操作成功!');
+            props.reload();
+          } else {
+            message.error('操作失败!');
+          }
+        }}
+      >
+        <Form.Item
+          label="处理结果"
+          name="describe"
+          rules={[{ required: true, message: '请输入处理结果!' }]}
+        >
+          <Input.TextArea showCount maxLength={200} rows={8} placeholder="请输入处理结果" />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};
+
+export default SolveComponent;

+ 95 - 0
src/pages/rule-engine/Alarm/Log/SolveLog/index.tsx

@@ -0,0 +1,95 @@
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Modal } from 'antd';
+import { useRef, useState } from 'react';
+import { service } from '@/pages/rule-engine/Alarm/Log';
+
+interface Props {
+  data: Partial<AlarmLogItem>;
+  close: () => void;
+}
+
+const typeMap = new Map();
+typeMap.set('system', '系统');
+typeMap.set('user', '人工');
+
+const SolveLog = (props: Props) => {
+  const [param, setParam] = useState<any>({
+    terms: [
+      {
+        column: 'alarmRecordId',
+        termType: 'eq',
+        value: props.data.id,
+        type: 'and',
+      },
+    ],
+    sorts: [
+      {
+        name: 'createTime',
+        order: 'desc',
+      },
+    ],
+  });
+  const actionRef = useRef<ActionType>();
+
+  const columns: ProColumns<AlarmLogSolveHistoryItem>[] = [
+    {
+      dataIndex: 'createTime',
+      title: '处理时间',
+    },
+    {
+      dataIndex: 'handleType',
+      title: '处理类型',
+      render: (text: any) => <span>{typeMap.get(text) || ''}</span>,
+    },
+    {
+      dataIndex: 'address',
+      title: '告警时间',
+    },
+    {
+      dataIndex: 'description',
+      title: '告警处理',
+    },
+  ];
+
+  return (
+    <Modal title={'处理记录'} visible onCancel={props.close} onOk={() => {}} width={1200}>
+      <SearchComponent<AlarmLogSolveHistoryItem>
+        field={columns}
+        target="bind-channel"
+        enableSave={false}
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          const terms = [
+            {
+              column: 'alarmRecordId',
+              termType: 'eq',
+              value: props.data.id,
+              type: 'and',
+            },
+          ];
+          setParam({
+            ...param,
+            terms: data?.terms ? [...data?.terms, ...terms] : [...terms],
+          });
+        }}
+      />
+      <ProTable<AlarmLogSolveHistoryItem>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        headerTitle={'记录列表'}
+        request={async (params) => {
+          return service.queryHandleHistory({
+            ...params,
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          });
+        }}
+        rowKey="id"
+      />
+    </Modal>
+  );
+};
+export default SolveLog;

+ 135 - 0
src/pages/rule-engine/Alarm/Log/TabComponent/index.less

@@ -0,0 +1,135 @@
+@import '~antd/es/style/themes/default.less';
+
+.ellipsis {
+  width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.alarm-log-card {
+  .alarm-log-item {
+    display: flex;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.1);
+
+    .alarm-log-title {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 15%;
+      padding: 10px;
+      overflow: hidden;
+      color: @primary-color;
+      background-color: #f0f2f3;
+      .alarm-log-level {
+        position: absolute;
+        top: 10px;
+        right: -12px;
+        display: flex;
+        justify-content: center;
+        width: 100px;
+        padding: 2px 0;
+        color: white;
+        background-color: red;
+        transform: skewX(45deg);
+        .alarm-log-text {
+          transform: skewX(-45deg);
+        }
+      }
+      .alarm-log-title-text {
+        margin: 0 10px;
+        .ellipsis();
+      }
+    }
+    .alarm-log-content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 87%;
+      padding: 20px;
+      background: url('/images/alarm/background.png') no-repeat;
+      background-size: 100% 100%;
+
+      .alarm-log-image {
+        display: flex;
+        align-items: center;
+
+        .alarm-type {
+          max-width: 120px;
+          padding-right: 50px;
+          border-right: 1px solid rgba(0, 0, 0, 0.09);
+          .name {
+            color: #000;
+            font-size: 18px;
+          }
+
+          .text {
+            margin-top: 8px;
+            color: #666;
+            font-size: 14px;
+            .ellipsis();
+          }
+        }
+        .alarm-log-right {
+          display: flex;
+          padding-left: 40px;
+          .alarm-log-time {
+            max-width: 165px;
+            margin: 0 10px;
+            .log-title {
+              margin-top: 8px;
+              color: #666;
+              font-size: 12px;
+            }
+
+            .context {
+              margin-top: 8px;
+              color: rgba(#000, 0.85);
+              font-size: 14px;
+              .ellipsis();
+            }
+          }
+        }
+      }
+
+      .alarm-log-actions {
+        .alarm-log-action {
+          display: flex;
+          justify-content: center;
+          width: 72px;
+          height: 72px;
+          background-color: #fff;
+          border: 1px solid @primary-color;
+
+          .btn {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            width: 72px;
+            height: 72px;
+            .icon {
+              margin-bottom: 5px;
+              color: @primary-color;
+              font-size: 25px;
+            }
+
+            div {
+              color: @primary-color;
+              font-size: 12px;
+            }
+          }
+        }
+        .alarm-log-action:hover {
+          background-color: @primary-color;
+          .icon,
+          div {
+            color: #fff;
+          }
+        }
+      }
+    }
+  }
+}

+ 305 - 0
src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx

@@ -0,0 +1,305 @@
+import SearchComponent from '@/components/SearchComponent';
+import { FileFilled, FileTextFilled, ToolFilled } from '@ant-design/icons';
+import type { ProColumns } from '@jetlinks/pro-table';
+import { Badge, Button, Card, Col, Empty, Pagination, Row, Space } from 'antd';
+import { useEffect, useState } from 'react';
+import './index.less';
+import SolveComponent from '../SolveComponent';
+import SolveLog from '../SolveLog';
+import { AlarmLogModel } from '../model';
+import moment from 'moment';
+import { observer } from '@formily/reactive-react';
+import { service } from '@/pages/rule-engine/Alarm/Log';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
+
+interface Props {
+  type: string;
+}
+
+const imgMap = new Map();
+imgMap.set('product', require('/public/images/alarm/product.png'));
+imgMap.set('device', require('/public/images/alarm/device.png'));
+imgMap.set('other', require('/public/images/alarm/other.png'));
+imgMap.set('org', require('/public/images/alarm/org.png'));
+
+const titleMap = new Map();
+titleMap.set('product', '产品');
+titleMap.set('device', '设备');
+titleMap.set('other', '其他');
+titleMap.set('org', '部门');
+
+const colorMap = new Map();
+colorMap.set(1, '#E50012');
+colorMap.set(2, '#FF9457');
+colorMap.set(3, '#FABD47');
+colorMap.set(4, '#999999');
+colorMap.set(5, '#C4C4C4');
+
+const TabComponent = observer((props: Props) => {
+  const columns: ProColumns<any>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '级别',
+      dataIndex: 'level',
+      valueType: 'select',
+      valueEnum: {
+        1: {
+          text: '级别一',
+          status: '1',
+        },
+        2: {
+          text: '级别二',
+          status: '2',
+        },
+        3: {
+          text: '级别三',
+          status: '3',
+        },
+        4: {
+          text: '级别四',
+          status: '4',
+        },
+        5: {
+          text: '级别五',
+          status: '5',
+        },
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        warning: {
+          text: '告警中',
+          status: 'warning',
+        },
+        normal: {
+          text: '无告警',
+          status: 'normal',
+        },
+      },
+    },
+  ];
+
+  const [param, setParam] = useState<any>({ pageSize: 10, terms: [] });
+  const history = useHistory<Record<string, string>>();
+
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const handleSearch = (params: any) => {
+    setParam(params);
+    service
+      .query({
+        ...params,
+        terms: [
+          ...params.terms,
+          {
+            termType: 'eq',
+            column: 'targetType',
+            value: props.type,
+            type: 'and',
+          },
+        ],
+        sorts: [{ name: 'alarmDate', order: 'desc' }],
+      })
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, [props.type]);
+
+  return (
+    <div className="alarm-log-card">
+      <SearchComponent<any>
+        field={columns}
+        target="alarm-log"
+        onSearch={(data) => {
+          const dt = {
+            pageSize: 10,
+            terms: [...data?.terms],
+          };
+          handleSearch(dt);
+        }}
+      />
+      <Card>
+        {dataSource?.data.length > 0 ? (
+          <Row gutter={24} style={{ marginTop: 10 }}>
+            {(dataSource?.data || []).map((item: any) => (
+              <Col key={item.id} span={24}>
+                <div className="alarm-log-item">
+                  <div className="alarm-log-title">
+                    <div
+                      className="alarm-log-level"
+                      style={{ backgroundColor: colorMap.get(item.level) }}
+                    >
+                      <div className="alarm-log-text">
+                        {AlarmLogModel.defaultLevel.find((i) => i.level === item.level)?.title ||
+                          item.level}
+                      </div>
+                    </div>
+                    <div className="alarm-log-title-text">{item.name}</div>
+                  </div>
+                  <div className="alarm-log-content">
+                    <div className="alarm-log-image">
+                      <img
+                        width={88}
+                        height={88}
+                        src={imgMap.get(props.type)}
+                        alt={''}
+                        style={{ marginRight: 20 }}
+                      />
+                      <div className="alarm-type">
+                        <div className="name">{titleMap.get(item.targetType)}</div>
+                        <div className="text">{item.targetName}</div>
+                      </div>
+                      <div className="alarm-log-right">
+                        <div className="alarm-log-time">
+                          <div className="log-title">最近告警时间</div>
+                          <div className="context">
+                            {moment(item.alarmDate).format('YYYY-MM-DD HH:mm:ss')}
+                          </div>
+                        </div>
+                        <div className="alarm-log-time" style={{ paddingLeft: 10 }}>
+                          <div className="log-title">状态</div>
+                          <div className="context">
+                            <Badge status={item.state.value === 'warning' ? 'error' : 'default'} />
+                            <span
+                              style={{
+                                color: item.state.value === 'warning' ? '#E50012' : 'black',
+                              }}
+                            >
+                              {item.state.text}
+                            </span>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                    <div className="alarm-log-actions">
+                      <Space>
+                        {item.state.value === 'warning' && (
+                          <div className="alarm-log-action">
+                            <Button
+                              type={'link'}
+                              onClick={() => {
+                                AlarmLogModel.solveVisible = true;
+                                AlarmLogModel.current = item;
+                              }}
+                            >
+                              <div className="btn">
+                                <ToolFilled className="icon" />
+                                <div>告警处理</div>
+                              </div>
+                            </Button>
+                          </div>
+                        )}
+                        <div className="alarm-log-action">
+                          <Button
+                            type={'link'}
+                            onClick={() => {
+                              AlarmLogModel.current = item;
+                              const url = getMenuPathByParams(
+                                MENUS_CODE['rule-engine/Alarm/Log/Detail'],
+                                item.id,
+                              );
+                              history.push(url);
+                            }}
+                          >
+                            <div className="btn">
+                              <FileFilled className="icon" />
+                              <div>告警日志</div>
+                            </div>
+                          </Button>
+                        </div>
+                        <div className="alarm-log-action">
+                          <Button
+                            type={'link'}
+                            onClick={() => {
+                              AlarmLogModel.logVisible = true;
+                              AlarmLogModel.current = item;
+                            }}
+                          >
+                            <div className="btn">
+                              <FileTextFilled className="icon" />
+                              <div>处理记录</div>
+                            </div>
+                          </Button>
+                        </div>
+                      </Space>
+                    </div>
+                  </div>
+                </div>
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Empty />
+        )}
+        {dataSource.data.length > 0 && (
+          <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
+            <Pagination
+              showSizeChanger
+              size="small"
+              className={'pro-table-card-pagination'}
+              total={dataSource?.total || 0}
+              current={dataSource?.pageIndex + 1}
+              onChange={(page, size) => {
+                handleSearch({
+                  ...param,
+                  pageIndex: page - 1,
+                  pageSize: size,
+                });
+              }}
+              pageSizeOptions={[10, 20, 50, 100]}
+              pageSize={dataSource?.pageSize}
+              showTotal={(num) => {
+                const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+              }}
+            />
+          </div>
+        )}
+      </Card>
+      {AlarmLogModel.solveVisible && (
+        <SolveComponent
+          close={() => {
+            AlarmLogModel.solveVisible = false;
+            AlarmLogModel.current = {};
+          }}
+          reload={() => {
+            AlarmLogModel.solveVisible = false;
+            AlarmLogModel.current = {};
+            handleSearch(param);
+          }}
+          data={AlarmLogModel.current}
+        />
+      )}
+      {AlarmLogModel.logVisible && (
+        <SolveLog
+          close={() => {
+            AlarmLogModel.logVisible = false;
+            AlarmLogModel.current = {};
+          }}
+          data={AlarmLogModel.current}
+        />
+      )}
+    </div>
+  );
+});
+
+export default TabComponent;

+ 19 - 5
src/pages/rule-engine/Alarm/Log/index.tsx

@@ -1,6 +1,11 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { observer } from '@formily/reactive-react';
+import { useEffect } from 'react';
 import { AlarmLogModel } from './model';
+import TabComponent from './TabComponent';
+import Service from './service';
+
+export const service = new Service('alarm/record');
 
 const Log = observer(() => {
   const list = [
@@ -13,7 +18,7 @@ const Log = observer(() => {
       tab: '设备',
     },
     {
-      key: 'department',
+      key: 'org',
       tab: '部门',
     },
     {
@@ -21,15 +26,24 @@ const Log = observer(() => {
       tab: '其他',
     },
   ];
+
+  useEffect(() => {
+    service.queryDefaultLevel().then((resp) => {
+      if (resp.status === 200) {
+        AlarmLogModel.defaultLevel = resp.result?.levels || [];
+      }
+    });
+  }, []);
+
   return (
     <PageContainer
-      // onTabChange={(key: 'product' | 'device' | 'department' | 'other') => {
-      //     AlarmLogModel.tab = key
-      // }}
+      onTabChange={(key: string) => {
+        AlarmLogModel.tab = key;
+      }}
       tabList={list}
       tabActiveKey={AlarmLogModel.tab}
     >
-      test
+      <TabComponent type={AlarmLogModel.tab} />
     </PageContainer>
   );
 });

+ 32 - 1
src/pages/rule-engine/Alarm/Log/model.ts

@@ -1,7 +1,38 @@
 import { model } from '@formily/reactive';
+import type { ProColumns } from '@jetlinks/pro-table';
 
 export const AlarmLogModel = model<{
-  tab: 'product' | 'device' | 'department' | 'other';
+  tab: string;
+  current: Partial<AlarmLogItem>;
+  solveVisible: boolean;
+  logVisible: boolean;
+  defaultLevel: {
+    level: number;
+    title: string;
+  }[];
+  columns: ProColumns<AlarmLogHistoryItem>[];
 }>({
   tab: 'product',
+  current: {},
+  solveVisible: false,
+  logVisible: false,
+  defaultLevel: [],
+  columns: [
+    {
+      dataIndex: 'alarmTime',
+      title: '告警时间',
+    },
+    {
+      dataIndex: 'alarmName',
+      title: '告警名称',
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+    },
+  ],
 });

+ 35 - 0
src/pages/rule-engine/Alarm/Log/service.ts

@@ -0,0 +1,35 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<AlarmLogItem> {
+  getTypes = () =>
+    request(`/${SystemConst.API_BASE}/relation/types`, {
+      method: 'GET',
+    });
+
+  handleLog = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/alarm/record/${id}/_handle`, {
+      method: 'POST',
+      data,
+    });
+
+  queryDefaultLevel = () =>
+    request(`/${SystemConst.API_BASE}/alarm/config/default/level`, {
+      method: 'GET',
+    });
+
+  queryHandleHistory = (data: any) =>
+    request(`/${SystemConst.API_BASE}/alarm/record/handle-history/_query`, {
+      method: 'POST',
+      data,
+    });
+
+  queryHistoryList = (data: any) =>
+    request(`/${SystemConst.API_BASE}/alarm/history/_query`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 38 - 0
src/pages/rule-engine/Alarm/Log/typings.d.ts

@@ -0,0 +1,38 @@
+type AlarmLogItem = {
+  id?: string;
+  name: string;
+  alarmConfigId: string;
+  alarmName: string;
+  targetType: string;
+  targetName: string;
+  targetTypeName: string;
+  alarmDate: number;
+  level: number;
+  description?: string;
+  state: Record<string, any>;
+};
+
+type AlarmLogSolveHistoryItem = {
+  id: string;
+  alarmId: string;
+  alarmRecordId: string;
+  handleType: string;
+  description: string;
+  creatorId?: string;
+  createTime: number;
+};
+
+type AlarmLogHistoryItem = {
+  id: string;
+  alarmId: string;
+  alarmConfigId: string;
+  alarmConfigName: string;
+  alarmRecordId: string;
+  level: number;
+  description: string;
+  alarmTime?: number;
+  targetType: string;
+  targetName: string;
+  targetId: string;
+  alarmInfo: string;
+};

+ 251 - 0
src/pages/system/Relationship/Save/index.tsx

@@ -0,0 +1,251 @@
+import { useIntl } from 'umi';
+import type { Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import { service } from '@/pages/system/Relationship';
+import { Modal } from '@/components';
+import { message } from 'antd';
+
+interface Props {
+  data: Partial<ReationItem>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const intl = useIntl();
+
+  const getTypes = () => service.getTypes();
+
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field).then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
+          ...item,
+          label: item.name,
+          value: JSON.stringify({
+            objectType: item.id,
+            objectTypeName: item.name,
+          }),
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+          },
+          relation: {
+            title: '标识',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-disabled': !!props.data?.id,
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入标识',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入标识',
+              },
+              // {
+              //   triggerType: 'onBlur',
+              //   // validator: (value: string) => {
+              //   //   return new Promise((resolve) => {
+              //   //     service
+              //   //       .validateField('username', value)
+              //   //       .then((resp) => {
+              //   //         if (resp.status === 200) {
+              //   //           if (resp.result.passed) {
+              //   //             resolve('');
+              //   //           } else {
+              //   //             resolve(model === 'edit' ? '' : resp.result.reason);
+              //   //           }
+              //   //         }
+              //   //         resolve('');
+              //   //       })
+              //   //       .catch(() => {
+              //   //         return '验证失败!';
+              //   //       });
+              //   //   });
+              //   // },
+              // },
+            ],
+            name: 'relation',
+            required: true,
+          },
+          object: {
+            title: '关联方',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-disabled': !!props.data?.id,
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择关联方',
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            required: true,
+            'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
+          },
+          target: {
+            title: '被关联方',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-disabled': !!props.data?.id,
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择被关联方',
+            },
+            'x-reactions': {
+              dependencies: ['..object'],
+              fulfill: {
+                state: {
+                  dataSource:
+                    '{{JSON.parse($deps[0] || "{}").objectType==="device"?[{label: "用户", value: JSON.stringify({"targetType":"user", "targetTypeName": "用户"})}] : []}}',
+                },
+              },
+            },
+            required: true,
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-component-props': {
+              rows: 5,
+              placeholder: '请输入说明',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                max: 200,
+                message: '最多可输入200个字符',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<any>();
+    const temp: any = {
+      ...props.data,
+      ...value,
+      ...JSON.parse(value?.object || '{}'),
+      ...JSON.parse(value?.target || '{}'),
+    };
+    delete temp.object;
+    delete temp.target;
+    const response: any = await service[!props.data?.id ? 'save' : 'update']({ ...temp });
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功',
+        }),
+      );
+      props.close();
+    } else {
+      message.error('操作失败!');
+    }
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.data.id ? 'edit' : 'add'}`,
+        defaultMessage: '编辑',
+      })}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+      permissionCode={'system/Relationship'}
+      permission={['add', 'edit']}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getTypes }} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 146 - 0
src/pages/system/Relationship/index.tsx

@@ -0,0 +1,146 @@
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { useRef, useState } from 'react';
+import Service from '@/pages/system/Relationship/service';
+import { PageContainer } from '@ant-design/pro-layout';
+import { PermissionButton } from '@/components';
+import { useIntl } from 'umi';
+import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
+import { message } from 'antd';
+import Save from './Save';
+
+export const service = new Service('relation');
+
+const Relationship = () => {
+  const intl = useIntl();
+  const [param, setParam] = useState<any>({});
+  const [current, setCurrent] = useState<Partial<ReationItem>>({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const actionRef = useRef<ActionType>();
+  const { permission } = PermissionButton.usePermission('system/Relationship');
+
+  const columns: ProColumns<ReationItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '名称',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'objectTypeName',
+      title: '关联方',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'targetTypeName',
+      title: '被关联方',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="warning"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              const resp: any = await service.remove(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="button"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<ReationItem>
+        field={columns}
+        target="relationship"
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          setParam(data);
+        }}
+      />
+      <ProTable<ReationItem>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        request={async (params) => {
+          return service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] });
+        }}
+        headerTitle={[
+          <PermissionButton
+            isPermission={permission.add}
+            key="add"
+            onClick={() => {
+              setVisible(true);
+              setCurrent({});
+            }}
+            type="primary"
+            tooltip={{
+              title: intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              }),
+            }}
+          >
+            新增
+          </PermissionButton>,
+        ]}
+      />
+      {visible && (
+        <Save
+          data={current}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Relationship;

+ 12 - 0
src/pages/system/Relationship/service.ts

@@ -0,0 +1,12 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ReationItem> {
+  getTypes = () =>
+    request(`/${SystemConst.API_BASE}/relation/types`, {
+      method: 'GET',
+    });
+}
+
+export default Service;

+ 12 - 0
src/pages/system/Relationship/typings.d.ts

@@ -0,0 +1,12 @@
+type ReationItem = {
+  id: string;
+  name: string;
+  objectType: string;
+  objectTypeName: string;
+  relation: string;
+  targetType: string;
+  targetTypeName: string;
+  createTime: number;
+  description?: string;
+  expands?: Record<string, any>;
+};

+ 4 - 0
src/utils/menu/router.ts

@@ -60,6 +60,8 @@ export enum MENUS_CODE {
   'rule-engine/Instance' = 'rule-engine/Instance',
   'rule-engine/SQLRule' = 'rule-engine/SQLRule',
   'rule-engine/Scene' = 'rule-engine/Scene',
+  'rule-engine/Alarm/Log' = 'rule-engine/Alarm/Log',
+  'rule-engine/Alarm/Log/Detail' = 'rule-engine/Alarm/Log/Detail',
   'rule-engine/Alarm/Config' = 'rule-engine/Alarm/Config',
   'rule-engine/Alarm/Configuration' = 'rule-engine/Alarm/Configuration',
   'simulator/Device' = 'simulator/Device',
@@ -79,6 +81,7 @@ export enum MENUS_CODE {
   'system/Tenant/Detail' = 'system/Tenant/Detail',
   'system/Tenant' = 'system/Tenant',
   'system/User' = 'system/User',
+  'system/Relationship' = 'system/Relationship',
   'user/Login' = 'user/Login',
   'visualization/Category' = 'visualization/Category',
   'visualization/Configuration' = 'visualization/Configuration',
@@ -141,4 +144,5 @@ export const getDetailNameByCode = {
   'link/Type/Detail': '网络组件详情',
   'link/AccessConfig/Detail': '配置详情',
   'media/Stream/Detail': '流媒体详情',
+  'rule-engine/Alarm/Log/Detail': '告警日志',
 };