Browse Source

feat: merge

xieyonghong 3 năm trước cách đây
mục cha
commit
7d74a0454f
59 tập tin đã thay đổi với 1518 bổ sung375 xóa
  1. BIN
      public/images/certificate.png
  2. BIN
      public/images/running/doc.png
  3. BIN
      public/images/running/docx.png
  4. BIN
      public/images/running/error.png
  5. BIN
      public/images/running/flv.png
  6. BIN
      public/images/running/img.png
  7. BIN
      public/images/running/jpg.png
  8. BIN
      public/images/running/mp3.png
  9. BIN
      public/images/running/mp4.png
  10. BIN
      public/images/running/mvb.png
  11. BIN
      public/images/running/other.png
  12. BIN
      public/images/running/pdf.png
  13. BIN
      public/images/running/png.png
  14. BIN
      public/images/running/ppt.png
  15. BIN
      public/images/running/pptx.png
  16. BIN
      public/images/running/rmvb.png
  17. BIN
      public/images/running/swf.png
  18. BIN
      public/images/running/tiff.png
  19. BIN
      public/images/running/txt.png
  20. BIN
      public/images/running/video.png
  21. BIN
      public/images/running/wma.png
  22. BIN
      public/images/running/xls.png
  23. BIN
      public/images/running/xlsx.png
  24. 1 1
      src/components/ProTableCard/CardItems/duerOs.tsx
  25. 2 2
      src/pages/Northbound/AliCloud/Detail/index.tsx
  26. 23 4
      src/pages/Northbound/AliCloud/index.tsx
  27. 115 67
      src/pages/Northbound/DuerOS/Detail/index.tsx
  28. 29 3
      src/pages/Northbound/DuerOS/index.tsx
  29. 4 1
      src/pages/Northbound/DuerOS/types.d.ts
  30. 64 0
      src/pages/account/NotificationRecord/detail/index.tsx
  31. 164 0
      src/pages/account/NotificationRecord/index.tsx
  32. 36 0
      src/pages/account/NotificationRecord/service.ts
  33. 12 0
      src/pages/account/NotificationRecord/typings.d.ts
  34. 216 0
      src/pages/account/NotificationSubscription/index.tsx
  35. 180 0
      src/pages/account/NotificationSubscription/save/index.tsx
  36. 56 0
      src/pages/account/NotificationSubscription/service.ts
  37. 9 0
      src/pages/account/NotificationSubscription/typings.d.ts
  38. 4 1
      src/pages/cloud/DuerOS/typings.d.ts
  39. 11 9
      src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx
  40. 8 1
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  41. 85 82
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  42. 1 0
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  43. 4 6
      src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx
  44. 4 1
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  45. 91 26
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  46. 1 1
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  47. 1 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  48. 1 1
      src/pages/device/Instance/Detail/Running/index.tsx
  49. 68 0
      src/pages/link/Certificate/Detail/components/CertificateFile/index.tsx
  50. 38 0
      src/pages/link/Certificate/Detail/components/Standard/index.tsx
  51. 143 0
      src/pages/link/Certificate/Detail/index.tsx
  52. 79 151
      src/pages/link/Certificate/index.tsx
  53. 14 0
      src/pages/link/Certificate/service.ts
  54. 7 6
      src/pages/link/Certificate/typings.d.ts
  55. 2 2
      src/pages/notice/Config/Detail/index.tsx
  56. 20 4
      src/pages/notice/Config/SyncUser/index.tsx
  57. 5 5
      src/pages/notice/index.tsx
  58. 16 0
      src/utils/menu/index.ts
  59. 4 0
      src/utils/menu/router.ts

BIN
public/images/certificate.png


BIN
public/images/running/doc.png


BIN
public/images/running/docx.png


BIN
public/images/running/error.png


BIN
public/images/running/flv.png


BIN
public/images/running/img.png


BIN
public/images/running/jpg.png


BIN
public/images/running/mp3.png


BIN
public/images/running/mp4.png


BIN
public/images/running/mvb.png


BIN
public/images/running/other.png


BIN
public/images/running/pdf.png


BIN
public/images/running/png.png


BIN
public/images/running/ppt.png


BIN
public/images/running/pptx.png


BIN
public/images/running/rmvb.png


BIN
public/images/running/swf.png


BIN
public/images/running/tiff.png


BIN
public/images/running/txt.png


BIN
public/images/running/video.png


BIN
public/images/running/wma.png


BIN
public/images/running/xls.png


BIN
public/images/running/xlsx.png


+ 1 - 1
src/components/ProTableCard/CardItems/duerOs.tsx

@@ -45,7 +45,7 @@ export default (props: DuerOSProps) => {
             <div>
               <label>设备类型</label>
               <div className={'ellipsis'}>
-                <Tooltip title={props.applianceType}>{props.applianceType}</Tooltip>
+                <Tooltip title={props.applianceType?.text}>{props.applianceType?.text}</Tooltip>
               </div>
             </div>
           </div>

+ 2 - 2
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -58,7 +58,7 @@ const Detail = observer(() => {
   );
 
   useEffect(() => {
-    if (params.id) {
+    if (params.id && params.id !== ':id') {
       service.detail(params.id).then((resp) => {
         if (resp.status === 200) {
           form.setValues(resp.result);
@@ -318,7 +318,7 @@ const Detail = observer(() => {
         <Row gutter={24}>
           <Col span={14}>
             <TitleComponent data={'基本信息'} />
-            <Form form={form} layout="vertical" onAutoSubmit={console.log}>
+            <Form form={form} layout="vertical">
               <SchemaField
                 schema={schema}
                 scope={{

+ 23 - 4
src/pages/Northbound/AliCloud/index.tsx

@@ -44,7 +44,10 @@ const AliCloud = () => {
               }
             : undefined
         }
-        onClick={() => {}}
+        onClick={() => {
+          const url = `${getMenuPathByParams(MENUS_CODE['Northbound/AliCloud/Detail'], record.id)}`;
+          history.push(url);
+        }}
       >
         <EditOutlined />
         {type !== 'table' &&
@@ -96,7 +99,20 @@ const AliCloud = () => {
         popConfirm={{
           title: '确认删除?',
           disabled: record.state.value === 'started',
-          onConfirm: () => {},
+          onConfirm: async () => {
+            if (record?.state?.value === 'disabled') {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            } else {
+              message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+            }
+          },
         }}
         tooltip={{
           title:
@@ -124,9 +140,12 @@ const AliCloud = () => {
     {
       title: '状态',
       dataIndex: 'state',
-      render: (text: any) => (
+      render: (text: any, record: any) => (
         <span>
-          <Badge status={text.value === 'disabled' ? 'error' : 'success'} text={text.text} />
+          <Badge
+            status={record?.state?.value === 'disabled' ? 'error' : 'success'}
+            text={record?.state?.text}
+          />
         </span>
       ),
       valueType: 'select',

+ 115 - 67
src/pages/Northbound/DuerOS/Detail/index.tsx

@@ -21,6 +21,7 @@ import { service } from '..';
 import { Store } from 'jetlinks-store';
 import { useParams } from 'umi';
 import Doc from '@/pages/Northbound/DuerOS/Detail/Doc';
+import _ from 'lodash';
 
 const Save = () => {
   const SchemaField = createSchemaField({
@@ -37,6 +38,20 @@ const Save = () => {
 
   const { id } = useParams<{ id: string }>();
 
+  const findProductMetadata = (_id: string) => {
+    if (!_id) return;
+    const _productList = Store.get('product-list');
+    const _product = _productList?.find((item: any) => item.id === _id);
+    return _product?.metadata && JSON.parse(_product.metadata || '{}');
+  };
+
+  const findApplianceType = (_id: string) => {
+    if (!_id) return;
+    const _productTypes = Store.get('product-types');
+    console.log(_productTypes, 'tt');
+    return _productTypes?.find((item: any) => item.id === _id);
+  };
+
   const getProduct = () =>
     service.getProduct().then((resp) => {
       Store.set('product-list', resp.result);
@@ -49,6 +64,92 @@ const Save = () => {
       return resp.result;
     });
 
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {
+          onFormInit(async (form1) => {
+            await getTypes();
+            await getProduct();
+            if (id === ':id') return;
+            const resp = await service.detail(id);
+            /// 单独处理一下applianceType
+            const _data = resp.result;
+            if (_data) {
+              _data.applianceType = _data?.applianceType?.value;
+            }
+            form1.setInitialValues(_data);
+          });
+          onFieldReact('actionMappings.*.layout.action', (field) => {
+            const productType = field.query('applianceType').value();
+            (field as Field).setDataSource(findApplianceType(productType)?.actions);
+          });
+          onFieldReact('actionMappings.*.layout.command.message.properties', (field) => {
+            const product = field.query('id').value();
+            (field as Field).setDataSource(findProductMetadata(product)?.properties);
+          });
+          onFieldReact('actionMappings.*.layout.command.message.functionId', (field) => {
+            const product = field.query('id').value();
+            (field as Field).setDataSource(findProductMetadata(product)?.functions);
+          });
+          onFieldValueChange(
+            'actionMappings.*.layout.command.message.functionId',
+            (field, form1) => {
+              const functionId = field.value;
+              if (!functionId) return;
+              const product = field.query('id').value();
+              const _functionList = findProductMetadata(product)?.functions;
+              const _function =
+                _functionList && _functionList.find((item: any) => item.id === functionId);
+              form1.setFieldState(field.query('.function'), (state) => {
+                state.value = _function?.inputs.map((item: any) => ({
+                  ...item,
+                  valueType: item?.valueType?.type,
+                }));
+              });
+            },
+          );
+          onFieldReact('propertyMappings.*.layout.source', (field) => {
+            const productType = field.query('applianceType').value();
+            (field as Field).setDataSource(findApplianceType(productType)?.properties);
+          });
+          onFieldReact('propertyMappings.*.layout.target', (field) => {
+            const product = field.query('id').value();
+            (field as Field).setDataSource(findProductMetadata(product)?.properties);
+          });
+        },
+      }),
+    [],
+  );
+
+  const getProductProperties = (f: Field) => {
+    const items =
+      form
+        .getValuesIn('propertyMappings')
+        ?.map((i: { target: string[] }) => i.target?.map((j) => j)) || [];
+    const checked = _.flatMap(items);
+    const index = checked.findIndex((i) => i === f.value);
+    checked.splice(index, 1);
+    const _id = form.getValuesIn('id');
+    const sourceList = findProductMetadata(_id)?.properties;
+    const list = sourceList?.filter((i: { id: string }) => !checked.includes(i.id));
+    return new Promise((resolve) => resolve(list));
+  };
+
+  const getDuerOSProperties = (f: Field) => {
+    const items =
+      form.getValuesIn('propertyMappings')?.map((i: { source: string }) => i.source) || [];
+    const checked = [...items];
+    const index = checked.findIndex((i) => i === f.value);
+    checked.splice(index, 1);
+    const _productType = form.getValuesIn('applianceType');
+    const targetList = findApplianceType(_productType?.value)?.properties;
+    console.log(targetList, 'list', _productType);
+    const list = targetList?.filter((i: { id: string }) => !checked.includes(i.id));
+    return new Promise((resolve) => resolve(list));
+  };
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -398,6 +499,7 @@ const Save = () => {
                       value: 'id',
                     },
                   },
+                  'x-reactions': ['{{useAsyncDataSource(getDuerOSProperties)}}'],
                 },
                 target: {
                   title: '平台属性',
@@ -414,6 +516,7 @@ const Save = () => {
                     },
                     mode: 'tags',
                   },
+                  'x-reactions': ['{{useAsyncDataSource(getProductProperties)}}'],
                 },
               },
             },
@@ -434,74 +537,10 @@ const Save = () => {
     },
   };
 
-  const findProductMetadata = (_id: string) => {
-    if (!_id) return;
-    const _productList = Store.get('product-list');
-    const _product = _productList?.find((item: any) => item.id === _id);
-    return _product?.metadata && JSON.parse(_product.metadata || '{}');
-  };
-
-  const findapplianceType = (_id: string) => {
-    if (!_id) return;
-    const _productTypes = Store.get('product-types');
-    return _productTypes?.find((item: any) => item.id === _id);
-  };
-  const form = useMemo(
-    () =>
-      createForm({
-        validateFirst: true,
-        effects() {
-          onFormInit(async (form1) => {
-            await getTypes();
-            await getProduct();
-            const resp = await service.detail(id);
-            form1.setInitialValues(resp.result);
-          });
-          onFieldReact('actionMappings.*.layout.action', (field) => {
-            const productType = field.query('applianceType').value();
-            (field as Field).setDataSource(findapplianceType(productType)?.actions);
-          });
-          onFieldReact('actionMappings.*.layout.command.message.properties', (field) => {
-            const product = field.query('id').value();
-            (field as Field).setDataSource(findProductMetadata(product)?.properties);
-          });
-          onFieldReact('actionMappings.*.layout.command.message.functionId', (field) => {
-            const product = field.query('id').value();
-            (field as Field).setDataSource(findProductMetadata(product)?.functions);
-          });
-          onFieldValueChange(
-            'actionMappings.*.layout.command.message.functionId',
-            (field, form1) => {
-              const functionId = field.value;
-              if (!functionId) return;
-              const product = field.query('id').value();
-              const _functionList = findProductMetadata(product)?.functions;
-              const _function =
-                _functionList && _functionList.find((item: any) => item.id === functionId);
-              form1.setFieldState(field.query('.function'), (state) => {
-                state.value = _function?.inputs.map((item: any) => ({
-                  ...item,
-                  valueType: item?.valueType?.type,
-                }));
-              });
-            },
-          );
-          onFieldReact('propertyMappings.*.layout.source', (field) => {
-            const productType = field.query('applianceType').value();
-            (field as Field).setDataSource(findapplianceType(productType)?.properties);
-          });
-          onFieldReact('propertyMappings.*.layout.target', (field) => {
-            const product = field.query('id').value();
-            (field as Field).setDataSource(findProductMetadata(product)?.properties);
-          });
-        },
-      }),
-    [],
-  );
-
   const handleSave = async () => {
     const data: any = await form.submit();
-    await service.savePatch(data);
+    const productName = Store.get('product-list')?.find((item: any) => item.id === data.id)?.name;
+    await service.savePatch({ ...data, productName });
     message.success('保存成功!');
     history.back();
   };
@@ -511,7 +550,16 @@ const Save = () => {
         <Row>
           <Col span={12}>
             <Form layout="vertical" form={form}>
-              <SchemaField schema={schema} scope={{ useAsyncDataSource, getTypes, getProduct }} />
+              <SchemaField
+                schema={schema}
+                scope={{
+                  useAsyncDataSource,
+                  getTypes,
+                  getProduct,
+                  getProductProperties,
+                  getDuerOSProperties,
+                }}
+              />
               <FormButtonGroup.Sticky>
                 <FormButtonGroup.FormItem>
                   <PermissionButton isPermission={true} type="primary" onClick={handleSave}>

+ 29 - 3
src/pages/Northbound/DuerOS/index.tsx

@@ -3,13 +3,18 @@ import SearchComponent from '@/components/SearchComponent';
 import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { PermissionButton, ProTableCard } from '@/components';
-import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+} from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { message, Space } from 'antd';
 import { DuerOSItem } from '@/pages/Northbound/DuerOS/types';
 import DuerOSCard from '@/components/ProTableCard/CardItems/duerOs';
 import { history } from '@@/core/history';
-import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import Service from './service';
 
 export const service = new Service('dueros/product');
@@ -79,10 +84,18 @@ export default () => {
     },
     {
       title: intl.formatMessage({
+        id: 'page.cloud.duerOS.productName',
+        defaultMessage: '产品',
+      }),
+      dataIndex: 'productName',
+    },
+    {
+      title: intl.formatMessage({
         id: 'pages.cloud.duerOS.applianceType',
         defaultMessage: '设备类型',
       }),
       dataIndex: 'applianceType',
+      renderText: (data) => data.text,
     },
     {
       title: intl.formatMessage({
@@ -120,6 +133,19 @@ export default () => {
           setSearchParams(data);
         }}
       />
+      <div style={{ backgroundColor: 'white', width: '100%', height: 60, padding: 20 }}>
+        <div
+          style={{
+            padding: 10,
+            width: '100%',
+            color: 'rgba(0, 0, 0, 0.55)',
+            backgroundColor: '#f6f6f6',
+          }}
+        >
+          <ExclamationCircleFilled style={{ marginRight: 10 }} />
+          将平台产品通过API的方式同步DuerOS平台
+        </div>
+      </div>
       <ProTableCard<DuerOSItem>
         rowKey="id"
         search={false}
@@ -136,7 +162,7 @@ export default () => {
             <PermissionButton
               isPermission={true}
               onClick={() => {
-                history.push(getMenuPathByParams(MENUS_CODE['Northbound/DuerOS/Detail']));
+                history.push(getMenuPathByCode(MENUS_CODE['Northbound/DuerOS/Detail']));
               }}
               key="button"
               icon={<PlusOutlined />}

+ 4 - 1
src/pages/Northbound/DuerOS/types.d.ts

@@ -17,7 +17,10 @@ type DuerOSItem = {
   version: number;
   manufacturerName: string;
   autoReportProperty: boolean;
-  applianceType: string;
+  applianceType: {
+    text: string;
+    value: string;
+  };
   actionMappings: ActionMapping[];
   propertyMappings: PropertyMapping[];
 } & BaseItem;

+ 64 - 0
src/pages/account/NotificationRecord/detail/index.tsx

@@ -0,0 +1,64 @@
+import { Descriptions, Modal } from 'antd';
+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<NotifitionRecord>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<any>({});
+
+  useEffect(() => {
+    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 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="告警时间" span={1}>
+          {moment(data?.alarmTime).format('YYYY-MM-DD HH:mm:ss')}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警级别" span={1}>
+          {(Store.get('default-level') || []).find((item: any) => item?.level === data?.level)
+            ?.title || 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 Detail;

+ 164 - 0
src/pages/account/NotificationRecord/index.tsx

@@ -0,0 +1,164 @@
+import { useIntl } from '@/.umi/plugin-locale/localeExports';
+import PermissionButton from '@/components/PermissionButton';
+import SearchComponent from '@/components/SearchComponent';
+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 { 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 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<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: '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: 'message',
+      title: '消息',
+    },
+    {
+      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({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          key={'update'}
+          type={'link'}
+          isPermission={true}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: record?.state?.value !== 'read' ? '标为已读' : '标为未读',
+          }}
+          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 />
+        </PermissionButton>,
+        <PermissionButton
+          key={'action'}
+          type={'link'}
+          isPermission={true}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: '查看',
+          }}
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+        >
+          <SearchOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<NotifitionRecord>
+        field={columns}
+        target="notification-record"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<NotifitionRecord>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        request={async (params) =>
+          service.queryList(encodeQuery({ ...params, sorts: { notifyTime: 'desc' } }))
+        }
+      />
+      {visible && (
+        <Detail
+          close={() => {
+            setCurrent({});
+            setVisible(false);
+          }}
+          data={current}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default NotificationRecord;

+ 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;

+ 12 - 0
src/pages/account/NotificationRecord/typings.d.ts

@@ -0,0 +1,12 @@
+type NotifitionRecord = {
+  id: string;
+  topicProvider: string;
+  message: string;
+  notifyTime: string;
+  state?: any;
+  subscribeId: string;
+  subscriberType: string;
+  subscriberType: string;
+  topicName: string;
+  dataId: string;
+};

+ 216 - 0
src/pages/account/NotificationSubscription/index.tsx

@@ -0,0 +1,216 @@
+import { useIntl } from '@/.umi/plugin-locale/localeExports';
+import PermissionButton from '@/components/PermissionButton';
+import SearchComponent from '@/components/SearchComponent';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  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 { Badge, message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import Save from './save';
+import Service from './service';
+
+export const service = new Service('notifications/subscriptions');
+
+const NotificationSubscription = observer(() => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  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 [
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        isPermission={true}
+        style={{ padding: 0 }}
+        tooltip={{
+          title: intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          }),
+        }}
+        onClick={() => {
+          setVisible(true);
+          setCurrent(record);
+        }}
+      >
+        <EditOutlined />
+      </PermissionButton>,
+      <PermissionButton
+        key={'action'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={true}
+        popConfirm={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${
+              record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          }),
+          onConfirm: async () => {
+            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={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+            defaultMessage: '启用',
+          }),
+        }}
+      >
+        {record?.state?.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+      </PermissionButton>,
+      <PermissionButton
+        key={'delete'}
+        type={'link'}
+        isPermission={true}
+        style={{ padding: 0 }}
+        popConfirm={{
+          title: '确认删除?',
+          onConfirm: async () => {
+            const resp: any = await service.remove(record.id);
+            if (resp.status === 200) {
+              message.success('操作成功!');
+              actionRef.current?.reload?.();
+            } else {
+              message.error('操作失败!');
+            }
+          },
+        }}
+        tooltip={{
+          title: '删除',
+        }}
+      >
+        <DeleteOutlined />
+      </PermissionButton>,
+    ];
+  };
+
+  const columns: ProColumns<NotifitionSubscriptionItem>[] = [
+    {
+      dataIndex: 'subscribeName',
+      title: '名称',
+    },
+    {
+      dataIndex: 'topicProvider',
+      title: '类型',
+      hideInSearch: true,
+      render: (text: any, record: any) => {
+        return <span>{typeList[record?.topicProvider] || text}</span>;
+      },
+    },
+    {
+      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: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [Tools(record)],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<NotifitionSubscriptionItem>
+        field={columns}
+        target="notification-subscription"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<NotifitionSubscriptionItem>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        headerTitle={[
+          <PermissionButton
+            onClick={() => {
+              setVisible(true);
+              setCurrent({});
+            }}
+            isPermission={true}
+            style={{ marginRight: 12 }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>,
+        ]}
+      />
+      {visible && (
+        <Save
+          close={() => {
+            setVisible(false);
+          }}
+          data={current}
+          reload={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+});
+
+export default NotificationSubscription;

+ 180 - 0
src/pages/account/NotificationSubscription/save/index.tsx

@@ -0,0 +1,180 @@
+import { message, Modal } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import { Checkbox, Form, FormGrid, FormItem, Input, Select } 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<NotifitionSubscriptionItem>;
+  close: () => void;
+  reload: () => void;
+}
+
+const Save = (props: Props) => {
+  const [data, setDada] = useState<Partial<NotifitionSubscriptionItem>>(props.data || {});
+  const [dataList, setDataList] = useState<any[]>([]);
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: data,
+      }),
+    [],
+  );
+
+  const queryProvidersList = () => service.getProvidersList();
+
+  const queryAlarmConfigList = () => {
+    return service.getAlarmConfigList().then((resp) => {
+      setDataList(resp);
+      return resp;
+    });
+  };
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      grid: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 1,
+        },
+        properties: {
+          subscribeName: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 2,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          topicProvider: {
+            title: '类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 1,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            'x-component-props': {
+              placeholder: '请选择类型',
+            },
+            'x-reactions': ['{{useAsyncDataSource(queryProvidersList)}}'],
+          },
+          'topicConfig.alarmConfigId': {
+            title: '告警规则',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 1,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            'x-component-props': {
+              placeholder: '请选择告警规则',
+            },
+            'x-reactions': ['{{useAsyncDataSource(queryAlarmConfigList)}}'],
+          },
+          notice: {
+            title: '通知方式',
+            type: 'array',
+            required: true,
+            'x-disabled': true,
+            default: [1],
+            enum: [
+              {
+                label: '站内通知',
+                value: 1,
+              },
+              {
+                label: '邮件通知',
+                value: 2,
+              },
+              {
+                label: '短信通知',
+                value: 3,
+              },
+            ],
+            'x-decorator': 'FormItem',
+            'x-component': 'Checkbox.Group',
+            'x-decorator-props': {
+              gridSpan: 2,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      Select,
+      Checkbox,
+    },
+  });
+
+  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={() => handleSave()} width={'45vw'}>
+      <Form form={form} layout="vertical">
+        <SchemaField
+          schema={schema}
+          scope={{
+            useAsyncDataSource,
+            queryProvidersList,
+            queryAlarmConfigList,
+          }}
+        />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 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;

+ 9 - 0
src/pages/account/NotificationSubscription/typings.d.ts

@@ -0,0 +1,9 @@
+type NotifitionSubscriptionItem = {
+  id?: string;
+  subscribeName: string;
+  topicProvider: string;
+  topicConfig: {
+    alarmConfigId: string;
+    alarmConfigName: string;
+  };
+};

+ 4 - 1
src/pages/cloud/DuerOS/typings.d.ts

@@ -31,7 +31,10 @@ type DuerOSItem = {
   version: number;
   manufacturerName: string;
   autoReportProperty: boolean;
-  applianceType: string;
+  applianceType: {
+    text: string;
+    value: string;
+  };
   actionMappings: ActionMapping[];
   propertyMappings: PropertyMapping[];
 } & BaseItem;

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

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

@@ -11,7 +11,7 @@ const Detail = (props: Props) => {
   const { value, type } = props;
 
   const renderValue = () => {
-    if (type === 'object') {
+    if (type === 'object' || type === 'array') {
       return (
         <div>
           <div>自定义属性</div>
@@ -29,6 +29,13 @@ const Detail = (props: Props) => {
           </div>
         </div>
       );
+    } else if (type === 'file') {
+      return (
+        <div>
+          <div>自定义属性</div>
+          <Input.TextArea value={value} rows={3} />
+        </div>
+      );
     } else {
       return (
         <div>

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

@@ -1,16 +1,6 @@
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import {
-  DatePicker,
-  Modal,
-  Popconfirm,
-  Radio,
-  Select,
-  Space,
-  Table,
-  Tabs,
-  Tooltip as ATooltip,
-} from 'antd';
+import { DatePicker, Modal, Radio, Select, Space, Table, Tabs, Tooltip as ATooltip } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import encodeQuery from '@/utils/encodeQuery';
 import { useEffect, useState } from 'react';
@@ -22,14 +12,13 @@ import Detail from './Detail';
 import AMap from './AMap';
 
 interface Props {
-  visible: boolean;
   close: () => void;
   data: Partial<PropertyMetadata>;
 }
 
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
-  const { visible, close, data } = props;
+  const { close, data } = props;
   const list = ['int', 'float', 'double', 'long'];
   const [dataSource, setDataSource] = useState<any>({});
   const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
@@ -45,19 +34,21 @@ const PropertyLog = (props: Props) => {
   const [detailVisible, setDetailVisible] = useState<boolean>(false);
   const [current, setCurrent] = useState<any>('');
 
-  const [geoList, setGeoList] = useState<any[]>([]);
+  const [geoList, setGeoList] = useState<any>({});
 
   const columns = [
     {
       title: '时间',
       dataIndex: 'timestamp',
       key: 'timestamp',
+      ellipsis: true,
       render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
     },
     {
       title: <span>{data.valueType?.type !== 'file' ? '自定义属性' : '文件内容'}</span>,
       dataIndex: 'value',
       key: 'value',
+      ellipsis: true,
       render: (text: any, record: any) => (
         <FileComponent type="table" value={{ formatValue: record.value }} data={data} />
       ),
@@ -68,22 +59,15 @@ const PropertyLog = (props: Props) => {
       key: 'action',
       render: (text: any, record: any) => (
         <a>
-          {data.valueType?.type !== 'file' ? (
-            <SearchOutlined
-              onClick={() => {
-                setDetailVisible(true);
-                setCurrent(record.value);
-              }}
-            />
-          ) : (
+          {data.valueType?.type === 'file' && data?.valueType?.fileType == 'url' ? (
             <ATooltip title="下载">
-              <Popconfirm
-                title="确认修改"
-                onConfirm={() => {
+              <DownloadOutlined
+                onClick={() => {
                   const type = (record?.value || '').split('.').pop();
                   const downloadUrl = record.value;
                   const downNode = document.createElement('a');
                   downNode.href = downloadUrl;
+                  downNode.target = '_blank';
                   downNode.download = `${InstanceModel.detail.name}-${data.name}${moment(
                     new Date().getTime(),
                   ).format('YYYY-MM-DD-HH-mm-ss')}.${type}`;
@@ -92,10 +76,15 @@ const PropertyLog = (props: Props) => {
                   downNode.click();
                   document.body.removeChild(downNode);
                 }}
-              >
-                <DownloadOutlined />
-              </Popconfirm>
+              />
             </ATooltip>
+          ) : (
+            <SearchOutlined
+              onClick={() => {
+                setDetailVisible(true);
+                setCurrent(record.value);
+              }}
+            />
           )}
         </a>
       ),
@@ -190,23 +179,18 @@ const PropertyLog = (props: Props) => {
   };
 
   useEffect(() => {
-    if (visible) {
-      handleSearch(
-        {
-          pageSize: 10,
-          pageIndex: 0,
-        },
-        start,
-        new Date().getTime(),
-      );
-    }
-  }, [visible]);
+    setRadioValue('today');
+    setTab('table');
+    setStart(moment().startOf('day').valueOf());
+    setEnd(new Date().getTime());
+  }, []);
 
   const scale = {
     value: { min: 0 },
     year: {
-      range: [0, 0.96],
+      range: [0, 1],
       type: 'timeCat',
+      mask: 'YYYY-MM-DD HH:mm:ss',
     },
   };
 
@@ -358,15 +342,62 @@ const PropertyLog = (props: Props) => {
     }
   };
 
+  useEffect(() => {
+    if (tab === 'table') {
+      handleSearch(
+        {
+          pageSize: 10,
+          pageIndex: 0,
+        },
+        start,
+        end,
+      );
+    } else if (tab === 'charts') {
+      if (list.includes(data.valueType?.type || '')) {
+        queryChartsList(start, end);
+      } else {
+        queryChartsAggList({
+          columns: [
+            {
+              property: data.id,
+              alias: data.id,
+              agg,
+            },
+          ],
+          query: {
+            interval: cycle,
+            format: 'yyyy-MM-dd HH:mm:ss',
+            from: start,
+            to: end,
+          },
+        });
+      }
+    } else if (tab === 'geo') {
+      service
+        .getPropertyData(
+          params.id,
+          encodeQuery({
+            paging: false,
+            terms: { property: data?.id, timestamp$BTW: start && start ? [start, end] : [] },
+            sorts: { timestamp: 'asc' },
+          }),
+        )
+        .then((resp) => {
+          if (resp.status === 200) {
+            setGeoList(resp.result);
+          }
+        });
+    }
+  }, [start, end]);
+
   // @ts-ignore
   return (
     <Modal
       maskClosable={false}
       title="详情"
-      visible={visible}
+      visible
       onCancel={() => close()}
       onOk={() => close()}
-      destroyOnClose={true}
       width="50vw"
     >
       <div style={{ marginBottom: '20px' }}>
@@ -389,36 +420,6 @@ const PropertyLog = (props: Props) => {
               setDateValue(undefined);
               setStart(st);
               setEnd(et);
-              if (tab === 'charts') {
-                if (list.includes(data.valueType?.type || '')) {
-                  queryChartsList(st, et);
-                } else {
-                  queryChartsAggList({
-                    columns: [
-                      {
-                        property: data.id,
-                        alias: data.id,
-                        agg,
-                      },
-                    ],
-                    query: {
-                      interval: cycle,
-                      format: 'yyyy-MM-dd HH:mm:ss',
-                      from: st,
-                      to: et,
-                    },
-                  });
-                }
-              } else {
-                handleSearch(
-                  {
-                    pageSize: 10,
-                    pageIndex: 0,
-                  },
-                  st,
-                  et,
-                );
-              }
             }}
             style={{ minWidth: 220 }}
           >
@@ -439,14 +440,6 @@ const PropertyLog = (props: Props) => {
                   const et = dates[1]?.valueOf();
                   setStart(st);
                   setEnd(et);
-                  handleSearch(
-                    {
-                      pageSize: 10,
-                      pageIndex: 0,
-                    },
-                    st,
-                    et,
-                  );
                 }
               }}
             />
@@ -487,7 +480,7 @@ const PropertyLog = (props: Props) => {
                 encodeQuery({
                   paging: false,
                   terms: { property: data.id, timestamp$BTW: start && end ? [start, end] : [] },
-                  sorts: { timestamp: 'desc' },
+                  sorts: { timestamp: 'asc' },
                 }),
               )
               .then((resp) => {
@@ -496,6 +489,16 @@ const PropertyLog = (props: Props) => {
                 }
               });
           }
+          if (key === 'table') {
+            handleSearch(
+              {
+                pageSize: 10,
+                pageIndex: 0,
+              },
+              start,
+              end,
+            );
+          }
         }}
       >
         {tabList.map((item) => (
@@ -505,7 +508,7 @@ const PropertyLog = (props: Props) => {
         ))}
         {data?.valueType?.type === 'geoPoint' && (
           <Tabs.TabPane tab="轨迹" key="geo">
-            <AMap value={geoList} name={data.name} />
+            <AMap value={geoList} name={data?.name || ''} />
           </Tabs.TabPane>
         )}
       </Tabs>

+ 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 (

+ 4 - 6
src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx

@@ -1,5 +1,5 @@
 import LivePlayer from '@/components/Player';
-import { Modal, Image } from 'antd';
+import { Image, Modal } from 'antd';
 
 interface Props {
   close: () => void;
@@ -11,14 +11,12 @@ const Detail = (props: Props) => {
   const { value, type } = props;
 
   const renderValue = () => {
-    if (['jpg', 'png', 'tiff'].includes(type)) {
+    if (['.jpg', '.png'].includes(type)) {
       return <Image src={value?.formatValue} />;
-    } else if (value?.formatValue.indexOf('https') !== -1) {
-      return <p>域名为https时,不支持访问http地址</p>;
-    } else if (['flv', 'm3u8', 'mp4'].includes(type)) {
+    } else if (['.flv', '.m3u8', '.mp4'].includes(type)) {
       return <LivePlayer live={false} url={value?.formatValue} />;
     }
-    return <p>当前仅支持播放.mp4,.flv,.m3u8格式的视频</p>;
+    return null;
   };
 
   return (

+ 4 - 1
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less

@@ -20,6 +20,9 @@
     justify-content: center;
     width: 60px;
     height: 100%;
-    border: 1px solid rgba(0, 0, 0, 0.08);
+
+    img {
+      width: 100%;
+    }
   }
 }

+ 91 - 26
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -2,6 +2,7 @@ import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import styles from './index.less';
 import Detail from './Detail';
 import { useState } from 'react';
+import { message, Tooltip } from 'antd';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -17,52 +18,116 @@ imgMap.set('ppt', require('/public/images/running/ppt.png'));
 imgMap.set('docx', require('/public/images/running/docx.png'));
 imgMap.set('xlsx', require('/public/images/running/xlsx.png'));
 imgMap.set('pptx', require('/public/images/running/pptx.png'));
-imgMap.set('jpg', require('/public/images/running/jpg.png'));
-imgMap.set('png', require('/public/images/running/png.png'));
 imgMap.set('pdf', require('/public/images/running/pdf.png'));
-imgMap.set('tiff', require('/public/images/running/tiff.png'));
-imgMap.set('swf', require('/public/images/running/swf.png'));
-imgMap.set('flv', require('/public/images/running/flv.png'));
-imgMap.set('rmvb', require('/public/images/running/rmvb.png'));
-imgMap.set('mp4', require('/public/images/running/mp4.png'));
-imgMap.set('mvb', require('/public/images/running/mvb.png'));
-imgMap.set('wma', require('/public/images/running/wma.png'));
-imgMap.set('mp3', require('/public/images/running/mp3.png'));
+imgMap.set('img', require('/public/images/running/img.png'));
+imgMap.set('error', require('/public/images/running/error.png'));
+imgMap.set('video', require('/public/images/running/video.png'));
 imgMap.set('other', require('/public/images/running/other.png'));
 
 const FileComponent = (props: Props) => {
   const { data, value } = props;
   const [type, setType] = useState<string>('other');
   const [visible, setVisible] = useState<boolean>(false);
+  const isHttps = document.location.protocol === 'https:';
 
   const renderValue = () => {
     if (!value?.formatValue) {
       return <div className={props.type === 'card' ? styles.other : {}}>--</div>;
     } else if (data?.valueType?.type === 'file') {
-      const flag: string = value?.formatValue.split('.').pop() || 'other';
-      return (
-        <div
-          className={styles.img}
-          onClick={() => {
-            if (['jpg', 'png', 'tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
-              setType(flag);
-              setVisible(true);
-            }
-          }}
-        >
-          <img src={imgMap.get(flag) || imgMap.get('other')} />
-        </div>
-      );
+      if (
+        data?.valueType?.fileType === 'base64' ||
+        data?.valueType?.fileType === 'Binary(二进制)'
+      ) {
+        return (
+          <div className={props.type === 'card' ? styles.other : {}}>
+            <Tooltip placement="topLeft" title={String(value?.formatValue)}>
+              {String(value?.formatValue)}
+            </Tooltip>
+          </div>
+        );
+      }
+      if (['.jpg', '.png'].some((item) => value?.formatValue.includes(item))) {
+        // 图片
+        return (
+          <div
+            className={styles.img}
+            onClick={() => {
+              if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
+                message.error('域名为https时,不支持访问http地址');
+              } else {
+                const flag =
+                  ['.jpg', '.png'].find((item) => value?.formatValue.includes(item)) || '';
+                setType(flag);
+                setVisible(true);
+              }
+            }}
+          >
+            <img
+              src={value?.formatValue}
+              onError={(e: any) => {
+                e.target.src = imgMap.get('error');
+              }}
+            />
+          </div>
+        );
+      }
+      if (
+        ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'].some((item) => value?.formatValue.includes(item))
+      ) {
+        return (
+          <div
+            className={styles.img}
+            onClick={() => {
+              if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
+                message.error('域名为https时,不支持访问http地址');
+              } else if (['.rmvb', '.mvb'].some((item) => value?.formatValue.includes(item))) {
+                message.error('当前仅支持播放.mp4,.flv,.m3u8格式的视频');
+              } else {
+                const flag =
+                  ['.m3u8', '.flv', '.mp4'].find((item) => value?.formatValue.includes(item)) || '';
+                setType(flag);
+                setVisible(true);
+              }
+            }}
+          >
+            <img src={imgMap.get('video')} />
+          </div>
+        );
+      } else if (
+        ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'].some((item) =>
+          value?.formatValue.includes(item),
+        )
+      ) {
+        const flag =
+          ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'].find((item) =>
+            value?.formatValue.includes(item),
+          ) || '';
+        return (
+          <div className={styles.img}>
+            <img src={imgMap.get(flag.slice(1))} />
+          </div>
+        );
+      } else {
+        return (
+          <div className={styles.img}>
+            <img src={imgMap.get('other')} />
+          </div>
+        );
+      }
     } else if (data?.valueType?.type === 'object' || data?.valueType?.type === 'geoPoint') {
       return (
         <div className={props.type === 'card' ? styles.other : {}}>
-          {JSON.stringify(value?.formatValue)}
+          <Tooltip placement="topLeft" title={JSON.stringify(value?.formatValue)}>
+            {JSON.stringify(value?.formatValue)}
+          </Tooltip>
         </div>
       );
     } else {
       return (
         <div className={props.type === 'card' ? styles.other : {}}>
-          {String(value?.formatValue)}
+          <Tooltip placement="topLeft" title={String(value?.formatValue)}>
+            {String(value?.formatValue)}
+          </Tooltip>
         </div>
       );
     }

+ 1 - 1
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -105,7 +105,7 @@ const Property = (props: Props) => {
         }}
         data={data}
       />
-      <PropertyLog data={data} visible={visible} close={() => setVisible(false)} />
+      {visible && <PropertyLog data={data} close={() => setVisible(false)} />}
       {indicatorVisible && (
         <Indicators
           data={data}

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

@@ -248,7 +248,7 @@ const Property = (props: Props) => {
           setVisible(false);
         }}
       />
-      <PropertyLog data={currentInfo} visible={infoVisible} close={() => setInfoVisible(false)} />
+      {infoVisible && <PropertyLog data={currentInfo} close={() => setInfoVisible(false)} />}
     </div>
   );
 };

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

@@ -36,7 +36,7 @@ const Running = () => {
       <Tabs
         defaultActiveKey="1"
         tabPosition="left"
-        style={{ height: 600 }}
+        style={{ minHeight: 600 }}
         tabBarExtraContent={{ left: operations() }}
       >
         <Tabs.TabPane tab="属性" key="1">

+ 68 - 0
src/pages/link/Certificate/Detail/components/CertificateFile/index.tsx

@@ -0,0 +1,68 @@
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { UploadOutlined } from '@ant-design/icons';
+import { Button, Input, message, Upload } from 'antd';
+import type { UploadChangeParam } from 'antd/lib/upload';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value?: string;
+  onChange?: (type: any) => void;
+}
+
+const CertificateFile = (props: Props) => {
+  const [keystoreBase64, setKeystoreBase64] = useState<string>('');
+
+  useEffect(() => {
+    setKeystoreBase64(props?.value || '');
+  }, [props.value]);
+
+  const handleChange = (info: UploadChangeParam) => {
+    if (info.file.status === 'done') {
+      const {
+        file: { response },
+      } = info;
+      if (response.status === 200) {
+        message.success('上传成功');
+        setKeystoreBase64(response.result);
+        if (props.onChange) {
+          props.onChange(response.result);
+        }
+      }
+    } else if (info.file.status === 'error') {
+      message.error(`${info.file.name} file upload failed.`);
+    }
+  };
+
+  return (
+    <div>
+      <Input.TextArea
+        onChange={(e) => {
+          setKeystoreBase64(e.target.value);
+          if (props.onChange) {
+            props.onChange(e.target.value);
+          }
+        }}
+        value={keystoreBase64}
+        rows={4}
+        placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾。'
+      />
+      <Upload
+        accept=".pem"
+        listType={'text'}
+        action={`/${SystemConst.API_BASE}/network/certificate/upload`}
+        headers={{
+          'X-Access-Token': Token.get(),
+        }}
+        onChange={handleChange}
+        showUploadList={false}
+      >
+        <Button style={{ marginTop: 10 }} icon={<UploadOutlined />}>
+          上传文件
+        </Button>
+      </Upload>
+    </div>
+  );
+};
+
+export default CertificateFile;

+ 38 - 0
src/pages/link/Certificate/Detail/components/Standard/index.tsx

@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value?: string;
+  onChange?: (type: any) => void;
+}
+
+const Standard = (props: Props) => {
+  const imgMap = new Map<any, any>();
+  imgMap.set('common', require('/public/images/certificate.png'));
+
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  return (
+    <div>
+      {['common'].map((i) => (
+        <div
+          onClick={() => {
+            setType(i);
+            if (props.onChange) {
+              props.onChange(i);
+            }
+          }}
+          style={{ width: 150, border: type === i ? '1px solid #2F54EB' : '' }}
+          key={i}
+        >
+          <img style={{ width: '100%' }} src={imgMap.get(i)} />
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Standard;

+ 143 - 0
src/pages/link/Certificate/Detail/index.tsx

@@ -0,0 +1,143 @@
+import PermissionButton from '@/components/PermissionButton';
+import usePermissions from '@/hooks/permission';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Form, FormButtonGroup, FormItem } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { Card, Col, Input, message, Row } from 'antd';
+import { createSchemaField, observer } from '@formily/react';
+import { useEffect, useMemo } from 'react';
+import { createForm } from '@formily/core';
+import CertificateFile from './components/CertificateFile';
+import Standard from './components/Standard';
+import { service } from '@/pages/link/Certificate';
+import { useParams } from 'umi';
+
+const Detail = observer(() => {
+  const params = useParams<{ id: string }>();
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+      }),
+    [],
+  );
+
+  useEffect(() => {
+    if (params.id && params.id !== ':id') {
+      service.detail(params.id).then((resp) => {
+        if (resp.status === 200) {
+          form.setValues(resp.result);
+        }
+      });
+    }
+  }, [params.id]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      CertificateFile,
+      Standard,
+    },
+  });
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      type: {
+        type: 'string',
+        title: '证书标准',
+        required: true,
+        default: 'common',
+        'x-decorator': 'FormItem',
+        'x-component': 'Standard',
+      },
+      name: {
+        type: 'string',
+        title: '证书名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入证书名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+      },
+      'configs.cert': {
+        title: '证书文件',
+        'x-component': 'CertificateFile',
+        'x-decorator': 'FormItem',
+        required: true,
+        'x-component-props': {
+          rows: 3,
+          placeholder:
+            '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
+        },
+      },
+      'configs.key': {
+        title: '证书私钥',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        required: true,
+        'x-component-props': {
+          rows: 3,
+          placeholder:
+            '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
+        },
+      },
+      description: {
+        title: '说明',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        'x-component-props': {
+          rows: 3,
+          showCount: true,
+          maxLength: 200,
+          placeholder: '请输入说明',
+        },
+      },
+    },
+  };
+
+  const { getOtherPermission } = usePermissions('link/Certificate');
+
+  return (
+    <PageContainer>
+      <Card>
+        <Row gutter={24}>
+          <Col span={12}>
+            <Form form={form} layout="vertical">
+              <SchemaField schema={schema} />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <PermissionButton
+                    type="primary"
+                    isPermission={getOtherPermission(['add', 'update'])}
+                    onClick={async () => {
+                      const data: any = await form.submit();
+                      const response: any = data.id
+                        ? await service.update(data)
+                        : await service.save(data);
+                      if (response.status === 200) {
+                        message.success('操作成功');
+                        history.back();
+                      }
+                    }}
+                  >
+                    保存
+                  </PermissionButton>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+});
+
+export default Detail;

+ 79 - 151
src/pages/link/Certificate/index.tsx

@@ -1,46 +1,37 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseService from '@/utils/BaseService';
-import type { CertificateItem } from '@/pages/link/Certificate/typings';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { message, Popconfirm, Tooltip } from 'antd';
-import { EditOutlined, MinusOutlined } from '@ant-design/icons';
-import BaseCrud from '@/components/BaseCrud';
+import ProTable from '@jetlinks/pro-table';
+import { message } from 'antd';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
-import { CurdModel } from '@/components/BaseCrud/model';
+import SearchComponent from '@/components/SearchComponent';
+import PermissionButton from '@/components/PermissionButton';
+import usePermissions from '@/hooks/permission';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { history } from 'umi';
+import Service from '../service';
+
+export const service = new Service('network/certificate');
 
-export const service = new BaseService<CertificateItem>('network/certificate');
 const Certificate = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const { permission } = usePermissions('link/Certificate');
 
   const columns: ProColumns<CertificateItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
+      dataIndex: 'type',
+      title: '证书标准',
     },
     {
       dataIndex: 'name',
-      title: intl.formatMessage({
-        id: 'pages.table.name',
-        defaultMessage: '名称',
-      }),
-    },
-    {
-      dataIndex: 'instance',
-      title: intl.formatMessage({
-        id: 'pages.link.type',
-        defaultMessage: '类型',
-      }),
+      title: '证书名称',
     },
     {
       dataIndex: 'description',
-      title: intl.formatMessage({
-        id: 'pages.table.describe',
-        defaultMessage: '描述',
-      }),
+      title: '说明',
     },
     {
       title: intl.formatMessage({
@@ -51,29 +42,29 @@ const Certificate = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a
-          key="edit"
+        <PermissionButton
+          key={'update'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.update}
+          tooltip={{
+            title: '编辑',
+          }}
           onClick={() => {
-            CurdModel.update(record);
-            CurdModel.model = 'edit';
+            const url = `${getMenuPathByParams(MENUS_CODE['link/Certificate/Detail'], record.id)}`;
+            history.push(url);
           }}
         >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a key="delete">
-          <Popconfirm
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove.tips',
-              defaultMessage: '确认删除?',
-            })}
-            onConfirm={async () => {
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'delete'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.delete}
+          popConfirm={{
+            title: '确认删除?',
+            onConfirm: async () => {
               await service.remove(record.id);
               message.success(
                 intl.formatMessage({
@@ -82,117 +73,54 @@ const Certificate = () => {
                 }),
               );
               actionRef.current?.reload();
-            }}
-          >
-            <Tooltip
-              title={intl.formatMessage({
-                id: 'pages.data.option.remove',
-                defaultMessage: '删除',
-              })}
-            >
-              <MinusOutlined />
-            </Tooltip>
-          </Popconfirm>
-        </a>,
+            },
+          }}
+          tooltip={{
+            title: '删除',
+          }}
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
       ],
     },
   ];
 
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      name: {
-        title: '名称',
-        'x-component': 'Input',
-        'x-decorator': 'FormItem',
-      },
-      instance: {
-        title: '类型',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-        default: 'PEM',
-        enum: [
-          { label: 'PFX', value: 'PFX' },
-          { label: 'JKS', value: 'JKS' },
-          { label: 'PEM', value: 'PEM' },
-        ],
-      },
-      configs: {
-        type: 'object',
-        properties: {
-          '{url:keystoreBase64}': {
-            title: '密钥库',
-            'x-component': 'FUpload',
-            'x-decorator': 'FormItem',
-            'x-component-props': {
-              type: 'file',
-            },
-          },
-          keystorePwd: {
-            title: '密钥库密码',
-            'x-component': 'Password',
-            'x-decorator': 'FormItem',
-            'x-visible': false,
-            'x-component-props': {
-              style: {
-                width: '100%',
-              },
-            },
-            'x-reactions': {
-              dependencies: ['..instance'],
-              fulfill: {
-                state: {
-                  visible: '{{["JKS","PFX"].includes($deps[0])}}',
-                },
-              },
-            },
-          },
-          '{url:trustKeyStoreBase64}': {
-            title: '信任库',
-            'x-component': 'FUpload',
-            'x-decorator': 'FormItem',
-            'x-component-props': {
-              style: {
-                width: '100px',
-              },
-              type: 'file',
-            },
-          },
-          trustKeyStorePwd: {
-            title: '信任库密码',
-            'x-visible': false,
-            'x-decorator': 'FormItem',
-            'x-component': 'Password',
-            'x-reactions': {
-              dependencies: ['..instance'],
-              fulfill: {
-                state: {
-                  visible: '{{["JKS","PFX"].includes($deps[0])}}',
-                },
-              },
-            },
-          },
-        },
-      },
-      description: {
-        title: '说明',
-        'x-component': 'Input.TextArea',
-        'x-decorator': 'FormItem',
-      },
-    },
-  };
-
   return (
     <PageContainer>
-      <BaseCrud
-        columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.link.certificate',
-          defaultMessage: '证书管理',
-        })}
-        schema={schema}
+      <SearchComponent<CertificateItem>
+        field={columns}
+        target="certificate"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<CertificateItem>
         actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              const url = `${getMenuPathByParams(MENUS_CODE['link/Certificate/Detail'])}`;
+              history.push(url);
+            }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
       />
     </PageContainer>
   );

+ 14 - 0
src/pages/link/Certificate/service.ts

@@ -0,0 +1,14 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<CertificateItem> {
+  // 上传证书并返回证书BASE64
+  public uploadCertificate = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/network/certificate/upload`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 7 - 6
src/pages/link/Certificate/typings.d.ts

@@ -1,7 +1,8 @@
-import type { BaseItem } from '@/utils/typings';
-
 type CertificateItem = {
-  instance: string;
-  description: string;
-  configs?: Record<string, any>;
-} & BaseItem;
+  id: string;
+  type: string;
+  name: string;
+  cert: string;
+  key: string;
+  description?: string;
+};

+ 2 - 2
src/pages/notice/Config/Detail/index.tsx

@@ -385,7 +385,7 @@ const Detail = observer(() => {
                     'x-component': 'NumberPicker',
                     'x-decorator': 'FormItem',
                     'x-reactions': {
-                      dependencies: ['.enableSSL'],
+                      dependencies: ['.ssl'],
                       when: '{{$deps[0]}}',
                       fulfill: {
                         state: {
@@ -399,7 +399,7 @@ const Detail = observer(() => {
                       },
                     },
                   },
-                  enableSSL: {
+                  ssl: {
                     // title: '开启SSL',
                     type: 'boolean',
                     'x-component': 'Checkbox',

+ 20 - 4
src/pages/notice/Config/SyncUser/index.tsx

@@ -104,14 +104,20 @@ const SyncUser = observer(() => {
   /**
    * 获取部门列表
    */
-  const getDepartment = async () => {
+  const getDepartment = async (name?: string) => {
     if (state.current?.id) {
       if (id === 'dingTalk') {
         service.syncUser
           .dingTalkDept(state.current?.id)
           .then((resp) => {
             if (resp.status === 200) {
-              setTreeData(resp.result);
+              let _data = resp.result;
+              if (name) {
+                _data = resp.result?.filter(
+                  (item: { id: string; name: string }) => item.name.indexOf(name) > -1,
+                );
+              }
+              setTreeData(_data);
               setDept(resp.result[0].id);
             }
           })
@@ -121,7 +127,13 @@ const SyncUser = observer(() => {
           .wechatDept(state.current?.id)
           .then((resp) => {
             if (resp.status === 200) {
-              setTreeData(resp.result);
+              let __data = resp.result;
+              if (name) {
+                __data = resp.result?.filter(
+                  (item: { id: string; name: string }) => item.name.indexOf(name) > -1,
+                );
+              }
+              setTreeData(__data);
               setDept(resp.result[0].id);
             }
           })
@@ -149,7 +161,11 @@ const SyncUser = observer(() => {
         <Row>
           <Col span={4}>
             <div style={{ borderRight: 'lightgray 1px solid', padding: '2px', height: '600px' }}>
-              <Input.Search style={{ marginBottom: 8 }} placeholder="请输入部门名称" />
+              <Input.Search
+                onSearch={(value) => getDepartment(value)}
+                style={{ marginBottom: 8 }}
+                placeholder="请输入部门名称"
+              />
               <Tree
                 fieldNames={{
                   title: 'name',

+ 5 - 5
src/pages/notice/index.tsx

@@ -15,7 +15,7 @@ const createImageLabel = (image: string, text: string) => {
   );
 };
 const weixinCorp = require('/public/images/notice/weixin-corp.png');
-const weixinOfficial = require('/public/images/notice/weixin-official.png');
+// const weixinOfficial = require('/public/images/notice/weixin-official.png');
 const dingTalkMessage = require('/public/images/notice/dingTalk-message.png');
 const dingTalkRebot = require('/public/images/notice/dingTalk-rebot.png');
 const sms = require('/public/images/notice/sms.png');
@@ -28,10 +28,10 @@ export const typeList = {
       label: createImageLabel(weixinCorp, '企业消息'),
       value: 'corpMessage',
     },
-    {
-      label: createImageLabel(weixinOfficial, '服务号消息'),
-      value: 'officialMessage',
-    },
+    // {
+    //   label: createImageLabel(weixinOfficial, '服务号消息'),
+    //   value: 'officialMessage',
+    // },
   ],
   dingTalk: [
     {

+ 16 - 0
src/utils/menu/index.ts

@@ -74,6 +74,22 @@ export const extraRouteArr = [
         name: '基本设置',
         url: '/account/center',
       },
+      {
+        code: 'account/Center/bind',
+        name: '第三方页面',
+        url: '/account/center/bind',
+        hideInMenu: true,
+      },
+      {
+        code: 'account/NotificationSubscription',
+        name: '通知订阅',
+        url: '/account/NotificationSubscription',
+      },
+      {
+        code: 'account/NotificationRecord',
+        name: '通知记录',
+        url: '/account/NotificationRecord',
+      },
       // {
       //   code: 'account/Center/bind',
       //   name: '第三方页面',

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

@@ -33,6 +33,7 @@ export enum MENUS_CODE {
   'edge/Device' = 'edge/Device',
   'edge/Product' = 'edge/Product',
   'link/Certificate' = 'link/Certificate',
+  'link/Certificate/Detail' = 'link/Certificate/Detail',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
   'link/Channel/Opcua' = 'link/Channel/Opcua',
@@ -112,6 +113,8 @@ export enum MENUS_CODE {
   'system/Department/Detail' = 'system/Department/Detail',
   'link/Type/Detail' = 'link/Type/Detail',
   'account/Center' = 'account/Center',
+  'account/NotificationSubscription' = 'account/NotificationSubscription',
+  'account/NotificationRecord' = 'account/NotificationRecord',
   'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/DuerOS/Detail' = 'Northbound/DuerOS/Detail',
@@ -161,4 +164,5 @@ export const getDetailNameByCode = {
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
   'Northbound/AliCloud/Detail': '阿里云详情',
+  'link/Certificate/Detail': '证书详情',
 };