Jelajahi Sumber

fix(merge): merge sc

lind 3 tahun lalu
induk
melakukan
cb8868df18
59 mengubah file dengan 1469 tambahan dan 332 penghapusan
  1. TEMPAT SAMPAH
      public/images/certificate.png
  2. TEMPAT SAMPAH
      public/images/running/doc.png
  3. TEMPAT SAMPAH
      public/images/running/docx.png
  4. TEMPAT SAMPAH
      public/images/running/error.png
  5. TEMPAT SAMPAH
      public/images/running/flv.png
  6. TEMPAT SAMPAH
      public/images/running/img.png
  7. TEMPAT SAMPAH
      public/images/running/jpg.png
  8. TEMPAT SAMPAH
      public/images/running/mp3.png
  9. TEMPAT SAMPAH
      public/images/running/mp4.png
  10. TEMPAT SAMPAH
      public/images/running/mvb.png
  11. TEMPAT SAMPAH
      public/images/running/other.png
  12. TEMPAT SAMPAH
      public/images/running/pdf.png
  13. TEMPAT SAMPAH
      public/images/running/png.png
  14. TEMPAT SAMPAH
      public/images/running/ppt.png
  15. TEMPAT SAMPAH
      public/images/running/pptx.png
  16. TEMPAT SAMPAH
      public/images/running/rmvb.png
  17. TEMPAT SAMPAH
      public/images/running/swf.png
  18. TEMPAT SAMPAH
      public/images/running/tiff.png
  19. TEMPAT SAMPAH
      public/images/running/txt.png
  20. TEMPAT SAMPAH
      public/images/running/video.png
  21. TEMPAT SAMPAH
      public/images/running/wma.png
  22. TEMPAT SAMPAH
      public/images/running/xls.png
  23. TEMPAT SAMPAH
      public/images/running/xlsx.png
  24. 1 1
      src/pages/Northbound/AliCloud/Detail/index.tsx
  25. 23 4
      src/pages/Northbound/AliCloud/index.tsx
  26. 64 0
      src/pages/account/NotificationRecord/detail/index.tsx
  27. 164 0
      src/pages/account/NotificationRecord/index.tsx
  28. 36 0
      src/pages/account/NotificationRecord/service.ts
  29. 12 0
      src/pages/account/NotificationRecord/typings.d.ts
  30. 216 0
      src/pages/account/NotificationSubscription/index.tsx
  31. 180 0
      src/pages/account/NotificationSubscription/save/index.tsx
  32. 56 0
      src/pages/account/NotificationSubscription/service.ts
  33. 9 0
      src/pages/account/NotificationSubscription/typings.d.ts
  34. 11 9
      src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx
  35. 8 1
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  36. 85 82
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  37. 1 0
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  38. 4 6
      src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx
  39. 4 1
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  40. 91 26
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  41. 1 1
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  42. 1 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  43. 1 1
      src/pages/device/Instance/Detail/Running/index.tsx
  44. 68 0
      src/pages/link/Certificate/Detail/components/CertificateFile/index.tsx
  45. 38 0
      src/pages/link/Certificate/Detail/components/Standard/index.tsx
  46. 143 0
      src/pages/link/Certificate/Detail/index.tsx
  47. 79 151
      src/pages/link/Certificate/index.tsx
  48. 14 0
      src/pages/link/Certificate/service.ts
  49. 7 6
      src/pages/link/Certificate/typings.d.ts
  50. 5 1
      src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx
  51. 10 9
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  52. 3 0
      src/pages/rule-engine/Scene/Save/index.tsx
  53. 23 1
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  54. 16 11
      src/pages/system/Platforms/index.tsx
  55. 27 13
      src/pages/system/Platforms/password.tsx
  56. 31 7
      src/pages/system/Platforms/save.tsx
  57. 17 0
      src/pages/system/Platforms/service.ts
  58. 16 0
      src/utils/menu/index.ts
  59. 4 0
      src/utils/menu/router.ts

TEMPAT SAMPAH
public/images/certificate.png


TEMPAT SAMPAH
public/images/running/doc.png


TEMPAT SAMPAH
public/images/running/docx.png


TEMPAT SAMPAH
public/images/running/error.png


TEMPAT SAMPAH
public/images/running/flv.png


TEMPAT SAMPAH
public/images/running/img.png


TEMPAT SAMPAH
public/images/running/jpg.png


TEMPAT SAMPAH
public/images/running/mp3.png


TEMPAT SAMPAH
public/images/running/mp4.png


TEMPAT SAMPAH
public/images/running/mvb.png


TEMPAT SAMPAH
public/images/running/other.png


TEMPAT SAMPAH
public/images/running/pdf.png


TEMPAT SAMPAH
public/images/running/png.png


TEMPAT SAMPAH
public/images/running/ppt.png


TEMPAT SAMPAH
public/images/running/pptx.png


TEMPAT SAMPAH
public/images/running/rmvb.png


TEMPAT SAMPAH
public/images/running/swf.png


TEMPAT SAMPAH
public/images/running/tiff.png


TEMPAT SAMPAH
public/images/running/txt.png


TEMPAT SAMPAH
public/images/running/video.png


TEMPAT SAMPAH
public/images/running/wma.png


TEMPAT SAMPAH
public/images/running/xls.png


TEMPAT SAMPAH
public/images/running/xlsx.png


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

@@ -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',

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

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

+ 5 - 1
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -16,6 +16,7 @@ interface FunctionCallProps {
   functionData: any[];
   value?: any;
   onChange?: (data: any) => void;
+  name?: string;
 }
 
 export default (props: FunctionCallProps) => {
@@ -24,6 +25,9 @@ export default (props: FunctionCallProps) => {
 
   useEffect(() => {
     setEditableRowKeys(props.functionData.map((d) => d.id));
+    formRef.current?.setFieldsValue({
+      table: props.functionData,
+    });
   }, [props.functionData]);
 
   useEffect(() => {
@@ -126,7 +130,7 @@ export default (props: FunctionCallProps) => {
   return (
     <ProForm<{ table: FunctionTableDataType[] }>
       formRef={formRef}
-      name={'proForm'}
+      name={props.name || 'proForm'}
       submitter={false}
       onValuesChange={() => {
         const values = formRef.current?.getFieldsValue();

+ 10 - 9
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx

@@ -64,6 +64,8 @@ export default (props: TimingTrigger) => {
         when: [],
         period: {
           unit: 'seconds',
+          from: moment(new Date()).format('HH:mm:ss'),
+          to: moment(new Date()).format('HH:mm:ss'),
         },
       });
     } else {
@@ -81,8 +83,8 @@ export default (props: TimingTrigger) => {
           ...omit(data, 'once'),
           mod: key,
           period: {
-            from: undefined,
-            to: undefined,
+            from: moment(new Date()).format('HH:mm:ss'),
+            to: moment(new Date()).format('HH:mm:ss'),
             unit: 'seconds',
           },
         });
@@ -91,7 +93,7 @@ export default (props: TimingTrigger) => {
           ...omit(data, 'period'),
           mod: key,
           once: {
-            time: undefined,
+            time: moment(new Date()).format('HH:mm:ss'),
           },
         });
       }
@@ -190,11 +192,10 @@ export default (props: TimingTrigger) => {
               {data.mod === PeriodModEnum.period ? (
                 <TimePicker.RangePicker
                   format={'HH:mm:ss'}
-                  value={
-                    data.period?.from
-                      ? [moment(data.period?.from, 'HH:mm:ss'), moment(data.period?.to, 'hh:mm:ss')]
-                      : [moment(new Date(), 'HH:mm:ss'), moment(new Date(), 'HH:mm:ss')]
-                  }
+                  value={[
+                    moment(data.period?.from, 'HH:mm:ss'),
+                    moment(data.period?.to, 'hh:mm:ss'),
+                  ]}
                   onChange={(_, dateString) => {
                     onChange({
                       ...data,
@@ -209,7 +210,7 @@ export default (props: TimingTrigger) => {
               ) : (
                 <TimePicker
                   format={'HH:mm:ss'}
-                  value={moment(data.once?.time || new Date(), 'HH:mm:ss')}
+                  value={moment(data.once?.time, 'HH:mm:ss')}
                   onChange={(_, dateString) => {
                     onChange({
                       ...data,

+ 3 - 0
src/pages/rule-engine/Scene/Save/index.tsx

@@ -24,6 +24,7 @@ import { service } from '../index';
 import './index.less';
 import { model } from '@formily/reactive';
 import type { FormModelType } from '@/pages/rule-engine/Scene/typings';
+import moment from 'moment';
 
 type ShakeLimitType = {
   enabled: boolean;
@@ -296,6 +297,8 @@ export default () => {
                   when: [],
                   period: {
                     unit: 'seconds',
+                    from: moment(new Date()).format('HH:mm:ss'),
+                    to: moment(new Date()).format('HH:mm:ss'),
                   },
                 }}
               >

+ 23 - 1
src/pages/rule-engine/Scene/Save/trigger/index.tsx

@@ -12,6 +12,7 @@ import { observer } from '@formily/reactive-react';
 import OrgTreeSelect from './OrgTreeSelect';
 import { FormModel } from '../index';
 import AllDevice from '@/pages/rule-engine/Scene/Save/action/device/AllDevice';
+import moment from 'moment';
 
 interface TriggerProps {
   value?: any;
@@ -305,6 +306,8 @@ export default observer((props: TriggerProps) => {
             when: [],
             period: {
               unit: 'seconds',
+              from: moment(new Date()).format('HH:mm:ss'),
+              to: moment(new Date()).format('HH:mm:ss'),
             },
           }}
         >
@@ -331,6 +334,25 @@ export default observer((props: TriggerProps) => {
                   filterOption={(input: string, option: any) =>
                     option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
                   }
+                  onSelect={(_: any, fcItem: any) => {
+                    if (fcItem) {
+                      const _properties = fcItem.valueType
+                        ? fcItem.valueType.properties
+                        : fcItem.inputs;
+                      const array = [];
+                      for (const datum of _properties) {
+                        array.push({
+                          id: datum.id,
+                          name: datum.name,
+                          type: datum.valueType ? datum.valueType.type : '-',
+                          format: datum.valueType ? datum.valueType.format : undefined,
+                          options: datum.valueType ? datum.valueType.elements : undefined,
+                          value: undefined,
+                        });
+                      }
+                      setFunctionItem(array);
+                    }
+                  }}
                 />
               </Form.Item>
             </Col>
@@ -339,7 +361,7 @@ export default observer((props: TriggerProps) => {
             </Col>
             <Col span={24}>
               <Form.Item name={['trigger', 'device', 'operation', 'functionParameters']}>
-                <FunctionCall functionData={functionItem} />
+                <FunctionCall functionData={functionItem} name={'functionForm'} />
               </Form.Item>
             </Col>
           </Row>

+ 16 - 11
src/pages/system/Platforms/index.tsx

@@ -20,6 +20,7 @@ export default () => {
   const [param, setParam] = useState({});
   const [saveVisible, setSaveVisible] = useState(false);
   const [passwordVisible, setPasswordVisible] = useState(false);
+  const [editData, setEditData] = useState<any | undefined>(undefined);
 
   const { permission } = PermissionButton.usePermission('system/Platforms');
 
@@ -58,22 +59,21 @@ export default () => {
             status={record.value}
             text={record.text}
             statusNames={{
-              started: StatusColorEnum.processing,
+              enabled: StatusColorEnum.processing,
               disable: StatusColorEnum.error,
-              notActive: StatusColorEnum.warning,
             }}
           />
         ) : (
           ''
         ),
       valueEnum: {
-        disable: {
+        disabled: {
           text: '禁用',
-          status: 'offline',
+          status: 'disabled',
         },
-        started: {
+        enabled: {
           text: '正常',
-          status: 'started',
+          status: 'enabled',
         },
       },
     },
@@ -93,7 +93,7 @@ export default () => {
       valueType: 'option',
       align: 'center',
       width: 200,
-      render: (_, record) => [
+      render: (_, record: any) => [
         <PermissionButton
           key={'update'}
           type={'link'}
@@ -105,7 +105,10 @@ export default () => {
               defaultMessage: '编辑',
             }),
           }}
-          onClick={() => {}}
+          onClick={() => {
+            setSaveVisible(true);
+            setEditData(record);
+          }}
         >
           <EditOutlined />
         </PermissionButton>,
@@ -141,6 +144,7 @@ export default () => {
             title: '重置密码',
           }}
           onClick={() => {
+            setEditData({ id: record.userId });
             setPasswordVisible(true);
           }}
         >
@@ -187,6 +191,7 @@ export default () => {
         params={param}
         columns={columns}
         actionRef={actionRef}
+        request={(params: any) => service.query(params)}
         headerTitle={
           <PermissionButton
             key="button"
@@ -206,8 +211,10 @@ export default () => {
       />
       <SaveModal
         visible={saveVisible}
+        data={editData}
         onCancel={() => {
           setSaveVisible(false);
+          setEditData(undefined);
         }}
         onReload={() => {
           actionRef.current?.reload();
@@ -218,9 +225,7 @@ export default () => {
         onCancel={() => {
           setPasswordVisible(false);
         }}
-        onReload={() => {
-          actionRef.current?.reload();
-        }}
+        data={editData}
       />
     </PageContainer>
   );

+ 27 - 13
src/pages/system/Platforms/password.tsx

@@ -2,7 +2,7 @@ import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import { Form, FormGrid, FormItem, Password } from '@formily/antd';
 import { message, Modal } from 'antd';
-import { useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import type { ISchema } from '@formily/json-schema';
 import { service } from '@/pages/system/Platforms/index';
 
@@ -29,9 +29,8 @@ export default (props: SaveProps) => {
     () =>
       createForm({
         validateFirst: true,
-        initialValues: props.data || { oath2: true },
       }),
-    [props.data],
+    [],
   );
 
   const schema: ISchema = {
@@ -45,6 +44,7 @@ export default (props: SaveProps) => {
         'x-component': 'Password',
         'x-component-props': {
           placeholder: '请输入密码',
+          checkStrength: true,
         },
         'x-decorator-props': {
           gridSpan: 1,
@@ -62,8 +62,26 @@ export default (props: SaveProps) => {
         ],
         'x-validator': [
           {
-            max: 64,
-            message: '最多可输入64个字符',
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
           },
           {
             required: true,
@@ -79,6 +97,7 @@ export default (props: SaveProps) => {
         'x-component': 'Password',
         'x-component-props': {
           placeholder: '请再次输入密码',
+          checkStrength: true,
         },
         'x-decorator-props': {
           gridSpan: 1,
@@ -123,23 +142,18 @@ export default (props: SaveProps) => {
     }
   };
 
-  const saveData = async () => {
-    // setLoading(true)
+  const saveData = useCallback(async () => {
     const data: any = await form.submit();
-    console.log(data);
     if (data) {
       setLoading(true);
-      const resp = await service.update(data);
+      const resp = await service.passwordReset(props.data.id, data.password);
       setLoading(false);
       if (resp.status === 200) {
-        if (props.onReload) {
-          props.onReload();
-        }
         modalClose();
         message.success('操作成功');
       }
     }
-  };
+  }, [props.data]);
 
   return (
     <Modal

+ 31 - 7
src/pages/system/Platforms/save.tsx

@@ -14,7 +14,7 @@ import {
   TreeSelect,
 } from '@formily/antd';
 import { message, Modal } from 'antd';
-import React, { useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
 import { PlusOutlined } from '@ant-design/icons';
 import type { ISchema } from '@formily/json-schema';
@@ -77,7 +77,9 @@ export default (props: SaveProps) => {
     () =>
       createForm({
         validateFirst: true,
-        initialValues: props.data || { oath2: true, id: randomString() },
+        initialValues: props.data
+          ? { ...props.data, confirm_password: props.data.password }
+          : { enableOAuth2: true, id: randomString() },
       }),
     [props.data],
   );
@@ -184,7 +186,9 @@ export default (props: SaveProps) => {
             'x-component': 'Password',
             'x-component-props': {
               placeholder: '请输入密码',
+              checkStrength: true,
             },
+            'x-visible': !props.data,
             'x-decorator-props': {
               gridSpan: 1,
             },
@@ -201,8 +205,26 @@ export default (props: SaveProps) => {
             ],
             'x-validator': [
               {
-                max: 64,
-                message: '最多可输入64个字符',
+                triggerType: 'onBlur',
+                validator: (value: string) => {
+                  return new Promise((resolve) => {
+                    service
+                      .validateField('password', value)
+                      .then((resp) => {
+                        if (resp.status === 200) {
+                          if (resp.result.passed) {
+                            resolve('');
+                          } else {
+                            resolve(resp.result.reason);
+                          }
+                        }
+                        resolve('');
+                      })
+                      .catch(() => {
+                        return '验证失败!';
+                      });
+                  });
+                },
               },
               {
                 required: true,
@@ -218,7 +240,9 @@ export default (props: SaveProps) => {
             'x-component': 'Password',
             'x-component-props': {
               placeholder: '请再次输入密码',
+              checkStrength: true,
             },
+            'x-visible': !props.data,
             'x-decorator-props': {
               gridSpan: 1,
             },
@@ -379,13 +403,13 @@ export default (props: SaveProps) => {
     }
   };
 
-  const saveData = async () => {
+  const saveData = useCallback(async () => {
     // setLoading(true)
     const data: any = await form.submit();
     console.log(data);
     if (data) {
       setLoading(true);
-      const resp = data.id ? await service.update(data) : await service.save(data);
+      const resp: any = props.data ? await service.update(data) : await service.save(data);
       setLoading(false);
       if (resp.status === 200) {
         if (props.onReload) {
@@ -395,7 +419,7 @@ export default (props: SaveProps) => {
         message.success('操作成功');
       }
     }
-  };
+  }, [props.data]);
 
   return (
     <Modal

+ 17 - 0
src/pages/system/Platforms/service.ts

@@ -8,6 +8,23 @@ class Service extends BaseService<platformsType> {
       method: 'GET',
       params,
     });
+
+  /**
+   * 密码校验
+   * @param type
+   * @param name
+   */
+  validateField = (type: 'username' | 'password', name: string) =>
+    request(`/${SystemConst.API_BASE}/user/${type}/_validate`, {
+      method: 'POST',
+      data: name,
+    });
+
+  passwordReset = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/user/${id}/password/_reset`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

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

@@ -71,6 +71,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',
@@ -158,4 +161,5 @@ export const getDetailNameByCode = {
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
   'Northbound/AliCloud/Detail': '阿里云详情',
+  'link/Certificate/Detail': '证书详情',
 };