Ver código fonte

feat: 个人中心通知订阅和通知记录

sun-chaochao 3 anos atrás
pai
commit
019101a8a9

+ 39 - 27
src/pages/account/NotificationRecord/detail/index.tsx

@@ -1,49 +1,61 @@
 import { Descriptions, Modal } from 'antd';
-import type { AccessLogItem } from '@/pages/Log/Access/typings';
 import { useEffect, useState } from 'react';
 import moment from 'moment';
+import { service } from '@/pages/account/NotificationRecord';
+import { service as service1 } from '@/pages/rule-engine/Alarm/Log';
+import { Store } from 'jetlinks-store';
 
 interface Props {
-  data: Partial<AccessLogItem>;
+  data: Partial<NotifitionRecord>;
   close: () => void;
 }
 
 const Detail = (props: Props) => {
-  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+  const [data, setDada] = useState<any>({});
 
   useEffect(() => {
-    setDada(props.data);
+    if (props.data.dataId) {
+      service.getAlarmList(props.data.dataId).then((resp) => {
+        if (resp.status === 200) {
+          setDada(resp.result);
+        }
+      });
+      service1.queryDefaultLevel().then((resp) => {
+        if (resp.status === 200) {
+          Store.set('default-level', resp.result?.levels || []);
+        }
+      });
+    }
   }, [props.data]);
 
   return (
     <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
-      <Descriptions bordered>
-        <Descriptions.Item label="URL">{data?.url}</Descriptions.Item>
-        <Descriptions.Item label="请求方法" span={2}>
-          {data?.httpMethod}
+      <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?.alarmName || '--'}
         </Descriptions.Item>
-        <Descriptions.Item label="动作">{data?.action}</Descriptions.Item>
-        <Descriptions.Item label="类名" span={2}>
-          {data?.target}
+        <Descriptions.Item label="告警时间" span={1}>
+          {moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
         </Descriptions.Item>
-        <Descriptions.Item label="方法名">{data?.method}</Descriptions.Item>
-        <Descriptions.Item label="IP" span={2}>
-          {data?.ip}
+        <Descriptions.Item label="告警级别" span={1}>
+          {(Store.get('default-level') || []).find((item: any) => item?.level === data?.level)
+            ?.title || data?.level}
         </Descriptions.Item>
-        <Descriptions.Item label="请求时间">
-          {moment(data?.requestTime).format('YYYY-MM-DD HH:mm:ss')}
+        <Descriptions.Item label="告警说明" span={1}>
+          {data?.description || '--'}
         </Descriptions.Item>
-        <Descriptions.Item label="请求耗时" span={2}>
-          {(data?.responseTime || 0) - (data?.requestTime || 0)}ms
-        </Descriptions.Item>
-        <Descriptions.Item label="请求头" span={3}>
-          {JSON.stringify(data?.httpHeaders)}
-        </Descriptions.Item>
-        <Descriptions.Item label="请求参数" span={3}>
-          {JSON.stringify(data?.parameters)}
-        </Descriptions.Item>
-        <Descriptions.Item label="异常信息" span={3}>
-          {data?.exception}
+        <Descriptions.Item label="告警流水" span={2}>
+          {data?.alarmInfo || '--'}
         </Descriptions.Item>
       </Descriptions>
     </Modal>

+ 68 - 14
src/pages/account/NotificationRecord/index.tsx

@@ -1,40 +1,81 @@
 import { useIntl } from '@/.umi/plugin-locale/localeExports';
 import PermissionButton from '@/components/PermissionButton';
 import SearchComponent from '@/components/SearchComponent';
-import BaseService from '@/utils/BaseService';
 import { ReadOutlined, SearchOutlined } from '@ant-design/icons';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { useRef, useState } from 'react';
-import type { NotifitionRecord } from './typings';
+import { useEffect, useRef, useState } from 'react';
 import Detail from './detail';
+import { Badge, message } from 'antd';
+import Service from './service';
+import encodeQuery from '@/utils/encodeQuery';
 
-export const service = new BaseService<NotifitionRecord>('network/certificate');
+export const service = new Service('notifications');
 
 const NotificationRecord = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const [param, setParam] = useState({});
   const [visible, setVisible] = useState<boolean>(false);
-  const [current, setCurrent] = useState<NotifitionRecord | undefined>(undefined);
+  const [current, setCurrent] = useState<Partial<NotifitionRecord>>({});
+  const [typeList, setTypeList] = useState<any>({});
+
+  useEffect(() => {
+    service.getProvidersList().then((resp) => {
+      const obj: any = {};
+      resp.map((i: any) => {
+        obj[i?.value] = { status: i?.value, text: i?.label };
+      });
+      setTypeList(obj);
+    });
+  }, []);
 
   const columns: ProColumns<NotifitionRecord>[] = [
     {
-      dataIndex: 'instance',
+      dataIndex: 'topicProvider',
       title: '类型',
+      render: (text: any, record: any) => {
+        return <span>{typeList[record?.topicProvider]?.text || text}</span>;
+      },
+      valueType: 'select',
+      request: () =>
+        service.getProvidersList().then((resp: any) =>
+          resp.map((item: any) => ({
+            label: item.label,
+            value: item.value,
+          })),
+        ),
     },
     {
-      dataIndex: 'name',
+      dataIndex: 'message',
       title: '消息',
     },
     {
-      dataIndex: 'description',
+      dataIndex: 'notifyTime',
       title: '通知时间',
+      valueType: 'dateTime',
     },
     {
       dataIndex: 'state',
       title: '状态',
+      render: (text: any, record: any) => (
+        <Badge
+          status={record.state?.value === 'read' ? 'success' : 'error'}
+          text={record?.state?.text || '-'}
+        />
+      ),
+      valueType: 'select',
+      valueEnum: {
+        unread: {
+          text: '未读',
+          status: 'unread',
+        },
+        read: {
+          text: '已读',
+          status: 'read',
+        },
+      },
     },
     {
       title: intl.formatMessage({
@@ -51,11 +92,18 @@ const NotificationRecord = () => {
           isPermission={true}
           style={{ padding: 0 }}
           tooltip={{
-            title: '标为已读',
+            title: record?.state?.value !== 'read' ? '标为已读' : '标为未读',
           }}
-          onClick={() => {
-            setCurrent(record);
-            setVisible(true);
+          popConfirm={{
+            title: `确认${record?.state?.value !== 'read' ? '标为已读' : '标为未读'}`,
+            onConfirm: async () => {
+              const state = record?.state?.value !== 'read' ? 'read' : 'unread';
+              const resp = await service.saveData(state, [record.id]);
+              if (resp.status === 200) {
+                message.success('操作成功');
+                actionRef.current?.reload();
+              }
+            },
           }}
         >
           <ReadOutlined />
@@ -68,6 +116,10 @@ const NotificationRecord = () => {
           tooltip={{
             title: '查看',
           }}
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
         >
           <SearchOutlined />
         </PermissionButton>,
@@ -90,15 +142,17 @@ const NotificationRecord = () => {
         actionRef={actionRef}
         params={param}
         columns={columns}
+        rowKey="id"
         search={false}
         request={async (params) =>
-          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+          service.queryList(encodeQuery({ ...params, sorts: { notifyTime: 'desc' } }))
         }
       />
       {visible && (
         <Detail
           close={() => {
-            actionRef.current?.reload();
+            setCurrent({});
+            setVisible(false);
           }}
           data={current}
         />

+ 36 - 0
src/pages/account/NotificationRecord/service.ts

@@ -0,0 +1,36 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<NotifitionRecord> {
+  public queryList = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/_query`, {
+      method: 'GET',
+      params: data,
+    });
+
+  public saveData = (state: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/_${state}`, {
+      method: 'POST',
+      data,
+    });
+
+  public getAlarmList = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/alarm/record/${id}`, {
+      method: 'GET',
+      params: data,
+    });
+
+  public getProvidersList = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/providers`, {
+      method: 'GET',
+      params,
+    }).then((resp: any) => {
+      return (resp?.result || []).map((item: any) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+}
+
+export default Service;

+ 11 - 4
src/pages/account/NotificationRecord/typings.d.ts

@@ -1,5 +1,12 @@
-import type { BaseItem } from '@/utils/typings';
-
 type NotifitionRecord = {
-  type: string;
-} & BaseItem;
+  id: string;
+  topicProvider: string;
+  message: string;
+  notifyTime: string;
+  state?: any;
+  subscribeId: string;
+  subscriberType: string;
+  subscriberType: string;
+  topicName: string;
+  dataId: string;
+};

+ 60 - 23
src/pages/account/NotificationSubscription/index.tsx

@@ -1,7 +1,6 @@
 import { useIntl } from '@/.umi/plugin-locale/localeExports';
 import PermissionButton from '@/components/PermissionButton';
 import SearchComponent from '@/components/SearchComponent';
-import BaseService from '@/utils/BaseService';
 import {
   DeleteOutlined,
   EditOutlined,
@@ -10,20 +9,33 @@ import {
   StopOutlined,
 } from '@ant-design/icons';
 import { PageContainer } from '@ant-design/pro-layout';
+import { observer } from '@formily/reactive-react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { useRef, useState } from 'react';
-import type { NotifitionSubscriptionItem } from './typings';
+import { Badge, message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
 import Save from './save';
+import Service from './service';
 
-export const service = new BaseService<NotifitionSubscriptionItem>('network/certificate');
+export const service = new Service('notifications/subscriptions');
 
-const NotificationSubscription = () => {
+const NotificationSubscription = observer(() => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const [param, setParam] = useState({});
   const [visible, setVisible] = useState<boolean>(false);
-  const [current, setCurrent] = useState<NotifitionSubscriptionItem | undefined>(undefined);
+  const [current, setCurrent] = useState<Partial<NotifitionSubscriptionItem>>({});
+  const [typeList, setTypeList] = useState<any>({});
+
+  useEffect(() => {
+    service.getProvidersList().then((resp) => {
+      const obj: any = {};
+      resp.map((i: any) => {
+        obj[i?.value] = i?.label || '';
+      });
+      setTypeList(obj);
+    });
+  }, []);
 
   const Tools = (record: any) => {
     return [
@@ -58,16 +70,16 @@ const NotificationSubscription = () => {
             defaultMessage: '确认禁用?',
           }),
           onConfirm: async () => {
-            // const resp =
-            //   record?.state?.value !== 'disabled'
-            //     ? await service._disable(record.id)
-            //     : await service._enable(record.id);
-            // if (resp.status === 200) {
-            //   message.success('操作成功!');
-            //   actionRef.current?.reload?.();
-            // } else {
-            //   message.error('操作失败!');
-            // }
+            const resp =
+              record?.state?.value !== 'disabled'
+                ? await service._disabled(record.id)
+                : await service._enabled(record.id);
+            if (resp.status === 200) {
+              message.success('操作成功!');
+              actionRef.current?.reload?.();
+            } else {
+              message.error('操作失败!');
+            }
           },
         }}
         tooltip={{
@@ -86,7 +98,15 @@ const NotificationSubscription = () => {
         style={{ padding: 0 }}
         popConfirm={{
           title: '确认删除?',
-          onConfirm: () => {},
+          onConfirm: async () => {
+            const resp: any = await service.remove(record.id);
+            if (resp.status === 200) {
+              message.success('操作成功!');
+              actionRef.current?.reload?.();
+            } else {
+              message.error('操作失败!');
+            }
+          },
         }}
         tooltip={{
           title: '删除',
@@ -99,23 +119,35 @@ const NotificationSubscription = () => {
 
   const columns: ProColumns<NotifitionSubscriptionItem>[] = [
     {
-      dataIndex: 'instance',
+      dataIndex: 'subscribeName',
       title: '名称',
     },
     {
-      dataIndex: 'name',
+      dataIndex: 'topicProvider',
       title: '类型',
       hideInSearch: true,
+      render: (text: any, record: any) => {
+        return <span>{typeList[record?.topicProvider] || text}</span>;
+      },
     },
     {
-      dataIndex: 'description',
+      dataIndex: 'topicConfig',
       title: '告警规则',
       hideInSearch: true,
+      render: (text: any, record: any) => (
+        <span>{record?.topicConfig?.alarmConfigName || '-'}</span>
+      ),
     },
     {
       dataIndex: 'state',
       title: '状态',
       hideInSearch: true,
+      render: (text: any, record: any) => (
+        <Badge
+          status={record.state?.value === 'enabled' ? 'success' : 'error'}
+          text={record?.state?.text || '-'}
+        />
+      ),
     },
     {
       title: '操作',
@@ -125,6 +157,7 @@ const NotificationSubscription = () => {
       render: (text, record) => [Tools(record)],
     },
   ];
+
   return (
     <PageContainer>
       <SearchComponent<NotifitionSubscriptionItem>
@@ -141,6 +174,7 @@ const NotificationSubscription = () => {
         params={param}
         columns={columns}
         search={false}
+        rowKey="id"
         request={async (params) =>
           service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
         }
@@ -148,7 +182,7 @@ const NotificationSubscription = () => {
           <PermissionButton
             onClick={() => {
               setVisible(true);
-              setCurrent(undefined);
+              setCurrent({});
             }}
             isPermission={true}
             style={{ marginRight: 12 }}
@@ -167,13 +201,16 @@ const NotificationSubscription = () => {
         <Save
           close={() => {
             setVisible(false);
-            actionRef.current?.reload();
           }}
           data={current}
+          reload={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
         />
       )}
     </PageContainer>
   );
-};
+});
 
 export default NotificationSubscription;

+ 48 - 26
src/pages/account/NotificationSubscription/save/index.tsx

@@ -1,18 +1,21 @@
-import { Modal } from 'antd';
-import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import { message, Modal } from 'antd';
 import { useEffect, useMemo, useState } from 'react';
 import { Form, FormGrid, FormItem, Input, Select, Checkbox } from '@formily/antd';
 import { createForm } from '@formily/core';
 import type { ISchema } from '@formily/react';
 import { createSchemaField } from '@formily/react';
+import { useAsyncDataSource } from '@/utils/util';
+import { service } from '@/pages/account/NotificationSubscription';
 
 interface Props {
-  data: Partial<AccessLogItem>;
+  data: Partial<NotifitionSubscriptionItem>;
   close: () => void;
+  reload: () => void;
 }
 
 const Save = (props: Props) => {
-  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+  const [data, setDada] = useState<Partial<NotifitionSubscriptionItem>>(props.data || {});
+  const [dataList, setDataList] = useState<any[]>([]);
 
   useEffect(() => {
     setDada(props.data);
@@ -23,13 +26,19 @@ const Save = (props: Props) => {
       createForm({
         validateFirst: true,
         initialValues: data,
-        effects() {
-          //
-        },
       }),
     [],
   );
 
+  const queryProvidersList = () => service.getProvidersList();
+
+  const queryAlarmConfigList = () => {
+    return service.getAlarmConfigList().then((resp) => {
+      setDataList(resp);
+      return resp;
+    });
+  };
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -41,7 +50,7 @@ const Save = (props: Props) => {
           minColumns: 1,
         },
         properties: {
-          name: {
+          subscribeName: {
             title: '名称',
             'x-component': 'Input',
             'x-decorator': 'FormItem',
@@ -61,7 +70,7 @@ const Save = (props: Props) => {
               },
             ],
           },
-          type: {
+          topicProvider: {
             title: '类型',
             'x-component': 'Select',
             'x-decorator': 'FormItem',
@@ -71,17 +80,12 @@ const Save = (props: Props) => {
               labelAlign: 'left',
               layout: 'vertical',
             },
-            enum: [
-              {
-                label: '设备告警',
-                value: 'device',
-              },
-            ],
             'x-component-props': {
               placeholder: '请选择类型',
             },
+            'x-reactions': ['{{useAsyncDataSource(queryProvidersList)}}'],
           },
-          rule: {
+          'topicConfig.alarmConfigId': {
             title: '告警规则',
             'x-component': 'Select',
             'x-decorator': 'FormItem',
@@ -91,15 +95,17 @@ const Save = (props: Props) => {
               labelAlign: 'left',
               layout: 'vertical',
             },
-            enum: [],
             'x-component-props': {
               placeholder: '请选择告警规则',
             },
+            'x-reactions': ['{{useAsyncDataSource(queryAlarmConfigList)}}'],
           },
           notice: {
             title: '通知方式',
             type: 'array',
             required: true,
+            'x-disabled': true,
+            default: [1],
             enum: [
               {
                 label: '站内通知',
@@ -137,19 +143,35 @@ const Save = (props: Props) => {
     },
   });
 
+  const handleSave = async () => {
+    let param: any = await form.submit();
+    delete param.notice;
+    const config = dataList.find((item) => item?.value === param?.topicConfig?.alarmConfigId);
+    param = {
+      ...data,
+      ...param,
+      topicConfig: {
+        ...param?.topicConfig,
+        alarmConfigName: config.label || '',
+      },
+    };
+    const response: any = await service.saveData(param);
+    if (response.status === 200) {
+      message.success('操作成功!');
+      props.reload();
+    }
+  };
+
   return (
-    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={'45vw'}>
+    <Modal title={'详情'} visible onCancel={props.close} onOk={() => handleSave()} width={'45vw'}>
       <Form form={form} layout="vertical">
         <SchemaField
           schema={schema}
-          scope={
-            {
-              // useAsyncDataSource,
-              // queryRegionsList,
-              // queryProductList,
-              // queryAliyunProductList,
-            }
-          }
+          scope={{
+            useAsyncDataSource,
+            queryProvidersList,
+            queryAlarmConfigList,
+          }}
         />
       </Form>
     </Modal>

+ 56 - 0
src/pages/account/NotificationSubscription/service.ts

@@ -0,0 +1,56 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<NotifitionSubscriptionItem> {
+  //
+  public saveData = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/subscribe`, {
+      method: 'PATCH',
+      data,
+    });
+
+  public _enabled = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/subscription/${id}/_enabled`, {
+      method: 'PUT',
+      data,
+    });
+
+  public _disabled = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/subscription/${id}/_disabled`, {
+      method: 'PUT',
+      data,
+    });
+
+  public remove = (id: string) =>
+    request(`/${SystemConst.API_BASE}/notifications/subscription/${id}`, {
+      method: 'DELETE',
+    });
+  // 获取订阅类型
+  public getProvidersList = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/notifications/providers`, {
+      method: 'GET',
+      params,
+    }).then((resp: any) => {
+      return (resp?.result || []).map((item: any) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+
+  // 获取告警配置
+  public getAlarmConfigList = () =>
+    request(`/${SystemConst.API_BASE}/alarm/config/_query/no-paging`, {
+      method: 'POST',
+      data: {
+        paging: false,
+      },
+    }).then((resp: any) => {
+      return (resp?.result || []).map((item: any) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+}
+
+export default Service;

+ 8 - 4
src/pages/account/NotificationSubscription/typings.d.ts

@@ -1,5 +1,9 @@
-import type { BaseItem } from '@/utils/typings';
-
 type NotifitionSubscriptionItem = {
-  type: string;
-} & BaseItem;
+  id?: string;
+  subscribeName: string;
+  topicProvider: string;
+  topicConfig: {
+    alarmConfigId: string;
+    alarmConfigName: string;
+  };
+};

+ 11 - 9
src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx

@@ -55,15 +55,17 @@ export default (props: Props) => {
           width: '100%',
         }}
       >
-        <PathSimplifier pathData={[dataSource]}>
-          <PathSimplifier.PathNavigator
-            speed={speed}
-            isAuto={false}
-            onCreate={(nav) => {
-              PathNavigatorRef.current = nav;
-            }}
-          />
-        </PathSimplifier>
+        {(dataSource?.path || []).length > 0 ? (
+          <PathSimplifier pathData={[dataSource]}>
+            <PathSimplifier.PathNavigator
+              speed={speed}
+              isAuto={false}
+              onCreate={(nav) => {
+                PathNavigatorRef.current = nav;
+              }}
+            />
+          </PathSimplifier>
+        ) : null}
       </AMap>
     </div>
   );

+ 1 - 1
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -78,7 +78,7 @@ const PropertyLog = (props: Props) => {
           ) : (
             <ATooltip title="下载">
               <Popconfirm
-                title="确认修改"
+                title="确认下载"
                 onConfirm={() => {
                   const type = (record?.value || '').split('.').pop();
                   const downloadUrl = record.value;

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

@@ -112,6 +112,7 @@ const Edit = (props: Props) => {
       obj[item.relation] = [...(item?.related || []).map((i: any) => JSON.stringify(i))];
     });
     setInitData(obj);
+    form.setValues(obj);
   }, [props.data]);
 
   return (

+ 18 - 1
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -40,11 +40,28 @@ const FileComponent = (props: Props) => {
       return <div className={props.type === 'card' ? styles.other : {}}>--</div>;
     } else if (data?.valueType?.type === 'file') {
       const flag: string = value?.formatValue.split('.').pop() || 'other';
+      if (['jpg', 'png'].includes(flag)) {
+        return (
+          <div
+            className={styles.img}
+            onClick={() => {
+              setType(flag);
+              setVisible(true);
+            }}
+          >
+            {value?.formatValue ? (
+              <img style={{ width: '100%' }} src={value?.formatValue} />
+            ) : (
+              <img src={imgMap.get(flag) || imgMap.get('other')} />
+            )}
+          </div>
+        );
+      }
       return (
         <div
           className={styles.img}
           onClick={() => {
-            if (['jpg', 'png', 'tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
+            if (['tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
               setType(flag);
               setVisible(true);
             }