wzyyy 3 лет назад
Родитель
Сommit
b0c7883895
42 измененных файлов с 1574 добавлено и 214 удалено
  1. 16 4
      public/icons/iconfont.css
  2. 1 1
      public/icons/iconfont.js
  3. 21 0
      public/icons/iconfont.json
  4. 6 0
      public/icons/iconfont.svg
  5. BIN
      public/icons/iconfont.ttf
  6. BIN
      public/icons/iconfont.woff
  7. BIN
      public/icons/iconfont.woff2
  8. 27 22
      src/app.tsx
  9. 1 1
      src/components/FTermArrayCards/index.tsx
  10. 1 1
      src/components/FTermTypeSelect/index.tsx
  11. 51 9
      src/pages/Northbound/AliCloud/Detail/index.tsx
  12. 23 1
      src/pages/account/NotificationSubscription/save/index.tsx
  13. 1 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  14. 10 2
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  15. 23 10
      src/pages/device/Instance/index.tsx
  16. 1 0
      src/pages/device/Instance/service.ts
  17. 14 0
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  18. 22 0
      src/pages/link/Certificate/Detail/index.tsx
  19. 3 1
      src/pages/link/Certificate/index.tsx
  20. 2 2
      src/pages/notice/Config/Detail/index.tsx
  21. 20 4
      src/pages/notice/Config/SyncUser/index.tsx
  22. 5 5
      src/pages/notice/index.tsx
  23. 1 1
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  24. 3 3
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  25. 8 8
      src/pages/rule-engine/Scene/Save/index.tsx
  26. 4 1
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  27. 15 5
      src/pages/rule-engine/Scene/Save/trigger/operation.tsx
  28. 69 8
      src/pages/rule-engine/Scene/TriggerTerm/index.tsx
  29. 73 0
      src/pages/system/Platforms/Api/base.tsx
  30. 136 35
      src/pages/system/Platforms/Api/basePage.tsx
  31. 60 0
      src/pages/system/Platforms/Api/index.less
  32. 2 18
      src/pages/system/Platforms/Api/index.tsx
  33. 48 47
      src/pages/system/Platforms/Api/leftTree.tsx
  34. 360 0
      src/pages/system/Platforms/Api/swagger-ui/base.tsx
  35. 254 0
      src/pages/system/Platforms/Api/swagger-ui/debugging.tsx
  36. 33 0
      src/pages/system/Platforms/Api/swagger-ui/index.tsx
  37. 94 0
      src/pages/system/Platforms/View/index.tsx
  38. 87 15
      src/pages/system/Platforms/index.tsx
  39. 31 9
      src/pages/system/Platforms/save.tsx
  40. 41 0
      src/pages/system/Platforms/service.ts
  41. 4 1
      src/utils/menu/index.ts
  42. 3 0
      src/utils/menu/router.ts

+ 16 - 4
public/icons/iconfont.css

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "iconfont"; /* Project id 3183515 */
-  src: url('iconfont.woff2?t=1649842068082') format('woff2'),
-       url('iconfont.woff?t=1649842068082') format('woff'),
-       url('iconfont.ttf?t=1649842068082') format('truetype'),
-       url('iconfont.svg?t=1649842068082#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1652669655734') format('woff2'),
+  url('iconfont.woff?t=1652669655734') format('woff'),
+  url('iconfont.ttf?t=1652669655734') format('truetype'),
+  url('iconfont.svg?t=1652669655734#iconfont') format('svg');
 }
 
 .iconfont {
@@ -14,6 +14,18 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-chakanAPI:before {
+  content: "\e6af";
+}
+
+.icon-fuquan:before {
+  content: "\e689";
+}
+
+.icon-zhongzhimima:before {
+  content: "\e69b";
+}
+
 .icon-denggao:before {
   content: "\e68a";
 }

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
public/icons/iconfont.js


+ 21 - 0
public/icons/iconfont.json

@@ -6,6 +6,27 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "29728797",
+      "name": "查看API",
+      "font_class": "chakanAPI",
+      "unicode": "e6af",
+      "unicode_decimal": 59055
+    },
+    {
+      "icon_id": "29728564",
+      "name": "赋权",
+      "font_class": "fuquan",
+      "unicode": "e689",
+      "unicode_decimal": 59017
+    },
+    {
+      "icon_id": "29728565",
+      "name": "重置密码",
+      "font_class": "zhongzhimima",
+      "unicode": "e69b",
+      "unicode_decimal": 59035
+    },
+    {
       "icon_id": "28953788",
       "name": "等高",
       "font_class": "denggao",

Разница между файлами не показана из-за своего большого размера
+ 6 - 0
public/icons/iconfont.svg


BIN
public/icons/iconfont.ttf


BIN
public/icons/iconfont.woff


BIN
public/icons/iconfont.woff2


+ 27 - 22
src/app.tsx

@@ -140,29 +140,34 @@ export const request: RequestConfig = {
       return;
     }
     if (response.status === 400 || response.status === 500) {
-      response.text().then((resp: string) => {
-        if (resp) {
-          notification.error({
-            key: 'error',
-            message: JSON.parse(resp).message || '服务器内部错误!',
-          });
-        } else {
-          response
-            .json()
-            .then((res: any) => {
-              notification.error({
-                key: 'error',
-                message: `请求错误:${res.message}`,
-              });
-            })
-            .catch(() => {
-              notification.error({
-                key: 'error',
-                message: '系统错误',
-              });
+      // 添加clone() 避免后续其它地方用response.text()时报错
+      response
+        .clone()
+        .text()
+        .then((resp: string) => {
+          if (resp) {
+            notification.error({
+              key: 'error',
+              message: JSON.parse(resp).message || '服务器内部错误!',
             });
-        }
-      });
+          } else {
+            response
+              .clone()
+              .json()
+              .then((res: any) => {
+                notification.error({
+                  key: 'error',
+                  message: `请求错误:${res.message}`,
+                });
+              })
+              .catch(() => {
+                notification.error({
+                  key: 'error',
+                  message: '系统错误',
+                });
+              });
+          }
+        });
       return response;
     }
     if (!response) {

+ 1 - 1
src/components/FTermArrayCards/index.tsx

@@ -109,7 +109,7 @@ export const FTermArrayCards: ComposedArrayCards = observer((props) => {
                               width: 100,
                             },
                           },
-                          default: 'and',
+                          'x-value': 'and',
                           enum: [
                             { label: '并且', value: 'and' },
                             { label: '或者', value: 'or' },

+ 1 - 1
src/components/FTermTypeSelect/index.tsx

@@ -21,7 +21,7 @@ const FTermTypeSelect = (props: Props) => {
         onChange={(value) => props.onChange(value)}
         value={props.value}
         style={{ width: '200px' }}
-        defaultValue={'or'}
+        defaultValue={'and'}
         options={[
           { label: '并且', value: 'and' },
           { label: '或者', value: 'or' },

+ 51 - 9
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -134,6 +134,10 @@ const Detail = observer(() => {
             max: 64,
             message: '最多可输入64个字符',
           },
+          {
+            required: true,
+            message: '请输入名称',
+          },
         ],
       },
       accessConfig: {
@@ -155,6 +159,12 @@ const Detail = observer(() => {
               tooltip: '阿里云内部给每台机器设置的唯一编号',
             },
             'x-reactions': ['{{useAsyncDataSource(queryRegionsList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择服务地址',
+              },
+            ],
           },
           accessKeyId: {
             type: 'string',
@@ -170,6 +180,10 @@ const Detail = observer(() => {
                 max: 64,
                 message: '最多可输入64个字符',
               },
+              {
+                required: true,
+                message: '请输入accessKey',
+              },
             ],
             'x-decorator-props': {
               tooltip: '用于程序通知方式调用云服务API的用户标识',
@@ -186,6 +200,10 @@ const Detail = observer(() => {
             },
             'x-validator': [
               {
+                required: true,
+                message: '请输入accessSecret',
+              },
+              {
                 max: 64,
                 message: '最多可输入64个字符',
               },
@@ -211,26 +229,36 @@ const Detail = observer(() => {
         'x-decorator-props': {
           tooltip: '物联网平台对应的阿里云产品',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择网桥产品',
+          },
+        ],
       },
       mappings: {
         type: 'array',
         required: true,
         'x-component': 'ArrayCollapse',
-        title: '产品映射',
+        'x-decorator': 'FormItem',
         items: {
           type: 'object',
-          required: true,
           'x-component': 'ArrayCollapse.CollapsePanel',
           'x-component-props': {
             header: '产品映射',
           },
           properties: {
-            grid: {
+            index: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Index',
+            },
+            layout: {
               type: 'void',
-              'x-component': 'FormGrid',
-              'x-component-props': {
-                minColumns: [24],
-                maxColumns: [24],
+              'x-decorator': 'FormGrid',
+              'x-decorator-props': {
+                maxColumns: 2,
+                minColumns: 2,
+                columnGap: 24,
               },
               properties: {
                 type: 'object',
@@ -247,10 +275,17 @@ const Detail = observer(() => {
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
                   'x-decorator-props': {
-                    gridSpan: 12,
+                    layout: 'vertical',
+                    labelAlign: 'left',
                     tooltip: '阿里云物联网平台产品标识',
                   },
                   'x-reactions': ['{{useAsyncDataSource(queryAliyunProductList)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择阿里云产品',
+                    },
+                  ],
                 },
                 productId: {
                   type: 'string',
@@ -259,7 +294,8 @@ const Detail = observer(() => {
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
                   'x-decorator-props': {
-                    gridSpan: 12,
+                    layout: 'vertical',
+                    labelAlign: 'left',
                   },
                   'x-component-props': {
                     placeholder: '请选择平台产品',
@@ -268,6 +304,12 @@ const Detail = observer(() => {
                       option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
                   },
                   'x-reactions': ['{{useAsyncDataSource(queryProductList)}}'],
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择平台产品',
+                    },
+                  ],
                 },
               },
             },

+ 23 - 1
src/pages/account/NotificationSubscription/save/index.tsx

@@ -1,6 +1,6 @@
 import { message, Modal } from 'antd';
 import { useEffect, useMemo, useState } from 'react';
-import { Form, FormGrid, FormItem, Input, Select, Checkbox } from '@formily/antd';
+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';
@@ -65,6 +65,10 @@ const Save = (props: Props) => {
             },
             'x-validator': [
               {
+                required: true,
+                message: '请输入名称',
+              },
+              {
                 max: 64,
                 message: '最多可输入64个字符',
               },
@@ -84,6 +88,12 @@ const Save = (props: Props) => {
               placeholder: '请选择类型',
             },
             'x-reactions': ['{{useAsyncDataSource(queryProvidersList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择类型',
+              },
+            ],
           },
           'topicConfig.alarmConfigId': {
             title: '告警规则',
@@ -99,6 +109,12 @@ const Save = (props: Props) => {
               placeholder: '请选择告警规则',
             },
             'x-reactions': ['{{useAsyncDataSource(queryAlarmConfigList)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择告警规则',
+              },
+            ],
           },
           notice: {
             title: '通知方式',
@@ -127,6 +143,12 @@ const Save = (props: Props) => {
               labelAlign: 'left',
               layout: 'vertical',
             },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择通知方式',
+              },
+            ],
           },
         },
       },

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

@@ -20,6 +20,7 @@
     justify-content: center;
     width: 60px;
     height: 100%;
+
     img {
       width: 100%;
     }

+ 10 - 2
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -1,7 +1,7 @@
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import styles from './index.less';
 import Detail from './Detail';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { message, Tooltip } from 'antd';
 
 interface Props {
@@ -29,6 +29,11 @@ const FileComponent = (props: Props) => {
   const [type, setType] = useState<string>('other');
   const [visible, setVisible] = useState<boolean>(false);
   const isHttps = document.location.protocol === 'https:';
+  const [temp, setTemp] = useState<boolean>(false);
+
+  useEffect(() => {
+    setTemp(false);
+  }, [props.value]);
 
   const renderValue = () => {
     if (!value?.formatValue) {
@@ -46,7 +51,7 @@ const FileComponent = (props: Props) => {
           </div>
         );
       }
-      if (['.jpg', '.png'].some((item) => value?.formatValue.includes(item))) {
+      if (['.jpg', '.png', '.swf', '.tiff'].some((item) => value?.formatValue.includes(item))) {
         // 图片
         return (
           <div
@@ -54,6 +59,8 @@ const FileComponent = (props: Props) => {
             onClick={() => {
               if (isHttps && value?.formatValue.indexOf('http:') !== -1) {
                 message.error('域名为https时,不支持访问http地址');
+              } else if (temp) {
+                message.error('该图片无法访问');
               } else {
                 const flag =
                   ['.jpg', '.png'].find((item) => value?.formatValue.includes(item)) || '';
@@ -66,6 +73,7 @@ const FileComponent = (props: Props) => {
               src={value?.formatValue}
               onError={(e: any) => {
                 e.target.src = imgMap.get('error');
+                setTemp(true);
               }}
             />
           </div>

+ 23 - 10
src/pages/device/Instance/index.tsx

@@ -124,19 +124,32 @@ const Instance = () => {
           }.tips`,
           defaultMessage: '确认禁用?',
         }),
-        onConfirm: async () => {
+        onConfirm: () => {
           if (record.state.value !== 'notActive') {
-            await service.undeployDevice(record.id);
+            service.undeployDevice(record.id).then((resp: any) => {
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
           } else {
-            await service.deployDevice(record.id);
+            service.deployDevice(record.id).then((resp: any) => {
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
           }
-          message.success(
-            intl.formatMessage({
-              id: 'pages.data.option.success',
-              defaultMessage: '操作成功!',
-            }),
-          );
-          actionRef.current?.reload();
         },
       }}
       isPermission={permission.action}

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

@@ -279,6 +279,7 @@ class Service extends BaseService<DeviceInstance> {
       method: 'POST',
       data: {
         paging: false,
+        sorts: [{ name: 'name', order: 'asc' }],
       },
     });
   // 保存设备的物模型指标

+ 14 - 0
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -715,6 +715,20 @@ const Edit = observer((props: Props) => {
                         labelAlign: 'left',
                         layout: 'vertical',
                       },
+                      'x-validator': [
+                        {
+                          max: 64,
+                          message: '最多可输入64个字符',
+                        },
+                        {
+                          required: true,
+                          message: '请输入标识',
+                        },
+                        {
+                          validateId: true,
+                          message: 'ID只能由数字、26个英文字母或者下划线组成',
+                        },
+                      ],
                     },
                     name: {
                       // 名称

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

@@ -50,6 +50,12 @@ const Detail = observer(() => {
         default: 'common',
         'x-decorator': 'FormItem',
         'x-component': 'Standard',
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择证书标准',
+          },
+        ],
       },
       name: {
         type: 'string',
@@ -62,6 +68,10 @@ const Detail = observer(() => {
         },
         'x-validator': [
           {
+            required: true,
+            message: '请输入证书名称',
+          },
+          {
             max: 64,
             message: '最多可输入64个字符',
           },
@@ -77,6 +87,12 @@ const Detail = observer(() => {
           placeholder:
             '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请上传证书文件',
+          },
+        ],
       },
       'configs.key': {
         title: '证书私钥',
@@ -88,6 +104,12 @@ const Detail = observer(() => {
           placeholder:
             '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
         },
+        'x-validator': [
+          {
+            required: true,
+            message: '请输入证书私钥',
+          },
+        ],
       },
       description: {
         title: '说明',

+ 3 - 1
src/pages/link/Certificate/index.tsx

@@ -1,11 +1,11 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+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 SearchComponent from '@/components/SearchComponent';
-import ProTable from '@jetlinks/pro-table';
 import PermissionButton from '@/components/PermissionButton';
 import usePermissions from '@/hooks/permission';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
@@ -24,6 +24,7 @@ const Certificate = () => {
     {
       dataIndex: 'type',
       title: '证书标准',
+      render: (text: any) => <span>{text?.text || '-'}</span>,
     },
     {
       dataIndex: 'name',
@@ -101,6 +102,7 @@ const Certificate = () => {
         params={param}
         columns={columns}
         search={false}
+        rowKey="id"
         headerTitle={
           <PermissionButton
             onClick={() => {

+ 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: [
     {

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

@@ -91,7 +91,7 @@ const TabComponent = observer((props: Props) => {
             type: 'and',
           },
         ],
-        sorts: [{ name: 'alarmDate', order: 'desc' }],
+        sorts: [{ name: 'alarmTime', order: 'desc' }],
       })
       .then((resp) => {
         if (resp.status === 200) {

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

@@ -193,8 +193,8 @@ export default (props: TimingTrigger) => {
                 <TimePicker.RangePicker
                   format={'HH:mm:ss'}
                   value={[
-                    moment(data.period?.from, 'HH:mm:ss'),
-                    moment(data.period?.to, 'hh:mm:ss'),
+                    moment(data.period?.from || new Date(), 'HH:mm:ss'),
+                    moment(data.period?.to || new Date(), 'hh:mm:ss'),
                   ]}
                   onChange={(_, dateString) => {
                     onChange({
@@ -210,7 +210,7 @@ export default (props: TimingTrigger) => {
               ) : (
                 <TimePicker
                   format={'HH:mm:ss'}
-                  value={moment(data.once?.time, 'HH:mm:ss')}
+                  value={moment(data.once?.time || new Date(), 'HH:mm:ss')}
                   onChange={(_, dateString) => {
                     onChange({
                       ...data,

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

@@ -60,6 +60,7 @@ export default () => {
 
   const [requestParams, setRequestParams] = useState<any>(undefined);
   const [triggerValue, setTriggerValue] = useState<any>([]);
+  const [triggerDatas, setTriggerDatas] = useState<any>({});
   const [actionParams, setActionParams] = useState<any>(undefined);
 
   const [actionsData, setActionsData] = useState<any[]>([]);
@@ -76,6 +77,7 @@ export default () => {
         setParallel(_data.parallel);
 
         setTriggerValue({ trigger: _data.terms || [] });
+        setTriggerDatas(_data.terms);
         if (_data.trigger?.shakeLimit) {
           setShakeLimit(_data.trigger?.shakeLimit || DefaultShakeLimit);
         }
@@ -211,13 +213,15 @@ export default () => {
           onValuesChange={(changeValue, allValues) => {
             if (changeValue.trigger) {
               if (changeValue.trigger.device) {
-                if (
+                if (changeValue.trigger.device.productId) {
+                  setTriggerValue([]);
+                  setRequestParams({ trigger: allValues.trigger });
+                } else if (
                   changeValue.trigger.device.selectorValues ||
                   (changeValue.trigger.device.operation &&
                     changeValue.trigger.device.operation.operator)
                 ) {
-                  setTriggerValue([]);
-                  setRequestParams({ trigger: allValues.trigger });
+                  setTriggerDatas(allValues.trigger);
                 }
               } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
                 setActionParams({ trigger: allValues.trigger });
@@ -321,11 +325,7 @@ export default () => {
               // >
               //   <TriggerDevice className={'trigger-type-content'} />
               // </Form.Item>
-              <TriggerDevice
-                value={requestParams && requestParams.trigger}
-                className={'trigger-type-content'}
-                form={form}
-              />
+              <TriggerDevice value={triggerDatas} className={'trigger-type-content'} form={form} />
             )}
           </Form.Item>
           {triggerType === TriggerWayType.device &&

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

@@ -142,6 +142,8 @@ export default observer((props: TriggerProps) => {
     getProducts();
   }, []);
 
+  console.log('triggerModel', FormModel);
+
   useEffect(() => {
     const triggerData = props.value;
     console.log('triggerData', triggerData);
@@ -174,7 +176,7 @@ export default observer((props: TriggerProps) => {
               onChange={(key: any, node: any) => {
                 props.form?.resetFields([['trigger', 'device', 'selector']]);
                 props.form?.resetFields([['trigger', 'device', 'selectorValues']]);
-                props.form?.resetFields([['trigger', 'device', 'operation', 'operator']]);
+                props.form?.resetFields([['trigger', 'device', 'operation']]);
                 productIdChange(key, node.metadata);
                 props.form?.setFieldsValue({
                   trigger: {
@@ -207,6 +209,7 @@ export default observer((props: TriggerProps) => {
                     ]}
                     // fieldNames={{ label: 'name', value: 'id' }}
                     onSelect={(key: string) => {
+                      props.form?.resetFields([['trigger', 'device', 'selectorValues']]);
                       setSelector(key);
                     }}
                     style={{ width: 120 }}

+ 15 - 5
src/pages/rule-engine/Scene/Save/trigger/operation.tsx

@@ -25,12 +25,22 @@ export default (props: OperatorProps) => {
       if (props.propertiesList) {
         return _key.map((item) => {
           const proItem = props.propertiesList!.find((a: any) => a.id === item);
+          if (proItem) {
+            return {
+              id: proItem.id,
+              name: proItem.name,
+              type: proItem.valueType ? proItem.valueType.type : '-',
+              format: proItem.valueType ? proItem.valueType.format : undefined,
+              options: proItem.valueType ? proItem.valueType.elements : undefined,
+              value: value[item],
+            };
+          }
           return {
-            id: proItem.id,
-            name: proItem.name,
-            type: proItem.valueType ? proItem.valueType.type : '-',
-            format: proItem.valueType ? proItem.valueType.format : undefined,
-            options: proItem.valueType ? proItem.valueType.elements : undefined,
+            id: item,
+            name: item,
+            type: '',
+            format: undefined,
+            options: undefined,
             value: value[item],
           };
         });

+ 69 - 8
src/pages/rule-engine/Scene/TriggerTerm/index.tsx

@@ -154,7 +154,7 @@ const TriggerTerm = (props: Props, ref: any) => {
                 : treeValue[0];
 
             const source = (field as Field).value;
-            const value = field.query(source === 'manual' ? '.value' : '.metric');
+            const value = field.query(source === 'manual' ? '.value.0' : '.metric');
             if (target) {
               if (source === 'manual') {
                 // 手动输入
@@ -178,14 +178,27 @@ const TriggerTerm = (props: Props, ref: any) => {
                     };
                   }
                 });
+                form1.setFieldState(field.query('.value.1'), (state) => {
+                  state.componentType = valueTypeMap[valueType];
+                  if (valueType === 'date') {
+                    state.componentProps = {
+                      showTime: true,
+                    };
+                  }
+                });
               } else if (source === 'metrics') {
+                const termType = field.query('..termType').value();
+                const tag = ['nbtw', 'btw'].includes(termType);
                 // 指标
                 form1.setFieldState(value, (state) => {
                   state.componentType = Select;
-                  state.dataSource = target?.metrics.map((item: any) => ({
-                    label: item.name,
-                    value: item.id,
-                  }));
+                  state.dataSource = target?.metrics
+                    ?.filter((i: { range: boolean }) => i.range === tag)
+                    .map((item: any) => ({
+                      label: item.name,
+                      value: item.id,
+                    }));
+                  state.value = undefined;
                 });
               }
             }
@@ -224,6 +237,9 @@ const TriggerTerm = (props: Props, ref: any) => {
         type: 'array',
         'x-component': 'FTermArrayCards',
         'x-decorator': 'FormItem',
+        'x-value': {
+          termType: 'and',
+        },
         'x-component-props': {
           title: '分组',
         },
@@ -313,7 +329,7 @@ const TriggerTerm = (props: Props, ref: any) => {
                               },
                             },
                           },
-                          value: {
+                          'value[0]': {
                             type: 'string',
                             'x-component': 'Input',
                             'x-decorator': 'FormItem',
@@ -323,15 +339,60 @@ const TriggerTerm = (props: Props, ref: any) => {
                               },
                             },
                             required: true,
+                            'x-reactions': [
+                              {
+                                dependencies: ['..source'],
+                                fulfill: {
+                                  state: {
+                                    visible: '{{$deps[0]==="manual"}}',
+                                  },
+                                },
+                              },
+                              {
+                                dependencies: ['...termType'],
+                                when: '{{["nbtw","btw"].includes($deps[0])}}',
+                                fulfill: {
+                                  state: {
+                                    decoratorProps: {
+                                      style: {
+                                        width: 'calc(50% - 55px)',
+                                      },
+                                    },
+                                  },
+                                },
+                                otherwise: {
+                                  state: {
+                                    decoratorProps: {
+                                      style: {
+                                        width: 'calc(100% - 110px)',
+                                      },
+                                    },
+                                  },
+                                },
+                              },
+                            ],
+                          },
+                          'value[1]': {
+                            type: 'string',
+                            'x-component': 'Input',
+                            'x-decorator': 'FormItem',
+                            'x-decorator-props': {
+                              style: {
+                                width: 'calc(50% - 55px)',
+                              },
+                            },
+                            required: true,
                             'x-reactions': {
-                              dependencies: ['.source'],
+                              dependencies: ['..source', '...termType'],
                               fulfill: {
                                 state: {
-                                  visible: '{{$deps[0]==="manual"}}',
+                                  visible:
+                                    '{{$deps[0]==="manual"&&["nbtw","btw"].includes($deps[1])}}',
                                 },
                               },
                             },
                           },
+
                           metric: {
                             type: 'string',
                             'x-component': 'Select',

+ 73 - 0
src/pages/system/Platforms/Api/base.tsx

@@ -0,0 +1,73 @@
+import Tree from '@/pages/system/Platforms/Api/leftTree';
+import Table from '@/pages/system/Platforms/Api/basePage';
+import SwaggerUI from '@/pages/system/Platforms/Api/swagger-ui';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/system/Platforms';
+import { model } from '@formily/reactive';
+import { observer } from '@formily/react';
+import './index.less';
+
+export const ApiModel = model<{
+  data: any[];
+  baseUrl: string;
+  showTable: boolean;
+  components: any;
+  swagger: any;
+  debugger: any;
+}>({
+  data: [],
+  baseUrl: '',
+  showTable: true,
+  components: {},
+  swagger: {},
+  debugger: {},
+});
+
+interface ApiPageProps {
+  showDebugger?: boolean;
+  isShowGranted?: boolean;
+}
+
+export default observer((props: ApiPageProps) => {
+  const [operations, setOperations] = useState<string[]>([]);
+
+  const initModel = () => {
+    ApiModel.data = [];
+    ApiModel.baseUrl = '';
+    ApiModel.showTable = true;
+    ApiModel.components = {};
+    ApiModel.swagger = {};
+    ApiModel.debugger = {};
+  };
+
+  const getOperations = () => {
+    service.apiOperations().then((resp: any) => {
+      if (resp.status === 200) {
+        setOperations(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    initModel();
+    getOperations();
+  }, []);
+
+  return (
+    <div className={'platforms-api'}>
+      <div className={'platforms-api-tree'}>
+        <Tree
+          onSelect={(data) => {
+            ApiModel.data = data;
+            ApiModel.showTable = true;
+          }}
+        />
+      </div>
+      {ApiModel.showTable ? (
+        <Table data={ApiModel.data} operations={operations} isShowGranted={props.isShowGranted} />
+      ) : (
+        <SwaggerUI showDebugger={props.showDebugger} />
+      )}
+    </div>
+  );
+});

+ 136 - 35
src/pages/system/Platforms/Api/basePage.tsx

@@ -1,43 +1,130 @@
-import { Button, Table } from 'antd';
-import { useCallback, useEffect, useState } from 'react';
+import { Button, message, Table } from 'antd';
+import { useCallback, useEffect, useState, useRef } from 'react';
+import { useLocation } from 'umi';
 import { service } from '../index';
+import { ApiModel } from '@/pages/system/Platforms/Api/base';
 
 interface TableProps {
-  parentId: string;
-  onJump: (id: string) => void;
+  data: any;
+  operations: string[];
+  // 是否只暂时已授权的接口
+  isShowGranted?: boolean;
 }
 
 export default (props: TableProps) => {
   const [selectKeys, setSelectKeys] = useState<string[]>([]);
-  const [dataSource, setDataSource] = useState([]);
+  const [dataSource, setDataSource] = useState<any[]>([]);
+  const [loading, setLoading] = useState(false);
 
-  const queryData = async (pId: string) => {
-    const resp: any = service.queryRoleList(pId);
-    if (resp.status === 200) {
-      setDataSource(resp.result);
-    }
+  const grantCache = useRef<string[]>([]);
+
+  const location = useLocation();
+
+  const getApiGrant = useCallback(() => {
+    const param = new URLSearchParams(location.search);
+    const code = param.get('code');
+
+    service.getApiGranted(code!).then((resp: any) => {
+      if (resp.status === 200) {
+        grantCache.current = resp.result;
+        setSelectKeys(resp.result);
+      }
+    });
+  }, [location]);
+
+  const getOperations = async (apiData: any[], operations: string[]) => {
+    // 过滤只能授权的接口,当isShowGranted为true时,过滤为已赋权的接口
+    setDataSource(
+      apiData.filter((item) => item && item.operationId && operations.includes(item.operationId)),
+    );
   };
 
   useEffect(() => {
-    queryData(props.parentId);
-  }, [props.parentId]);
+    if (props.isShowGranted) {
+      if (props.data && selectKeys) {
+        getOperations(props.data, selectKeys);
+      } else {
+        setDataSource([]);
+      }
+    }
+  }, [props.isShowGranted, selectKeys, props.data]);
 
-  const save = useCallback(async () => {}, [selectKeys]);
+  useEffect(() => {
+    if (!props.isShowGranted) {
+      if (props.data && props.data.length && props.operations) {
+        getOperations(props.data, props.operations);
+      } else {
+        setDataSource([]);
+      }
+    }
+  }, [props.data, props.operations, props.isShowGranted]);
+
+  useEffect(() => {
+    getApiGrant();
+  }, []);
+
+  const save = useCallback(async () => {
+    const param = new URLSearchParams(location.search);
+    const code = param.get('code');
+    // 和原有已授权数据进行对比
+    const addGrant = selectKeys.filter((key) => {
+      if (grantCache.current.includes(key)) {
+        return false;
+      }
+      return true;
+    });
+
+    // 获取删除的数据
+    const removeGrant = grantCache.current.filter((key) => {
+      if (selectKeys.includes(key)) {
+        return false;
+      }
+      return true;
+    });
+
+    const addOperations = addGrant.map((a: string) => {
+      const item = dataSource.find((b) => b.operationId === a);
+      return {
+        id: a,
+        permissions: item.security,
+      };
+    });
+
+    const removeOperations = removeGrant.map((a: string) => {
+      const item = dataSource.find((b) => b.operationId === a);
+      return {
+        id: a,
+        permissions: item.security,
+      };
+    });
+
+    grantCache.current = addGrant;
+
+    setLoading(true);
+    const resp = await service.addApiGrant(code!, { operations: addOperations });
+    const resp2 = await service.removeApiGrant(code!, { operations: removeOperations });
+    setLoading(false);
+    if (resp.status === 200 || resp2.status === 200) {
+      message.success('操作成功');
+    }
+  }, [selectKeys, location, dataSource]);
 
   return (
     <div className={'platforms-api-table'}>
       <Table<any>
+        rowKey={'operationId'}
         columns={[
           {
             title: 'API',
-            dataIndex: 'name',
+            dataIndex: 'url',
             render: (text: string, record) => {
               return (
                 <Button
                   type={'link'}
                   style={{ padding: 0 }}
                   onClick={() => {
-                    props.onJump(record.id);
+                    ApiModel.swagger = record;
+                    ApiModel.showTable = false;
                   }}
                 >
                   {text}
@@ -47,30 +134,44 @@ export default (props: TableProps) => {
           },
           {
             title: '说明',
-            dataIndex: '',
+            dataIndex: 'summary',
           },
         ]}
+        pagination={false}
         dataSource={dataSource}
-        rowSelection={{
-          selectedRowKeys: selectKeys,
-          onSelect: (record, selected) => {
-            if (selected) {
-              const newArr = [...selectKeys, record];
-              setSelectKeys(newArr);
-            } else {
-              setSelectKeys([...selectKeys.filter((key) => key !== record)]);
-            }
-          },
-          onSelectAll: (_, selectedRows) => {
-            setSelectKeys(selectedRows);
-          },
-        }}
+        rowSelection={
+          props.isShowGranted !== true
+            ? {
+                selectedRowKeys: selectKeys,
+                onSelect: (record, selected) => {
+                  if (selected) {
+                    const newArr = [...selectKeys, record.operationId];
+                    setSelectKeys(newArr);
+                  } else {
+                    setSelectKeys([...selectKeys.filter((key) => key !== record.operationId)]);
+                  }
+                },
+                onSelectAll: (selected, selectedRows) => {
+                  if (selected) {
+                    setSelectKeys(
+                      selectedRows.filter((item) => !!item).map((item) => item.operationId),
+                    );
+                  } else {
+                    setSelectKeys([]);
+                  }
+                },
+              }
+            : undefined
+        }
+        scroll={{ y: 600 }}
       />
-      <div className={'platforms-api-save'}>
-        <Button type={'primary'} onClick={save}>
-          保存
-        </Button>
-      </div>
+      {props.isShowGranted !== true && (
+        <div className={'platforms-api-save'}>
+          <Button type={'primary'} onClick={save} loading={loading}>
+            保存
+          </Button>
+        </div>
+      )}
     </div>
   );
 };

+ 60 - 0
src/pages/system/Platforms/Api/index.less

@@ -1,3 +1,5 @@
+@import '~antd/es/style/themes/default.less';
+
 .platforms-api {
   display: flex;
   padding: 24px;
@@ -5,6 +7,8 @@
 
   .platforms-api-tree {
     width: 320px;
+    padding-right: 12px;
+    border-right: 1px solid #e9e9e9;
   }
 
   .platforms-api-table {
@@ -12,9 +16,65 @@
     flex-direction: column;
     flex-grow: 1;
     width: 0;
+    margin-left: 24px;
 
     .platforms-api-save {
       margin-top: 12px;
     }
   }
+
+  .platforms-api-swagger {
+    flex: 1;
+    margin-left: 24px;
+
+    .platforms-api-swagger-back {
+      margin-bottom: 24px;
+    }
+
+    .platforms-api-swagger-content {
+      .swagger-content-title {
+        font-weight: bold;
+        font-size: 16px;
+      }
+
+      .swagger-content-item,
+      .swagger-content-url {
+        margin-top: 24px;
+      }
+
+      .swagger-content-url .url-method {
+        color: #fff;
+        border: none;
+
+        &.put {
+          background-color: @orange-6;
+        }
+
+        &.delete {
+          background-color: @red-6;
+        }
+
+        &.post {
+          background-color: @green-6;
+        }
+
+        &.get {
+          background-color: @blue-6;
+        }
+
+        &.patch {
+          background-color: @lime-6;
+        }
+      }
+
+      .swagger-content-request-type {
+        display: flex;
+        justify-content: space-between;
+
+        > span:nth-child(odd) {
+          font-weight: bold;
+        }
+      }
+    }
+  }
 }

+ 2 - 18
src/pages/system/Platforms/Api/index.tsx

@@ -1,26 +1,10 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import Table from './basePage';
-import Tree from './leftTree';
-import './index.less';
-import { useState } from 'react';
+import BasePage from './base';
 
 export default () => {
-  const [jumpId, setJumpId] = useState('');
-  const [parentId, setParentId] = useState('');
-
   return (
     <PageContainer>
-      <div className={'platforms-api'}>
-        <div className={'platforms-api-tree'}>
-          <Tree
-            onSelect={(id) => {
-              setJumpId('');
-              setParentId(id);
-            }}
-          />
-        </div>
-        {!jumpId ? <Table parentId={parentId} onJump={setJumpId} /> : <></>}
-      </div>
+      <BasePage />
     </PageContainer>
   );
 };

+ 48 - 47
src/pages/system/Platforms/Api/leftTree.tsx

@@ -1,9 +1,10 @@
 import { Tree } from 'antd';
-import React, { useState } from 'react';
-import { queryChannel } from '@/pages/media/SplitScreen/service';
+import React, { useEffect, useState } from 'react';
+import { service } from '@/pages/system/Platforms';
+import { ApiModel } from '@/pages/system/Platforms/Api/base';
 
 type LeftTreeType = {
-  onSelect: (id: string) => void;
+  onSelect: (data: any) => void;
 };
 
 interface DataNode {
@@ -17,15 +18,11 @@ interface DataNode {
 export default (props: LeftTreeType) => {
   const [treeData, setTreeData] = useState<DataNode[]>([]);
 
-  /**
-   * 是否为子节点
-   * @param node
-   */
-  const isLeaf = (node: DataNode): boolean => {
-    if (node.children) {
-      return false;
+  const getLevelOne = async () => {
+    const resp = await service.getApiFirstLevel();
+    if (resp.urls && resp.urls.length) {
+      setTreeData(resp.urls.map((item: any) => ({ ...item, id: item.url })));
     }
-    return true;
   };
 
   const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
@@ -47,29 +44,37 @@ export default (props: LeftTreeType) => {
     });
   };
 
-  const getChildren = (key: React.Key, params: any): Promise<any> => {
+  const handleTreeData = (data: any) => {
+    const newArr = data.tags.map((item: any) => ({ id: item.name, name: item.name, isLeaf: true }));
+
+    Object.keys(data.paths).forEach((a: any) => {
+      Object.keys(data.paths[a]).forEach((b) => {
+        const { tags, ...extraData } = data.paths[a][b];
+        const tag = tags[0];
+        const obj = {
+          url: a,
+          method: b,
+          ...extraData,
+        };
+        const item = newArr.find((c: any) => c.id === tag);
+        if (item) {
+          item.extraData = item.extraData ? [...item.extraData, obj] : [obj];
+        }
+      });
+    });
+    console.log(newArr);
+    return newArr;
+  };
+
+  const getChildren = (key: string, name: string): Promise<any> => {
     return new Promise(async (resolve) => {
-      const resp = await queryChannel(params);
-      if (resp.status === 200) {
-        const { total, pageIndex, pageSize } = resp.result;
+      const resp = await service.getApiNextLevel(name);
+      if (resp) {
+        ApiModel.components = resp.components;
+        ApiModel.baseUrl = resp.servers[0].url;
+        const handleData = handleTreeData(resp);
         setTreeData((origin) => {
-          const data = updateTreeData(
-            origin,
-            key,
-            resp.result.data.map((item: DataNode) => ({
-              ...item,
-              isLeaf: isLeaf(item),
-            })),
-          );
-
-          if (total > (pageIndex + 1) * pageSize) {
-            setTimeout(() => {
-              getChildren(key, {
-                ...params,
-                pageIndex: params.pageIndex + 1,
-              });
-            }, 50);
-          }
+          const data = updateTreeData(origin, key, handleData);
 
           return data;
         });
@@ -78,38 +83,34 @@ export default (props: LeftTreeType) => {
     });
   };
 
-  const onLoadData = ({ key, children }: any): Promise<void> => {
+  const onLoadData = (node: any): Promise<void> => {
+    console.log(node);
     return new Promise(async (resolve) => {
-      if (children) {
+      if (node.children) {
         resolve();
         return;
       }
-      await getChildren(key, {
-        pageIndex: 0,
-        pageSize: 100,
-        terms: [
-          {
-            column: 'deviceId',
-            value: key,
-          },
-        ],
-      });
+      await getChildren(node.key, node.name);
       resolve();
     });
   };
 
+  useEffect(() => {
+    getLevelOne();
+  }, []);
+
   return (
     <Tree
       showIcon
       showLine={{ showLeafIcon: false }}
-      height={550}
+      height={700}
       fieldNames={{
         title: 'name',
         key: 'id',
       }}
       onSelect={(_, { node }: any) => {
-        if (props.onSelect && node.isLeaf) {
-          props.onSelect(node.id);
+        if (node.isLeaf && props.onSelect) {
+          props.onSelect(node.extraData);
         }
       }}
       loadData={onLoadData}

+ 360 - 0
src/pages/system/Platforms/Api/swagger-ui/base.tsx

@@ -0,0 +1,360 @@
+import { observer } from '@formily/react';
+import { ApiModel } from '@/pages/system/Platforms/Api/base';
+import { TitleComponent } from '@/components';
+import ReactJson from 'react-json-view';
+import { Button, Input, Table, Tabs } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { cloneDeep, isArray, isObject } from 'lodash';
+import classNames from 'classnames';
+
+export default observer(() => {
+  const [dataSource, setDataSource] = useState<any[]>([]);
+  const [responseData, setResponseData] = useState<any[]>([]);
+
+  const getContent = (data: any) => {
+    return Object.keys(data)[0];
+  };
+
+  const ObjectFindValue = (name: string, obj: any): any => {
+    let value: any = '';
+    if (obj[name]) {
+      value = obj[name];
+    } else {
+      Object.keys(obj).some((key) => {
+        const _value = isObject(obj[key]) ? ObjectFindValue(name, obj[key]) : undefined;
+        if (_value) {
+          value = _value;
+          return true;
+        }
+        return false;
+      });
+    }
+    return value;
+  };
+
+  const titleCase = (value: string) => {
+    return value.slice(0, 1).toLowerCase() + value.slice(1);
+  };
+
+  const handleEntityTable = useCallback(
+    (entityName: string, entityData: any, entityType: string, required: boolean) => {
+      let propertiesData: any[] = [];
+
+      if (entityData) {
+        propertiesData = Object.keys(entityData).map((key) => {
+          return {
+            name: key,
+            description: entityData[key].description,
+            method: '',
+            required: !!entityData[key].required,
+            type: entityData[key].type,
+          };
+        });
+      }
+      // 数组类型,实体名末尾加s
+      const _isArray = entityType === 'array' ? 's' : '';
+
+      setDataSource([
+        ...dataSource,
+        {
+          name: titleCase(entityName) + _isArray,
+          description: entityName,
+          method: 'body',
+          required: required,
+          type: entityType || entityName,
+          children: propertiesData,
+        },
+      ]);
+    },
+    [dataSource],
+  );
+
+  const getEntity = () => {
+    const contentType: any = Object.values(ApiModel.swagger.requestBody.content);
+    if (contentType) {
+      const refUrl = ObjectFindValue('$ref', ApiModel.swagger.requestBody.content);
+      if (refUrl) {
+        const entityName = refUrl.split('/').pop();
+        const entityType = ObjectFindValue('type', ApiModel.swagger.requestBody.content);
+        const entityRequired = ApiModel.swagger.requestBody.required;
+        const entity: any = ApiModel.components.schemas[entityName];
+        const file = ObjectFindValue('file', ApiModel.swagger.requestBody.content);
+        // 是否为文件上传
+        if (file && isObject(file)) {
+          const fileObj = [
+            {
+              name: 'file',
+              description: '',
+              method: 'query',
+              required: true,
+              type: 'file',
+            },
+          ];
+          setDataSource(fileObj);
+          ApiModel.debugger.params = fileObj;
+        } else if (entity) {
+          handleEntityTable(entityName, entity.properties || entity, entityType, !!entityRequired);
+        }
+        return entityType === 'array' ? [entity.properties || entity] : entity.properties || entity;
+      }
+      return '';
+    }
+    return '';
+  };
+
+  const handleEntity = (entityData: any): any => {
+    let newEntity = {};
+    console.log(entityData);
+    if (isArray(entityData)) {
+      newEntity = [handleEntity(entityData[0])];
+    } else if (isObject(entityData)) {
+      Object.keys(entityData).forEach((key) => {
+        const type = entityData[key].type;
+        console.log(entityData[key]);
+        if (type) {
+          if (type.includes('integer')) {
+            newEntity[key] = 0;
+          } else if (type === 'boolean') {
+            newEntity[key] = true;
+          } else if (type === 'object') {
+            newEntity[key] = {};
+          } else if (type === 'array') {
+            newEntity[key] = [];
+          } else {
+            newEntity[key] = '';
+          }
+        } else {
+          newEntity[key] = '';
+        }
+      });
+    }
+    return newEntity;
+  };
+
+  const getResult = (name: string, oldName: string = '') => {
+    const entity = cloneDeep(ApiModel.components.schemas[name].properties);
+    console.log(entity);
+    if (name === oldName) {
+      // 禁止套娃
+      return [];
+    }
+    Object.keys(entity).forEach((key) => {
+      const type = entity[key].type;
+      if ((entity[key].items && entity[key].items.$ref) || entity[key].$ref) {
+        const _ref = entity[key].$ref || entity[key].items.$ref;
+        const refName = _ref.split('/').pop();
+        if (type === 'array') {
+          entity[key] = [getResult(refName, name)];
+        } else {
+          entity[key] = getResult(refName, name);
+        }
+      } else if (type) {
+        if (type.includes('integer')) {
+          entity[key] = 0;
+        } else if (type === 'boolean') {
+          entity[key] = true;
+        } else {
+          entity[key] = '';
+        }
+      }
+    });
+    return entity;
+  };
+
+  const handleResponseParam = (name: any, oldName: string = ''): any[] => {
+    if (!ApiModel.components.schemas[name]) {
+      return [];
+    }
+
+    const entity = cloneDeep(ApiModel.components.schemas[name].properties);
+    console.log(entity);
+
+    const newArr: any[] = [];
+    if (name === oldName) {
+      return newArr;
+    }
+
+    Object.keys(entity).forEach((key) => {
+      const type = entity[key].type;
+      const obj: any = {
+        code: key,
+        description: entity[key].description,
+        type: type,
+      };
+
+      if ((entity[key].items && entity[key].items.$ref) || entity[key].$ref) {
+        const _ref = entity[key].$ref || entity[key].items.$ref;
+        const refName = _ref.split('/').pop();
+        if (refName) {
+          obj.type = refName;
+          obj.children = handleResponseParam(refName, name);
+        }
+      }
+      newArr.push(obj);
+    });
+    return newArr;
+  };
+
+  const handleResponse = () => {
+    const newArr: any[] = [];
+    Object.keys(ApiModel.swagger.responses).forEach((key) => {
+      const refUrl = ObjectFindValue('$ref', ApiModel.swagger.responses[key]);
+      const entityName = refUrl.split('/').pop();
+
+      newArr.push({
+        code: key,
+        description: ApiModel.swagger.responses[key].description,
+        schema: key !== '400' ? entityName : '',
+        entityName: entityName,
+        result: key !== '400' ? getResult(entityName) : {},
+      });
+    });
+    setResponseData(newArr);
+  };
+
+  useEffect(() => {
+    if (ApiModel.swagger.parameters) {
+      const params = ApiModel.swagger.parameters.map((item: any) => {
+        return {
+          name: item.name,
+          required: item.required,
+          type: item.schema.type,
+          description: item.description,
+          method: item.in,
+        };
+      });
+      ApiModel.debugger.params = params;
+      setDataSource(params);
+    }
+    if (ApiModel.swagger.requestBody) {
+      ApiModel.debugger.body = handleEntity(getEntity());
+    }
+
+    if (ApiModel.swagger.responses) {
+      handleResponse();
+    }
+  }, []);
+
+  return (
+    <div className={'platforms-api-swagger-content'}>
+      <div className={'swagger-content-title'}>{ApiModel.swagger.summary}</div>
+      <div className={'swagger-content-url'}>
+        <Input.Group compact>
+          <Button className={classNames('url-method', ApiModel.swagger.method)}>
+            {ApiModel.swagger.method ? ApiModel.swagger.method.toUpperCase() : ''}
+          </Button>
+          <Input
+            style={{
+              width: `calc(100% - ${ApiModel.swagger.method !== 'delete' ? '70px' : '80px'})`,
+            }}
+            value={ApiModel.swagger.url}
+            readOnly
+          />
+        </Input.Group>
+      </div>
+      <div className={'swagger-content-item swagger-content-request-type'}>
+        <span>请求数据类型</span>
+        <span>
+          {ApiModel.swagger.requestBody
+            ? getContent(ApiModel.swagger.requestBody.content)
+            : 'application/x-www-form-urlencoded'}
+        </span>
+        <span>响应数据类型</span>
+        <span>{`["/"]`}</span>
+      </div>
+      {ApiModel.swagger.description && (
+        <div className={'swagger-content-item'}>
+          <TitleComponent data={'接口描述'} />
+          <div> {ApiModel.swagger.description} </div>
+        </div>
+      )}
+      {ApiModel.swagger.requestBody &&
+        ApiModel.debugger.body &&
+        !!Object.keys(ApiModel.debugger.body).length && (
+          <div className={'swagger-content-item'}>
+            <TitleComponent data={'请求示例'} />
+            <div>
+              {
+                // @ts-ignore
+                <ReactJson
+                  displayObjectSize={false}
+                  displayDataTypes={false}
+                  name={false}
+                  src={ApiModel.debugger.body}
+                />
+              }
+            </div>
+          </div>
+        )}
+      <div className={'swagger-content-item'}>
+        <TitleComponent data={'请求参数'} />
+        <Table
+          pagination={false}
+          size={'small'}
+          columns={[
+            { title: '参数名', dataIndex: 'name' },
+            { title: '参数说明', dataIndex: 'description' },
+            { title: '请求类型', dataIndex: 'method' },
+            {
+              title: '是否必须',
+              dataIndex: 'required',
+              render: (text) => <span>{`${!!text}`}</span>,
+            },
+            { title: '参数类型', dataIndex: 'type' },
+          ]}
+          dataSource={dataSource}
+        />
+      </div>
+      <div className={'swagger-content-item'}>
+        <TitleComponent data={'响应状态'} />
+        <Table
+          pagination={false}
+          size={'small'}
+          columns={[
+            { title: '状态码', dataIndex: 'code' },
+            { title: '说明', dataIndex: 'description' },
+            { title: 'schema', dataIndex: 'schema' },
+          ]}
+          dataSource={responseData}
+        />
+      </div>
+      <div className={'swagger-content-item'}>
+        <Tabs>
+          {responseData
+            .filter((item) => item.code !== '400')
+            .map((item) => {
+              console.log(item);
+              return (
+                <Tabs.TabPane key={item.code} tab={item.code}>
+                  <div>
+                    <div>
+                      <TitleComponent data={'响应参数'} style={{ margin: 0 }} />
+                      <Table
+                        pagination={false}
+                        size={'small'}
+                        columns={[
+                          { title: '参数名称', dataIndex: 'code' },
+                          { title: '参数说明', dataIndex: 'description' },
+                          { title: '类型', dataIndex: 'type' },
+                        ]}
+                        dataSource={handleResponseParam(item.entityName)}
+                      />
+                    </div>
+                    {
+                      // @ts-ignore
+                      <ReactJson
+                        displayObjectSize={false}
+                        displayDataTypes={false}
+                        name={false}
+                        src={item.result}
+                      />
+                    }
+                  </div>
+                </Tabs.TabPane>
+              );
+            })}
+        </Tabs>
+      </div>
+    </div>
+  );
+});

+ 254 - 0
src/pages/system/Platforms/Api/swagger-ui/debugging.tsx

@@ -0,0 +1,254 @@
+import { TitleComponent } from '@/components';
+import ReactJson from 'react-json-view';
+import { request } from 'umi';
+import MonacoEditor from 'react-monaco-editor';
+import { Button, Input } from 'antd';
+import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
+import { observer } from '@formily/react';
+import { ApiModel } from '@/pages/system/Platforms/Api/base';
+import { createForm } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { FormItem, Input as FormilyInput, ArrayTable, Editable } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import classNames from 'classnames';
+
+export default observer(() => {
+  const [result, setResult] = useState({});
+  const [body, setBody] = useState({});
+
+  const editor: any = useRef(null);
+
+  useEffect(() => {
+    if (ApiModel.debugger.body && editor.current) {
+      const { editor: MEditor } = editor.current;
+      MEditor.setValue(JSON.stringify(ApiModel.debugger.body));
+      setTimeout(() => {
+        MEditor.getAction('editor.action.formatDocument').run();
+      }, 300);
+      // MEditor.trigger('anyString', 'editor.action.formatDocument');//自动格式化代码
+      MEditor.setValue(MEditor.getValue());
+    }
+  }, [ApiModel.debugger, editor.current]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      Input: FormilyInput,
+      ArrayTable,
+    },
+  });
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+      }),
+    [],
+  );
+
+  const onSearch = useCallback(async () => {
+    const formData: any = await form.submit();
+    console.log(formData);
+    let newUrl = ApiModel.swagger.url;
+    if (formData && formData.params && formData.params.length) {
+      const params = formData.params;
+      params.forEach((item: any) => {
+        if (newUrl.includes(`{${item.name}}`)) {
+          newUrl = newUrl.replace(`{${item.name}}`, item.values);
+        }
+      });
+      console.log(newUrl);
+    }
+
+    // 判断请求类型
+    const method = ApiModel.swagger.method && ApiModel.swagger.method.toUpperCase();
+    let options = {};
+    if (['POST', 'PUT', 'PATCH'].includes(method)) {
+      options = {
+        method,
+        data: body || {},
+      };
+    } else if (['GET', 'DELETE'].includes(method)) {
+      options = {
+        method,
+        params: body || {},
+      };
+    }
+
+    request(`${ApiModel.baseUrl}${newUrl}`, options).then((resp) => {
+      if (resp.status === 200) {
+        setResult(resp);
+      } else {
+        resp
+          .clone()
+          .text()
+          .then((res: string) => {
+            if (res) {
+              setResult(JSON.parse(res));
+            } else {
+              resp
+                .clone()
+                .json()
+                .then((res2: any) => {
+                  setResult(res2);
+                });
+            }
+          });
+      }
+    });
+  }, [body]);
+
+  useEffect(() => {
+    if (form && ApiModel.debugger && ApiModel.debugger.params) {
+      const arr = ApiModel.debugger.params.map((item: any) => {
+        return {
+          name: item.name,
+          values: '',
+        };
+      });
+      form.setValues({ params: arr });
+    }
+  }, [form, ApiModel.debugger]);
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      params: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: { pageSize: 10 },
+          scroll: { x: '100%' },
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '参数名称' },
+              properties: {
+                name: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                  required: true,
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '参数名称' },
+              properties: {
+                values: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                  required: true,
+                },
+              },
+            },
+            column6: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '操作',
+                dataIndex: 'operations',
+                width: 100,
+                fixed: 'right',
+                align: 'center',
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Remove',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '新增',
+          },
+        },
+      },
+    },
+  };
+
+  return (
+    <div className={'platforms-api-swagger-content'}>
+      <div className={'swagger-content-title'}>{ApiModel.swagger.summary}</div>
+      <div className={'swagger-content-url'}>
+        <Input.Group compact>
+          <Button className={classNames('url-method', ApiModel.swagger.method)}>
+            {ApiModel.swagger.method ? ApiModel.swagger.method.toUpperCase() : ''}
+          </Button>
+          <Input
+            allowClear
+            style={{
+              width: `calc(100% - ${ApiModel.swagger.method !== 'delete' ? '140px' : '150px'})`,
+            }}
+            value={ApiModel.swagger.url}
+          />
+          <Button type="primary" onClick={onSearch}>
+            发送
+          </Button>
+        </Input.Group>
+      </div>
+      <div className={'swagger-content-item'}>
+        <TitleComponent data={'请求参数'} />
+        <div>
+          {ApiModel.debugger.params && (
+            <FormProvider form={form}>
+              <SchemaField schema={schema} />
+            </FormProvider>
+          )}
+          {ApiModel.debugger.body && (
+            <MonacoEditor
+              height={200}
+              language={'json'}
+              theme={'dark'}
+              ref={editor}
+              onChange={(value) => {
+                try {
+                  setBody(JSON.parse(value));
+                } catch (e) {
+                  console.warn(e);
+                }
+              }}
+              editorDidMount={(_editor) => {
+                _editor.getAction('editor.action.formatDocument').run();
+              }}
+            />
+          )}
+        </div>
+      </div>
+      <div className={'swagger-content-item'}>
+        <TitleComponent data={'响应内容'} />
+        <div>
+          {
+            // @ts-ignore
+            <ReactJson
+              displayObjectSize={false}
+              displayDataTypes={false}
+              name={false}
+              src={result}
+            />
+          }
+        </div>
+      </div>
+    </div>
+  );
+});

+ 33 - 0
src/pages/system/Platforms/Api/swagger-ui/index.tsx

@@ -0,0 +1,33 @@
+import { Button, Tabs } from 'antd';
+import { ApiModel } from '@/pages/system/Platforms/Api/base';
+import Base from './base';
+import Debugger from './debugging';
+
+interface SwaggerProps {
+  showDebugger?: boolean;
+}
+
+export default (props: SwaggerProps) => {
+  return (
+    <div className={'platforms-api-swagger'}>
+      <Button
+        onClick={() => {
+          ApiModel.showTable = true;
+        }}
+        className={'platforms-api-swagger-back'}
+      >
+        返回
+      </Button>
+      <Tabs type="card">
+        <Tabs.TabPane tab={'文档'} key={1}>
+          <Base />
+        </Tabs.TabPane>
+        {props.showDebugger === true && (
+          <Tabs.TabPane tab={'调试'} key={2}>
+            <Debugger />
+          </Tabs.TabPane>
+        )}
+      </Tabs>
+    </div>
+  );
+};

+ 94 - 0
src/pages/system/Platforms/View/index.tsx

@@ -0,0 +1,94 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Button, Card, Col, Popover, Row } from 'antd';
+import ApiPage from '../Api/base';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'umi';
+import { service } from '@/pages/system/Platforms';
+
+const defaultHeight = 50;
+
+export default () => {
+  const location = useLocation();
+
+  const [clientId, setClientId] = useState('');
+  const [secureKey, setSecureKey] = useState('');
+
+  const getDetail = async (id: string) => {
+    const resp = await service.getDetail(id);
+    if (resp.status === 200) {
+      setClientId(resp.result.id);
+      setSecureKey(resp.result.secureKey);
+    }
+  };
+
+  useEffect(() => {
+    const param = new URLSearchParams(location.search);
+    const code = param.get('code');
+    if (code) {
+      getDetail(code);
+    }
+  }, [location]);
+
+  useEffect(() => {
+    //  请求SDK下载地址
+  }, []);
+
+  const downLoadJDK = (
+    <div>
+      <div
+        style={{
+          width: 300,
+          height: 120,
+          padding: 12,
+          border: '1px solid #e9e9e9',
+          borderRadius: 2,
+          marginBottom: 12,
+        }}
+      >
+        暂时没有接口
+      </div>
+      <div>
+        <Button type={'primary'}>jar下载</Button>
+      </div>
+    </div>
+  );
+
+  return (
+    <PageContainer>
+      <Row gutter={[16, 16]}>
+        <Col span={24}>
+          <Row gutter={16}>
+            <Col span={12}>
+              <Card title="基本信息">
+                <div style={{ height: defaultHeight }}>
+                  <div>
+                    <span style={{ fontWeight: 'bold', fontSize: 16 }}>clientId: </span>
+                    {clientId}
+                  </div>
+                  <div>
+                    <span style={{ fontWeight: 'bold', fontSize: 16 }}>secureKey: </span>
+                    {secureKey}
+                  </div>
+                </div>
+              </Card>
+            </Col>
+            <Col span={12}>
+              <Card title="SDK下载">
+                <div style={{ height: defaultHeight }}>
+                  <Popover trigger="click" title={'POM依赖'} content={downLoadJDK}>
+                    <Button> Java </Button>
+                  </Popover>
+                </div>
+              </Card>
+            </Col>
+          </Row>
+        </Col>
+        <Col span={24}>
+          <Card title={'API文档'}>
+            <ApiPage showDebugger={true} isShowGranted={true} />
+          </Card>
+        </Col>
+      </Row>
+    </PageContainer>
+  );
+};

+ 87 - 15
src/pages/system/Platforms/index.tsx

@@ -2,22 +2,31 @@ import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { useRef, useState } from 'react';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { BadgeStatus, PermissionButton } from '@/components';
+import { useIntl, useHistory } from 'umi';
+import { BadgeStatus, PermissionButton, AIcon } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
-import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import SaveModal from './save';
 import PasswordModal from './password';
 import Service from './service';
 import { message } from 'antd';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export const service = new Service('api-client');
 
 export default () => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
+  const history = useHistory();
   const [param, setParam] = useState({});
+  const [saveType, setSaveType] = useState<'save' | 'edit'>('save');
   const [saveVisible, setSaveVisible] = useState(false);
   const [passwordVisible, setPasswordVisible] = useState(false);
   const [editData, setEditData] = useState<any | undefined>(undefined);
@@ -41,10 +50,14 @@ export default () => {
       dataIndex: 'username',
       title: '用户名',
     },
-    {
-      dataIndex: 'roleIdList',
-      title: '角色',
-    },
+    // {
+    //   dataIndex: 'roleIdList',
+    //   title: '角色',
+    //   renderText: (record => {
+    //     console.log(record);
+    //     return ''
+    //   })
+    // },
     {
       dataIndex: 'state',
       title: intl.formatMessage({
@@ -60,7 +73,7 @@ export default () => {
             text={record.text}
             statusNames={{
               enabled: StatusColorEnum.processing,
-              disable: StatusColorEnum.error,
+              disabled: StatusColorEnum.error,
             }}
           />
         ) : (
@@ -106,6 +119,7 @@ export default () => {
             }),
           }}
           onClick={() => {
+            setSaveType('edit');
             setSaveVisible(true);
             setEditData(record);
           }}
@@ -116,30 +130,37 @@ export default () => {
           key={'empowerment'}
           type={'link'}
           style={{ padding: 0 }}
-          isPermission={permission.update}
+          isPermission={permission.empowerment}
           tooltip={{
             title: '赋权',
           }}
-          onClick={() => {}}
+          onClick={() => {
+            const url = getMenuPathByCode('system/Platforms/Api');
+            history.push(`${url}?code=${record.id}`);
+          }}
         >
-          <EditOutlined />
+          <AIcon type={'icon-fuquan'} />
         </PermissionButton>,
         <PermissionButton
           key={'api'}
           type={'link'}
           style={{ padding: 0 }}
+          isPermission={true}
           tooltip={{
             title: '查看API',
           }}
-          onClick={() => {}}
+          onClick={() => {
+            const url = getMenuPathByCode('system/Platforms/View');
+            history.push(`${url}?code=${record.id}`);
+          }}
         >
-          <EditOutlined />
+          <AIcon type={'icon-chakanAPI'} />
         </PermissionButton>,
         <PermissionButton
           key={'password'}
           type={'link'}
           style={{ padding: 0 }}
-          isPermission={permission.action}
+          isPermission={permission.update}
           tooltip={{
             title: '重置密码',
           }}
@@ -148,7 +169,56 @@ export default () => {
             setPasswordVisible(true);
           }}
         >
-          <EditOutlined />
+          <AIcon type={'icon-zhongzhimima'} />
+        </PermissionButton>,
+        <PermissionButton
+          key={'state'}
+          type={'link'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'disabled' ? 'disabled' : 'enabled'
+              }.tips`,
+              defaultMessage: '确认禁用?',
+            }),
+            onConfirm: () => {
+              if (record.state.value !== 'disabled') {
+                service.undeploy(record.id).then((resp: any) => {
+                  if (resp.status === 200) {
+                    message.success(
+                      intl.formatMessage({
+                        id: 'pages.data.option.success',
+                        defaultMessage: '操作成功!',
+                      }),
+                    );
+                    actionRef.current?.reload();
+                  }
+                });
+              } else {
+                service.deploy(record.id).then((resp: any) => {
+                  if (resp.status === 200) {
+                    message.success(
+                      intl.formatMessage({
+                        id: 'pages.data.option.success',
+                        defaultMessage: '操作成功!',
+                      }),
+                    );
+                    actionRef.current?.reload();
+                  }
+                });
+              }
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'disabled' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
         </PermissionButton>,
         <PermissionButton
           key={'delete'}
@@ -198,6 +268,7 @@ export default () => {
             type="primary"
             isPermission={permission.add}
             onClick={() => {
+              setSaveType('save');
               setSaveVisible(true);
             }}
             icon={<PlusOutlined />}
@@ -212,6 +283,7 @@ export default () => {
       <SaveModal
         visible={saveVisible}
         data={editData}
+        type={saveType}
         onCancel={() => {
           setSaveVisible(false);
           setEditData(undefined);

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

@@ -1,4 +1,5 @@
-import { createForm, Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import type { Field } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import {
   Checkbox,
@@ -14,19 +15,20 @@ import {
   TreeSelect,
 } from '@formily/antd';
 import { message, Modal } from 'antd';
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
 import { PlusOutlined } from '@ant-design/icons';
 import type { ISchema } from '@formily/json-schema';
 import { PermissionButton } from '@/components';
 import usePermissions from '@/hooks/permission';
 import { action } from '@formily/reactive';
-import { Response } from '@/utils/typings';
+import type { Response } from '@/utils/typings';
 import { service } from '@/pages/system/Platforms/index';
 import { randomString } from '@/utils/util';
 
 interface SaveProps {
   visible: boolean;
+  type: 'save' | 'edit';
   data?: any;
   onReload?: () => void;
   onCancel?: () => void;
@@ -77,13 +79,33 @@ export default (props: SaveProps) => {
     () =>
       createForm({
         validateFirst: true,
-        initialValues: props.data
-          ? { ...props.data, confirm_password: props.data.password }
-          : { enableOAuth2: true, id: randomString() },
       }),
     [props.data],
   );
 
+  const getDetail = async (id: string) => {
+    const resp = await service.getDetail(id);
+    if (resp.status === 200) {
+      form.setValues({
+        ...resp.result,
+        confirm_password: resp.result.password,
+      });
+    }
+  };
+
+  useEffect(() => {
+    if (props.visible) {
+      if (props.type === 'edit') {
+        getDetail(props.data.id);
+      } else {
+        form.setValues({
+          enableOAuth2: true,
+          id: randomString(),
+        });
+      }
+    }
+  }, [props.type, props.visible]);
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -292,7 +314,7 @@ export default (props: SaveProps) => {
                     const tab: any = window.open(`${origin}/#/system/role?save=true`);
                     tab!.onTabSaveSuccess = (value: any) => {
                       form.setFieldState('roleIdList', async (state) => {
-                        state.dataSource = await getRole().then((resp) =>
+                        state.dataSource = await getRole().then((resp: any) =>
                           resp.result?.map((item: Record<string, unknown>) => ({
                             ...item,
                             label: item.name,
@@ -409,7 +431,7 @@ export default (props: SaveProps) => {
     console.log(data);
     if (data) {
       setLoading(true);
-      const resp: any = props.data ? await service.update(data) : await service.save(data);
+      const resp: any = props.type === 'edit' ? await service.edit(data) : await service.save(data);
       setLoading(false);
       if (resp.status === 200) {
         if (props.onReload) {
@@ -419,7 +441,7 @@ export default (props: SaveProps) => {
         message.success('操作成功');
       }
     }
-  }, [props.data]);
+  }, [props.type]);
 
   return (
     <Modal

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

@@ -9,6 +9,10 @@ class Service extends BaseService<platformsType> {
       params,
     });
 
+  getDetail = (id: string) => request(`${this.uri}/${id}/detail`, { method: 'GET' });
+
+  edit = (data: any) => request(`${this.uri}/${data.id}`, { method: 'PUT', data });
+
   /**
    * 密码校验
    * @param type
@@ -25,6 +29,43 @@ class Service extends BaseService<platformsType> {
       method: 'POST',
       data,
     });
+
+  undeploy = (id: string) => request(`${this.uri}/${id}/disable`, { method: 'PUT' });
+
+  deploy = (id: string) => request(`${this.uri}/${id}/enable`, { method: 'PUT' });
+
+  getApiFirstLevel = () =>
+    request(`/${SystemConst.API_BASE}/v3/api-docs/swagger-config`, { method: 'GET' });
+
+  getApiNextLevel = (name: string) =>
+    request(`/${SystemConst.API_BASE}/v3/api-docs/${name}`, { method: 'GET' });
+
+  /**
+   * 对接口进行授权
+   * @param id 第三方平台的ID
+   * @param data
+   */
+  saveApiGrant = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/grant`, { method: 'POST', data });
+
+  addApiGrant = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/grant/_add`, { method: 'POST', data });
+
+  removeApiGrant = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/grant/_delete`, { method: 'POST', data });
+
+  /**
+   * 获取已授权的接口ID
+   * @param id 第三方平台的ID
+   */
+  getApiGranted = (id: string) =>
+    request(`/${SystemConst.API_BASE}/api-client/${id}/granted`, { method: 'GET' });
+
+  /**
+   * 获取可授权的接口ID
+   */
+  apiOperations = () =>
+    request(`/${SystemConst.API_BASE}//api-client/operations`, { method: 'GET' });
 }
 
 export default Service;

+ 4 - 1
src/utils/menu/index.ts

@@ -64,7 +64,10 @@ const extraRouteObj = {
     children: [{ code: 'AMap', name: '地图' }],
   },
   'system/Platforms': {
-    children: [{ code: 'Api', name: '赋权' }],
+    children: [
+      { code: 'Api', name: '赋权' },
+      { code: 'View', name: 'Api详情' },
+    ],
   },
 };
 //额外路由

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

@@ -123,6 +123,8 @@ export enum MENUS_CODE {
   'Northbound/AliCloud' = 'Northbound/AliCloud',
   'Northbound/AliCloud/Detail' = 'Northbound/AliCloud/Detail',
   'system/Platforms' = 'system/Platforms',
+  'system/Platforms/Api' = 'system/Platforms/Api',
+  'system/Platforms/View' = 'system/Platforms/View',
 }
 
 export type MENUS_CODE_TYPE = keyof typeof MENUS_CODE | string;
@@ -144,6 +146,7 @@ export enum BUTTON_PERMISSION_ENUM {
   'debug' = 'debug',
   'log' = 'log',
   'tigger' = 'tigger',
+  'empowerment' = 'empowerment',
 }
 
 // 调试按钮、通知记录、批量导出、批量导入、选择通道、推送、分配资产、绑定用户对应的ID是啥