Bläddra i källkod

ant design pro v5 发布 (#8642)

* init

* remove Authorized

* remove login

* remove request

* remove path-to-regexp"

* remove create-umi

* clean dependencies

* remove unused less

* better login mock

* add ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION=site env

* support settings dynamic

* support umi-plugin-setting-drawer

* 🎨 format: clean code

* remove clean fetch:blocks

* add NoticeIcon

* add NoticeIcon

* use  "@ant-design/pro-layout": "6.0.0-1"

* support errorHandler

* remove SelectLang

* update version

* PageHeaderWrapper to PageContainer

* PageHeaderWrapper to PageContainer (#6622)

* 📌 versions: use alpha version (#6634)

* use alpha version

5.0.0 no yet

* less test

* fix lint error

* rename setting

Co-authored-by: chenshuai2144 <qixian.cs@outlook.com>

* fix 401 do not to login error

* 405 start:no-mock

* siderWidth: 208

* side

* false

* username

* remove resolutions

* update list demo

* correct the text

* fix tabs style in antd@4.3

* fix login error

* use  "@ant-design/pro-layout": "6.0.0-5"

* eslint

* access

* pro-layout 6.0.0-7

* merge master

* up layout version

* 💄 UI: fix login style warning

* upgrade dependency

* delete throw error

* versions: up @umijs/fabric

* change login

* do not open defaultOpenAll

* rebase error

* update demos

* deps upgreade

* :bug: 修复切换不同权限的账户登录后,access的鉴权不生效问题 (#6997)

* :bug: 修复切换不同权限的账户登录后,access的鉴权不生效问题

* :art: prettier code

* 修改hash路由下search参数在hash之前,search最后一个参数错误bug (#6908)

* 🐛 bug: fix tsc error

* config.js里面 history.type 为 hash时,退出重登录后,拼接返回路径时,#号前路径会重复

* prettier all code

* merge master

* 🐛 bug: fix the problem that modifying URL can bypass permissions

* reset hash

* fix infinite loop of login page (#7095)

* merge master

* rm unuse code

* rm unuse code

* remove unuse code

* 🐛 bug: fix redirect error in hash mode

* fix redirect

* 💥 feat: support defaultSettings

close #7407

* sort

* sorter

* default close umi ui

* goto reload window.location.href

* add setupTests.js

* add localStorageMock init

* open esbuild

* add exportStatic

* better code

* add initialStateConfig

* remove unuse code

* login 翻译

* add key

* update siderWidth

close #7585

* fix redirect error

close #7632

* add childrenRender type

* better styles

* better styles

* remove unuse code

close #7648

* default use browser history

* close

* support unAccessible

* up @umijs/preset-react version

* merge master

* support dumi function

* ✨ feat: add react-dev-inspector plugins

* 🚑 hotfix: support InspectorWrapper

* 🚑 hotfix: support InspectorWrapper

* add dumi docs href

* 👷 CI: fix Deploy CI

* 👷 CI : fix azure-pipelines

* fix keys error

* fix: version (#7789)

* fix lint

* fix lint

* fix typo

* fix type

* better code

* add openAPI plugins

* use openapi plugins

* support schemaPath = url

* add umi-plugins

* add dumi doc

* add plugin doc

* add config doc

* fix: outLogin request  method (#8059) (#8061)

Co-authored-by: luqili <a@luqili.com>

* 非 dev 环境不展示 openapi

* locale pt_br Pages added (#8088)

* revert surge preview

* test: update rebase action

* add persian local and fix menu direction for rtl display (#8079)

* up @umijs/preset-dumi version

* up typescript verison

* add waterMark

* default open webpack5

* default open webpack5

* support webpack5

* support openAPI is array

* upgrade deps

* dir

* 页面代码结构推荐

* Update global.tsx

* Update index.tsx

* refactor(app.tsx): dev links use Link

* loginPath

* fix ci

* fix tsc

* fix tsc

* fix(app.tsx): dev links use umi Link

* fix tes

* fix warn

* refactor(app.tsx): optimization import Link

* resetv5-app-tsx-links 

#8226

* use link to hash

* default open fastRefresh

* Update README.md (#8356)

Ant Design Pro V5 is not released yet.
`trial` is often used for released paid product, and `preview` is the regularly used word for pre-release version.

* Fixing a few issues with internationalization on the login (#7945)

Co-authored-by: chenshuai2144 <qixian.cs@outlook.com>

* Update api.ts (#8424)

* chore: support verify commit

* fix locale error

* support all block

* locale: 增加标记,以便删除国际化脚本识别

* fix(locale): 删除request错误捕捉

* chore: remove tag

* chore: use fabric verify-commit

* chore: up to 5.0.0-beta.3

* chore: remove home page png

* use latest

* merge master

* fix conflict type and eslint waring! (#8644)

* chore: open mfsu and webpack5

* try fix ci

* fix ci

* try fix ci

* add test

* add test

* 增加加载时间

* fix test

* prettier all code

* docs: change oneapi doc

* chore: add umi

* rmeove unuse code

Co-authored-by: lijiehua <41830859@qq.com>
Co-authored-by: 偏右 <afc163@gmail.com>
Co-authored-by: Jerry <510846@qq.com>
Co-authored-by: Vern Brandl <tkvern@users.noreply.github.com>
Co-authored-by: Lee Fan <535536456@qq.com>
Co-authored-by: zhangshuling <Zsl0516>
Co-authored-by: Justin <zcodeworld@gmail.com>
Co-authored-by: Jeff Tian <jeff.tian@outlook.com>
Co-authored-by: luqili <33475522+luqil@users.noreply.github.com>
Co-authored-by: luqili <a@luqili.com>
Co-authored-by: Thiago Tognoli <thiagotognoli@users.noreply.github.com>
Co-authored-by: abolfazl <Abolfazl.rajabpour@gmail.com>
Co-authored-by: HouKunLin <houkunlin@aliyun.com>
Co-authored-by: SHINCHVEN <shinchven@gmail.com>
Co-authored-by: Scott Goci <scottjg@gmail.com>
Co-authored-by: _XiaoTian <istianlei@qq.com>
Co-authored-by: 丁田秀 <295434665@qq.com>
Co-authored-by: Jiankian <i@anline.cn>
陈帅 4 år sedan
förälder
incheckning
a2d434f023
98 ändrade filer med 3178 tillägg och 2510 borttagningar
  1. 2 2
      .github/workflows/ci.yml
  2. 5 2
      .github/workflows/deploy.yml
  3. 3 3
      .github/workflows/preview-build.yml
  4. 1 1
      .github/workflows/preview-deploy.yml
  5. 7 7
      .github/workflows/rebase.yml
  6. 1 1
      README.md
  7. 4 0
      config/config.dev.ts
  8. 30 5
      config/config.ts
  9. 9 11
      config/defaultSettings.ts
  10. 593 0
      config/oneapi.json
  11. 32 53
      config/routes.ts
  12. 1 0
      jest.config.js
  13. 16 11
      mock/listTableList.ts
  14. 99 97
      mock/notices.ts
  15. 87 52
      mock/user.ts
  16. 26 26
      package.json
  17. BIN
      public/home_bg.png
  18. 0 0
      public/logo.svg
  19. 9 0
      src/access.ts
  20. 136 0
      src/app.tsx
  21. 0 35
      src/components/Authorized/Authorized.tsx
  22. 0 33
      src/components/Authorized/AuthorizedRoute.tsx
  23. 0 88
      src/components/Authorized/CheckPermissions.tsx
  24. 0 96
      src/components/Authorized/PromiseRender.tsx
  25. 0 80
      src/components/Authorized/Secured.tsx
  26. 0 11
      src/components/Authorized/index.tsx
  27. 0 31
      src/components/Authorized/renderAuthorize.ts
  28. 37 0
      src/components/Footer/index.tsx
  29. 0 88
      src/components/GlobalHeader/AvatarDropdown.tsx
  30. 0 168
      src/components/GlobalHeader/NoticeIconView.tsx
  31. 0 83
      src/components/GlobalHeader/RightContent.tsx
  32. 3 8
      src/components/HeaderSearch/index.less
  33. 7 11
      src/components/HeaderSearch/index.tsx
  34. 126 0
      src/components/NoticeIcon/NoticeIcon.tsx
  35. 8 11
      src/components/NoticeIcon/NoticeList.tsx
  36. 136 126
      src/components/NoticeIcon/index.tsx
  37. 0 5
      src/components/PageLoading/index.tsx
  38. 103 0
      src/components/RightContent/AvatarDropdown.tsx
  39. 4 24
      src/components/GlobalHeader/index.less
  40. 62 0
      src/components/RightContent/index.tsx
  41. 272 0
      src/components/index.md
  42. 0 1
      src/e2e/__mocks__/antd-pro-merge-less.js
  43. 6 2
      src/e2e/baseLayout.e2e.js
  44. 3 0
      src/global.less
  45. 1 2
      src/global.tsx
  46. 0 183
      src/layouts/BasicLayout.tsx
  47. 0 10
      src/layouts/BlankLayout.tsx
  48. 0 58
      src/layouts/SecurityLayout.tsx
  49. 0 70
      src/layouts/UserLayout.tsx
  50. 2 1
      src/locales/en-US.ts
  51. 3 1
      src/locales/en-US/pages.ts
  52. 24 0
      src/locales/fa-IR.ts
  53. 5 0
      src/locales/fa-IR/component.ts
  54. 17 0
      src/locales/fa-IR/globalHeader.ts
  55. 52 0
      src/locales/fa-IR/menu.ts
  56. 67 0
      src/locales/fa-IR/pages.ts
  57. 7 0
      src/locales/fa-IR/pwa.ts
  58. 32 0
      src/locales/fa-IR/settingDrawer.ts
  59. 60 0
      src/locales/fa-IR/settings.ts
  60. 2 0
      src/locales/pt-BR.ts
  61. 70 0
      src/locales/pt-BR/pages.ts
  62. 1 0
      src/locales/zh-CN.ts
  63. 3 1
      src/locales/zh-CN/pages.ts
  64. 0 30
      src/models/connect.d.ts
  65. 0 126
      src/models/global.ts
  66. 0 93
      src/models/login.ts
  67. 0 38
      src/models/setting.ts
  68. 0 85
      src/models/user.ts
  69. 2 4
      src/pages/TableList/components/UpdateForm.tsx
  70. 0 36
      src/pages/TableList/data.d.ts
  71. 14 15
      src/pages/TableList/index.tsx
  72. 0 38
      src/pages/TableList/service.ts
  73. 0 44
      src/pages/User/login/index.less
  74. 0 263
      src/pages/User/login/index.tsx
  75. 0 1
      src/pages/document.ejs
  76. 43 0
      src/layouts/UserLayout.less
  77. 318 0
      src/pages/user/Login/index.tsx
  78. 1 1
      src/service-worker.js
  79. 83 0
      src/services/ant-design-pro/api.ts
  80. 10 0
      src/services/ant-design-pro/index.ts
  81. 21 0
      src/services/ant-design-pro/login.ts
  82. 101 0
      src/services/ant-design-pro/typings.d.ts
  83. 0 19
      src/services/login.ts
  84. 12 0
      src/services/swagger/index.ts
  85. 166 0
      src/services/swagger/pet.ts
  86. 54 0
      src/services/swagger/store.ts
  87. 52 0
      src/services/swagger/typings.d.ts
  88. 114 0
      src/services/swagger/user.ts
  89. 0 13
      src/services/user.ts
  90. 0 21
      src/typings.d.ts
  91. 0 16
      src/utils/Authorized.ts
  92. 0 32
      src/utils/authority.ts
  93. 0 55
      src/utils/request.ts
  94. 0 16
      src/utils/utils.less
  95. 0 37
      src/utils/utils.test.ts
  96. 0 24
      src/utils/utils.ts
  97. 3 5
      tests/run-tests.js
  98. 10 0
      tests/setupTests.js

+ 2 - 2
.github/workflows/ci.yml

@@ -7,7 +7,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        node_version: [10.x, 12.x]
+        node_version: [12.x, 14.x]
         os: [ubuntu-latest, windows-latest, macOS-latest]
     steps:
       - uses: actions/checkout@v1
@@ -16,7 +16,7 @@ jobs:
         with:
           node-version: ${{ matrix.node_version }}
       - run: echo ${{github.ref}}
-      - run: npm install
+      - run: yarn
       - run: yarn run lint
       - run: yarn run tsc
       - run: yarn run build

+ 5 - 2
.github/workflows/deploy.yml

@@ -12,10 +12,13 @@ jobs:
         uses: actions/checkout@master
 
       - name: install
-        run: npm install
+        run: yarn
 
       - name: plugins
-        run: yarn add umi-plugin-antd-theme umi-plugin-pro
+        run: yarn add umi-plugin-antd-theme umi-plugin-pro umi-plugin-setting-drawer
+
+      - name: fetch-blocks
+        run: yarn run pro fetch-blocks --branch=v5
 
       - name: site
         run: npm run site

+ 3 - 3
.github/workflows/preview-build.yml

@@ -15,9 +15,9 @@ jobs:
 
       - name: build
         run: |
-          npm install
-          npm install umi-plugin-pro --save
-          npm run build
+          yarn
+          yarn add umi-plugin-pro --save
+          yarn build
 
       - name: upload dist artifact
         uses: actions/upload-artifact@v2

+ 1 - 1
.github/workflows/preview-deploy.yml

@@ -2,7 +2,7 @@ name: Preview Deploy
 
 on:
   workflow_run:
-    workflows: ["Preview Build"]
+    workflows: ['Preview Build']
     types:
       - completed
 

+ 7 - 7
.github/workflows/rebase.yml

@@ -8,10 +8,10 @@ jobs:
     if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@master
-      with:
-        fetch-depth: 0
-    - name: Automatic Rebase
-      uses: cirrus-actions/rebase@1.3
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - uses: actions/checkout@master
+        with:
+          fetch-depth: 0
+      - name: Automatic Rebase
+        uses: cirrus-actions/rebase@1.3
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 1 - 1
README.md

@@ -21,7 +21,7 @@ An out-of-box UI solution for enterprise applications as a React boilerplate.
 - FAQ: http://pro.ant.design/docs/faq
 - Mirror Site in China: http://ant-design-pro.gitee.io
 
-## 5.0 is ready for trial! 🎉🎉🎉
+## 5.0 is ready for preview! 🎉🎉🎉
 
 [Try Ant Design Pro 5.0.0](https://beta-pro.ant.design/docs/upgrade-v5-cn)
 

+ 4 - 0
config/config.dev.ts

@@ -12,4 +12,8 @@ export default defineConfig({
     babelPlugins: [],
     babelOptions: {},
   },
+  // mfsu: {},
+  // webpack5: {
+  //    lazyCompilation: {},
+  // },
 });

+ 30 - 5
config/config.ts

@@ -1,5 +1,7 @@
 // https://umijs.org/config/
 import { defineConfig } from 'umi';
+import { join } from 'path';
+
 import defaultSettings from './defaultSettings';
 import proxy from './proxy';
 import routes from './routes';
@@ -12,9 +14,13 @@ export default defineConfig({
   dva: {
     hmr: true,
   },
-  history: {
-    type: 'browser',
+  layout: {
+    // https://umijs.org/zh-CN/plugins/plugin-layout
+    locale: true,
+    siderWidth: 208,
+    ...defaultSettings,
   },
+  // https://umijs.org/zh-CN/plugins/plugin-locale
   locale: {
     // default zh-CN
     default: 'zh-CN',
@@ -23,7 +29,7 @@ export default defineConfig({
     baseNavigator: true,
   },
   dynamicImport: {
-    loading: '@/components/PageLoading/index',
+    loading: '@ant-design/pro-layout/es/PageLoading',
   },
   targets: {
     ie: 11,
@@ -34,14 +40,33 @@ export default defineConfig({
   theme: {
     'primary-color': defaultSettings.primaryColor,
   },
+  // esbuild is father build tools
+  // https://umijs.org/plugins/plugin-esbuild
+  esbuild: {},
   title: false,
   ignoreMomentLocale: true,
   proxy: proxy[REACT_APP_ENV || 'dev'],
   manifest: {
     basePath: '/',
   },
-  // 快速刷新功能 https://umijs.org/config#fastrefresh
+  // Fast Refresh 热更新
   fastRefresh: {},
-  esbuild: {},
+  openAPI: [
+    {
+      requestLibPath: "import { request } from 'umi'",
+      // 或者使用在线的版本
+      // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
+      schemaPath: join(__dirname, 'oneapi.json'),
+      mock: false,
+    },
+    {
+      requestLibPath: "import { request } from 'umi'",
+      schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
+      projectName: 'swagger',
+    },
+  ],
+  nodeModulesTransform: { type: 'none' },
+  mfsu: {},
   webpack5: {},
+  exportStatic: {},
 });

+ 9 - 11
config/defaultSettings.ts

@@ -1,23 +1,21 @@
-import { Settings as ProSettings } from '@ant-design/pro-layout';
+import { Settings as LayoutSettings } from '@ant-design/pro-layout';
 
-type DefaultSettings = Partial<ProSettings> & {
-  pwa: boolean;
-};
-
-const proSettings: DefaultSettings = {
-  navTheme: 'dark',
+const Settings: LayoutSettings & {
+  pwa?: boolean;
+  logo?: string;
+} = {
+  navTheme: 'light',
   // 拂晓蓝
   primaryColor: '#1890ff',
-  layout: 'side',
+  layout: 'mix',
   contentWidth: 'Fluid',
   fixedHeader: false,
   fixSiderbar: true,
   colorWeak: false,
   title: 'Ant Design Pro',
   pwa: false,
+  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
   iconfontUrl: '',
 };
 
-export type { DefaultSettings };
-
-export default proSettings;
+export default Settings;

+ 593 - 0
config/oneapi.json

@@ -0,0 +1,593 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "Ant Design Pro",
+    "version": "1.0.0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost:8000/"
+    },
+    {
+      "url": "https://localhost:8000/"
+    }
+  ],
+  "paths": {
+    "/api/currentUser": {
+      "get": {
+        "tags": ["api"],
+        "description": "获取当前的用户",
+        "operationId": "currentUser",
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/CurrentUser"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "x-swagger-router-controller": "api"
+    },
+    "/api/login/captcha": {
+      "post": {
+        "description": "发送验证码",
+        "operationId": "getFakeCaptcha",
+        "tags": ["login"],
+        "parameters": [
+          {
+            "name": "phone",
+            "in": "query",
+            "description": "手机号",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/FakeCaptcha"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/api/login/outLogin": {
+      "post": {
+        "description": "登录接口",
+        "operationId": "outLogin",
+        "tags": ["login"],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "x-swagger-router-controller": "api"
+    },
+    "/api/login/account": {
+      "post": {
+        "tags": ["login"],
+        "description": "登录接口",
+        "operationId": "login",
+        "requestBody": {
+          "description": "登录系统",
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/LoginParams"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/LoginResult"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        },
+        "x-codegen-request-body-name": "body"
+      },
+      "x-swagger-router-controller": "api"
+    },
+    "/api/notices": {
+      "summary": "getNotices",
+      "description": "NoticeIconItem",
+      "get": {
+        "tags": ["api"],
+        "operationId": "getNotices",
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/NoticeIconList"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/api/rule": {
+      "get": {
+        "tags": ["rule"],
+        "description": "获取规则列表",
+        "operationId": "rule",
+        "parameters": [
+          {
+            "name": "current",
+            "in": "query",
+            "description": "当前的页码",
+            "schema": {
+              "type": "number"
+            }
+          },
+          {
+            "name": "pageSize",
+            "in": "query",
+            "description": "页面的容量",
+            "schema": {
+              "type": "number"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RuleList"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "tags": ["rule"],
+        "description": "新建规则",
+        "operationId": "addRule",
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RuleListItem"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "put": {
+        "tags": ["rule"],
+        "description": "新建规则",
+        "operationId": "updateRule",
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RuleListItem"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete": {
+        "tags": ["rule"],
+        "description": "删除规则",
+        "operationId": "removeRule",
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "x-swagger-router-controller": "api"
+    },
+    "/swagger": {
+      "x-swagger-pipe": "swagger_raw"
+    }
+  },
+  "components": {
+    "schemas": {
+      "CurrentUser": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "avatar": {
+            "type": "string"
+          },
+          "userid": {
+            "type": "string"
+          },
+          "email": {
+            "type": "string"
+          },
+          "signature": {
+            "type": "string"
+          },
+          "title": {
+            "type": "string"
+          },
+          "group": {
+            "type": "string"
+          },
+          "tags": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "properties": {
+                "key": {
+                  "type": "string"
+                },
+                "label": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "notifyCount": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "unreadCount": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "country": {
+            "type": "string"
+          },
+          "access": {
+            "type": "string"
+          },
+          "geographic": {
+            "type": "object",
+            "properties": {
+              "province": {
+                "type": "object",
+                "properties": {
+                  "label": {
+                    "type": "string"
+                  },
+                  "key": {
+                    "type": "string"
+                  }
+                }
+              },
+              "city": {
+                "type": "object",
+                "properties": {
+                  "label": {
+                    "type": "string"
+                  },
+                  "key": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          },
+          "address": {
+            "type": "string"
+          },
+          "phone": {
+            "type": "string"
+          }
+        }
+      },
+      "LoginResult": {
+        "type": "object",
+        "properties": {
+          "status": {
+            "type": "string"
+          },
+          "type": {
+            "type": "string"
+          },
+          "currentAuthority": {
+            "type": "string"
+          }
+        }
+      },
+      "PageParams": {
+        "type": "object",
+        "properties": {
+          "current": {
+            "type": "number"
+          },
+          "pageSize": {
+            "type": "number"
+          }
+        }
+      },
+      "RuleListItem": {
+        "type": "object",
+        "properties": {
+          "key": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "disabled": {
+            "type": "boolean"
+          },
+          "href": {
+            "type": "string"
+          },
+          "avatar": {
+            "type": "string"
+          },
+          "name": {
+            "type": "string"
+          },
+          "owner": {
+            "type": "string"
+          },
+          "desc": {
+            "type": "string"
+          },
+          "callNo": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "status": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "updatedAt": {
+            "type": "string",
+            "format": "datetime"
+          },
+          "createdAt": {
+            "type": "string",
+            "format": "datetime"
+          },
+          "progress": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "RuleList": {
+        "type": "object",
+        "properties": {
+          "data": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/RuleListItem"
+            }
+          },
+          "total": {
+            "type": "integer",
+            "description": "列表的内容总数",
+            "format": "int32"
+          },
+          "success": {
+            "type": "boolean"
+          }
+        }
+      },
+      "FakeCaptcha": {
+        "type": "object",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "status": {
+            "type": "string"
+          }
+        }
+      },
+      "LoginParams": {
+        "type": "object",
+        "properties": {
+          "username": {
+            "type": "string"
+          },
+          "password": {
+            "type": "string"
+          },
+          "autoLogin": {
+            "type": "boolean"
+          },
+          "type": {
+            "type": "string"
+          }
+        }
+      },
+      "ErrorResponse": {
+        "required": ["errorCode"],
+        "type": "object",
+        "properties": {
+          "errorCode": {
+            "type": "string",
+            "description": "业务约定的错误码"
+          },
+          "errorMessage": {
+            "type": "string",
+            "description": "业务上的错误信息"
+          },
+          "success": {
+            "type": "boolean",
+            "description": "业务上的请求是否成功"
+          }
+        }
+      },
+      "NoticeIconList": {
+        "type": "object",
+        "properties": {
+          "data": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/NoticeIconItem"
+            }
+          },
+          "total": {
+            "type": "integer",
+            "description": "列表的内容总数",
+            "format": "int32"
+          },
+          "success": {
+            "type": "boolean"
+          }
+        }
+      },
+      "NoticeIconItemType": {
+        "title": "NoticeIconItemType",
+        "description": "已读未读列表的枚举",
+        "type": "string",
+        "properties": {},
+        "enum": ["notification", "message", "event"]
+      },
+      "NoticeIconItem": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string"
+          },
+          "extra": {
+            "type": "string",
+            "format": "any"
+          },
+          "key": { "type": "string" },
+          "read": {
+            "type": "boolean"
+          },
+          "avatar": {
+            "type": "string"
+          },
+          "title": {
+            "type": "string"
+          },
+          "status": {
+            "type": "string"
+          },
+          "datetime": {
+            "type": "string",
+            "format": "date"
+          },
+          "description": {
+            "type": "string"
+          },
+          "type": {
+            "extensions": {
+              "x-is-enum": true
+            },
+            "$ref": "#/components/schemas/NoticeIconItemType"
+          }
+        }
+      }
+    }
+  }
+}

+ 32 - 53
config/routes.ts

@@ -1,73 +1,52 @@
 export default [
   {
-    path: '/',
-    component: '../layouts/BlankLayout',
+    path: '/user',
+    layout: false,
     routes: [
       {
         path: '/user',
-        component: '../layouts/UserLayout',
         routes: [
           {
             name: 'login',
             path: '/user/login',
-            component: './User/login',
+            component: './user/Login',
           },
         ],
       },
+    ],
+  },
+  {
+    path: '/welcome',
+    name: 'welcome',
+    icon: 'smile',
+    component: './Welcome',
+  },
+  {
+    path: '/admin',
+    name: 'admin',
+    icon: 'crown',
+    access: 'canAdmin',
+    component: './Admin',
+    routes: [
       {
-        path: '/',
-        component: '../layouts/SecurityLayout',
-        routes: [
-          {
-            path: '/',
-            component: '../layouts/BasicLayout',
-            authority: ['admin', 'user'],
-            routes: [
-              {
-                path: '/',
-                redirect: '/welcome',
-              },
-              {
-                path: '/welcome',
-                name: 'welcome',
-                icon: 'smile',
-                component: './Welcome',
-              },
-              {
-                path: '/admin',
-                name: 'admin',
-                icon: 'crown',
-                component: './Admin',
-                authority: ['admin'],
-                routes: [
-                  {
-                    path: '/admin/sub-page',
-                    name: 'sub-page',
-                    icon: 'smile',
-                    component: './Welcome',
-                    authority: ['admin'],
-                  },
-                ],
-              },
-              {
-                name: 'list.table-list',
-                icon: 'table',
-                path: '/list',
-                component: './TableList',
-              },
-              {
-                component: './404',
-              },
-            ],
-          },
-          {
-            component: './404',
-          },
-        ],
+        path: '/admin/sub-page',
+        name: 'sub-page',
+        icon: 'smile',
+        component: './Welcome',
       },
     ],
   },
   {
+    name: 'list.table-list',
+    icon: 'table',
+    path: '/list',
+    component: './TableList',
+  },
+  {
+    path: '/',
+    redirect: '/welcome',
+  },
+  {
     component: './404',
   },
 ];

+ 1 - 0
jest.config.js

@@ -2,6 +2,7 @@ module.exports = {
   testURL: 'http://localhost:8000',
   testEnvironment: './tests/PuppeteerEnvironment',
   verbose: false,
+  extraSetupFiles: ['./tests/setupTests.js'],
   globals: {
     ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
     localStorage: null,

+ 16 - 11
mock/listTableList.ts

@@ -1,10 +1,11 @@
 // eslint-disable-next-line import/no-extraneous-dependencies
 import { Request, Response } from 'express';
-import { TableListItem, TableListParams } from '@/pages/TableList/data';
+import moment from 'moment';
+import { parse } from 'url';
 
 // mock tableListDataSource
 const genList = (current: number, pageSize: number) => {
-  const tableListDataSource: TableListItem[] = [];
+  const tableListDataSource: API.RuleListItem[] = [];
 
   for (let i = 0; i < pageSize; i += 1) {
     const index = (current - 1) * 10 + i;
@@ -21,8 +22,8 @@ const genList = (current: number, pageSize: number) => {
       desc: '这是一段描述',
       callNo: Math.floor(Math.random() * 1000),
       status: Math.floor(Math.random() * 10) % 4,
-      updatedAt: new Date(),
-      createdAt: new Date(),
+      updatedAt: moment().format('YYYY-MM-DD'),
+      createdAt: moment().format('YYYY-MM-DD'),
       progress: Math.ceil(Math.random() * 100),
     });
   }
@@ -38,13 +39,17 @@ function getRule(req: Request, res: Response, u: string) {
     realUrl = req.url;
   }
   const { current = 1, pageSize = 10 } = req.query;
-  const params = (new URLSearchParams(realUrl.split('?')[1]) as unknown) as TableListParams;
+  const params = parse(realUrl, true).query as unknown as API.PageParams &
+    API.RuleListItem & {
+      sorter: any;
+      filter: any;
+    };
 
   let dataSource = [...tableListDataSource].slice(
     ((current as number) - 1) * (pageSize as number),
     (current as number) * (pageSize as number),
   );
-  const sorter = JSON.parse(params.sorter as any);
+  const sorter = JSON.parse(params.sorter || ('{}' as any));
   if (sorter) {
     dataSource = dataSource.sort((prev, next) => {
       let sortNumber = 0;
@@ -86,14 +91,14 @@ function getRule(req: Request, res: Response, u: string) {
   }
 
   if (params.name) {
-    dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
+    dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
   }
   const result = {
     data: dataSource,
     total: tableListDataSource.length,
     success: true,
     pageSize,
-    current: parseInt(`${params.currentPage}`, 10) || 1,
+    current: parseInt(`${params.current}`, 10) || 1,
   };
 
   return res.json(result);
@@ -116,7 +121,7 @@ function postRule(req: Request, res: Response, u: string, b: Request) {
     case 'post':
       (() => {
         const i = Math.ceil(Math.random() * 10000);
-        const newRule = {
+        const newRule: API.RuleListItem = {
           key: tableListDataSource.length,
           href: 'https://ant.design',
           avatar: [
@@ -128,8 +133,8 @@ function postRule(req: Request, res: Response, u: string, b: Request) {
           desc,
           callNo: Math.floor(Math.random() * 1000),
           status: Math.floor(Math.random() * 10) % 2,
-          updatedAt: new Date(),
-          createdAt: new Date(),
+          updatedAt: moment().format('YYYY-MM-DD'),
+          createdAt: moment().format('YYYY-MM-DD'),
           progress: Math.ceil(Math.random() * 100),
         };
         tableListDataSource.unshift(newRule);

+ 99 - 97
mock/notices.ts

@@ -1,103 +1,105 @@
 import { Request, Response } from 'express';
 
 const getNotices = (req: Request, res: Response) => {
-  res.json([
-    {
-      id: '000000001',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
-      title: '你收到了 14 份新周报',
-      datetime: '2017-08-09',
-      type: 'notification',
-    },
-    {
-      id: '000000002',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
-      title: '你推荐的 曲妮妮 已通过第三轮面试',
-      datetime: '2017-08-08',
-      type: 'notification',
-    },
-    {
-      id: '000000003',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
-      title: '这种模板可以区分多种通知类型',
-      datetime: '2017-08-07',
-      read: true,
-      type: 'notification',
-    },
-    {
-      id: '000000004',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
-      title: '左侧图标用于区分不同的类型',
-      datetime: '2017-08-07',
-      type: 'notification',
-    },
-    {
-      id: '000000005',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
-      title: '内容不要超过两行字,超出时自动截断',
-      datetime: '2017-08-07',
-      type: 'notification',
-    },
-    {
-      id: '000000006',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '曲丽丽 评论了你',
-      description: '描述信息描述信息描述信息',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000007',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '朱偏右 回复了你',
-      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000008',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '标题',
-      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000009',
-      title: '任务名称',
-      description: '任务需要在 2017-01-12 20:00 前启动',
-      extra: '未开始',
-      status: 'todo',
-      type: 'event',
-    },
-    {
-      id: '000000010',
-      title: '第三方紧急代码变更',
-      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
-      extra: '马上到期',
-      status: 'urgent',
-      type: 'event',
-    },
-    {
-      id: '000000011',
-      title: '信息安全考试',
-      description: '指派竹尔于 2017-01-09 前完成更新并发布',
-      extra: '已耗时 8 天',
-      status: 'doing',
-      type: 'event',
-    },
-    {
-      id: '000000012',
-      title: 'ABCD 版本发布',
-      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
-      extra: '进行中',
-      status: 'processing',
-      type: 'event',
-    },
-  ]);
+  res.json({
+    data: [
+      {
+        id: '000000001',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+        title: '你收到了 14 份新周报',
+        datetime: '2017-08-09',
+        type: 'notification',
+      },
+      {
+        id: '000000002',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+        title: '你推荐的 曲妮妮 已通过第三轮面试',
+        datetime: '2017-08-08',
+        type: 'notification',
+      },
+      {
+        id: '000000003',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+        title: '这种模板可以区分多种通知类型',
+        datetime: '2017-08-07',
+        read: true,
+        type: 'notification',
+      },
+      {
+        id: '000000004',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+        title: '左侧图标用于区分不同的类型',
+        datetime: '2017-08-07',
+        type: 'notification',
+      },
+      {
+        id: '000000005',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+        title: '内容不要超过两行字,超出时自动截断',
+        datetime: '2017-08-07',
+        type: 'notification',
+      },
+      {
+        id: '000000006',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '曲丽丽 评论了你',
+        description: '描述信息描述信息描述信息',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000007',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '朱偏右 回复了你',
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000008',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '标题',
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000009',
+        title: '任务名称',
+        description: '任务需要在 2017-01-12 20:00 前启动',
+        extra: '未开始',
+        status: 'todo',
+        type: 'event',
+      },
+      {
+        id: '000000010',
+        title: '第三方紧急代码变更',
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+        extra: '马上到期',
+        status: 'urgent',
+        type: 'event',
+      },
+      {
+        id: '000000011',
+        title: '信息安全考试',
+        description: '指派竹尔于 2017-01-09 前完成更新并发布',
+        extra: '已耗时 8 天',
+        status: 'doing',
+        type: 'event',
+      },
+      {
+        id: '000000012',
+        title: 'ABCD 版本发布',
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+        extra: '进行中',
+        status: 'processing',
+        type: 'event',
+      },
+    ],
+  });
 };
 
 export default {

+ 87 - 52
mock/user.ts

@@ -13,58 +13,85 @@ async function getFakeCaptcha(req: Request, res: Response) {
   return res.json('captcha-xxx');
 }
 
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
+
+/**
+ * 当前用户的权限,如果为空代表没登录
+ * current user access, if is '', user need login
+ * 如果是 pro 的预览,默认是有权限的
+ */
+let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
+
+const getAccess = () => {
+  return access;
+};
+
 // 代码中会兼容本地 service mock 以及部署站点的静态数据
 export default {
   // 支持值为 Object 和 Array
-  'GET /api/currentUser': {
-    name: 'Serati Ma',
-    avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
-    userid: '00000001',
-    email: 'antdesign@alipay.com',
-    signature: '海纳百川,有容乃大',
-    title: '交互专家',
-    group: '蚂蚁集团-某某某事业群-某某平台部-某某技术部-UED',
-    tags: [
-      {
-        key: '0',
-        label: '很有想法的',
-      },
-      {
-        key: '1',
-        label: '专注设计',
-      },
-      {
-        key: '2',
-        label: '辣~',
-      },
-      {
-        key: '3',
-        label: '大长腿',
-      },
-      {
-        key: '4',
-        label: '川妹子',
-      },
-      {
-        key: '5',
-        label: '海纳百川',
-      },
-    ],
-    notifyCount: 12,
-    unreadCount: 11,
-    country: 'China',
-    geographic: {
-      province: {
-        label: '浙江省',
-        key: '330000',
-      },
-      city: {
-        label: '杭州市',
-        key: '330100',
+  'GET /api/currentUser': (req: Request, res: Response) => {
+    if (!getAccess()) {
+      res.status(401).send({
+        data: {
+          isLogin: false,
+        },
+        errorCode: '401',
+        errorMessage: '请先登录!',
+        success: true,
+      });
+      return;
+    }
+    res.send({
+      name: 'Serati Ma',
+      avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+      userid: '00000001',
+      email: 'antdesign@alipay.com',
+      signature: '海纳百川,有容乃大',
+      title: '交互专家',
+      group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+      tags: [
+        {
+          key: '0',
+          label: '很有想法的',
+        },
+        {
+          key: '1',
+          label: '专注设计',
+        },
+        {
+          key: '2',
+          label: '辣~',
+        },
+        {
+          key: '3',
+          label: '大长腿',
+        },
+        {
+          key: '4',
+          label: '川妹子',
+        },
+        {
+          key: '5',
+          label: '海纳百川',
+        },
+      ],
+      notifyCount: 12,
+      unreadCount: 11,
+      country: 'China',
+      access: getAccess(),
+      geographic: {
+        province: {
+          label: '浙江省',
+          key: '330000',
+        },
+        city: {
+          label: '杭州市',
+          key: '330100',
+        },
       },
-    },
-    address: '西湖区工专路 77 号',
-    phone: '0752-268888888',
+      address: '西湖区工专路 77 号',
+      phone: '0752-268888888',
+    });
   },
   // GET POST 可省略
   'GET /api/users': [
@@ -88,22 +115,24 @@ export default {
     },
   ],
   'POST /api/login/account': async (req: Request, res: Response) => {
-    const { password, userName, type } = req.body;
+    const { password, username, type } = req.body;
     await waitTime(2000);
-    if (password === 'ant.design' && userName === 'admin') {
+    if (password === 'ant.design' && username === 'admin') {
       res.send({
         status: 'ok',
         type,
         currentAuthority: 'admin',
       });
+      access = 'admin';
       return;
     }
-    if (password === 'ant.design' && userName === 'user') {
+    if (password === 'ant.design' && username === 'user') {
       res.send({
         status: 'ok',
         type,
         currentAuthority: 'user',
       });
+      access = 'user';
       return;
     }
     if (type === 'mobile') {
@@ -112,6 +141,7 @@ export default {
         type,
         currentAuthority: 'admin',
       });
+      access = 'admin';
       return;
     }
 
@@ -120,9 +150,14 @@ export default {
       type,
       currentAuthority: 'guest',
     });
+    access = 'guest';
+  },
+  'POST /api/login/outLogin': (req: Request, res: Response) => {
+    access = '';
+    res.send({ data: {}, success: true });
   },
   'POST /api/register': (req: Request, res: Response) => {
-    res.send({ status: 'ok', currentAuthority: 'user' });
+    res.send({ status: 'ok', currentAuthority: 'user', success: true });
   },
   'GET /api/500': (req: Request, res: Response) => {
     res.status(500).send({

+ 26 - 26
package.json

@@ -1,6 +1,6 @@
 {
   "name": "ant-design-pro",
-  "version": "4.5.0",
+  "version": "5.0.0-beta.3",
   "private": true,
   "description": "An out-of-box UI solution for enterprise applications",
   "scripts": {
@@ -15,16 +15,17 @@
     "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
     "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
     "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
-    "fetch:blocks": "pro fetch-blocks && npm run prettier",
     "gh-pages": "gh-pages -d dist",
     "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
     "postinstall": "umi g tmp",
     "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
+    "lint-staged": "lint-staged",
     "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
     "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
     "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
-    "lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
+    "lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
     "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+    "openapi": "umi openapi",
     "precommit": "lint-staged",
     "prettier": "prettier -c --write \"src/**/*\"",
     "site": "npm run fetch:blocks && npm run build",
@@ -38,6 +39,7 @@
     "test": "umi test",
     "test:all": "node ./tests/run-tests.js",
     "test:component": "umi test ./src/components",
+    "serve": "umi-serve",
     "tsc": "tsc --noEmit"
   },
   "lint-staged": {
@@ -53,26 +55,26 @@
     "not ie <= 10"
   ],
   "dependencies": {
-    "@ant-design/icons": "^4.0.0",
-    "@ant-design/pro-descriptions": "^1.2.0",
-    "@ant-design/pro-form": "^1.3.0",
-    "@ant-design/pro-layout": "^6.9.0",
-    "@ant-design/pro-table": "^2.17.0",
-    "@umijs/route-utils": "^1.0.33",
-    "antd": "^4.15.0",
+    "@ant-design/icons": "^4.5.0",
+    "@ant-design/pro-descriptions": "^1.6.8",
+    "@ant-design/pro-form": "^1.18.3",
+    "@ant-design/pro-layout": "^6.15.3",
+    "@ant-design/pro-table": "^2.30.8",
+    "@umijs/route-utils": "^1.0.36",
+    "antd": "^4.14.0",
     "classnames": "^2.2.6",
     "lodash": "^4.17.11",
     "moment": "^2.25.3",
     "omit.js": "^2.0.2",
-    "react": "^16.14.0",
+    "react": "^17.0.0",
     "react-dev-inspector": "^1.1.1",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",
-    "umi": "^3.4.1",
-    "umi-request": "^1.0.8"
+    "umi": "^3.5.0",
+    "umi-serve": "^1.9.10"
   },
   "devDependencies": {
-    "@ant-design/pro-cli": "^1.0.28",
+    "@ant-design/pro-cli": "^2.0.2",
     "@types/classnames": "^2.2.7",
     "@types/express": "^4.17.0",
     "@types/history": "^4.7.2",
@@ -81,14 +83,16 @@
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@types/react-helmet": "^6.1.0",
-    "@umijs/fabric": "^2.5.1",
+    "@umijs/fabric": "^2.6.2",
+    "@umijs/openapi": "^1.1.14",
     "@umijs/plugin-blocks": "^2.0.5",
     "@umijs/plugin-esbuild": "^1.0.1",
+    "@umijs/plugin-openapi": "^1.2.0",
     "@umijs/preset-ant-design-pro": "^1.2.0",
-    "@umijs/preset-react": "^1.4.8",
+    "@umijs/preset-dumi": "^1.1.7",
+    "@umijs/preset-react": "^1.7.4",
     "@umijs/yorkie": "^2.0.3",
     "carlo": "^0.9.46",
-    "chalk": "^4.0.0",
     "cross-env": "^7.0.0",
     "cross-port-killer": "^1.1.1",
     "detect-installer": "^1.0.1",
@@ -99,21 +103,14 @@
     "jsdom-global": "^3.0.2",
     "lint-staged": "^10.0.0",
     "mockjs": "^1.0.1-beta3",
-    "prettier": "^2.0.1",
+    "prettier": "^2.3.2",
     "puppeteer-core": "^8.0.0",
     "stylelint": "^13.0.0",
-    "typescript": "^4.0.3"
+    "typescript": "^4.2.2"
   },
   "engines": {
     "node": ">=10.0.0"
   },
-  "checkFiles": [
-    "src/**/*.js*",
-    "src/**/*.ts*",
-    "src/**/*.less",
-    "config/**/*.js*",
-    "scripts/**/*.js"
-  ],
   "create-umi": {
     "ignoreScript": [
       "docker*",
@@ -142,5 +139,8 @@
       "CNAME",
       "create-umi"
     ]
+  },
+  "gitHooks": {
+    "commit-msg": "fabric verify-commit"
   }
 }

BIN
public/home_bg.png


src/assets/logo.svg → public/logo.svg


+ 9 - 0
src/access.ts

@@ -0,0 +1,9 @@
+/**
+ * @see https://umijs.org/zh-CN/plugins/plugin-access
+ * */
+export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) {
+  const { currentUser } = initialState || {};
+  return {
+    canAdmin: currentUser && currentUser.access === 'admin',
+  };
+}

+ 136 - 0
src/app.tsx

@@ -0,0 +1,136 @@
+import type { Settings as LayoutSettings } from '@ant-design/pro-layout';
+import { PageLoading } from '@ant-design/pro-layout';
+import { notification } from 'antd';
+import type { RequestConfig, RunTimeLayoutConfig } from 'umi';
+import { history, Link } from 'umi';
+import RightContent from '@/components/RightContent';
+import Footer from '@/components/Footer';
+import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
+import { BookOutlined, LinkOutlined } from '@ant-design/icons';
+
+const isDev = process.env.NODE_ENV === 'development';
+const loginPath = '/user/login';
+
+/** 获取用户信息比较慢的时候会展示一个 loading */
+export const initialStateConfig = {
+  loading: <PageLoading />,
+};
+
+/**
+ * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
+ * */
+export async function getInitialState(): Promise<{
+  settings?: Partial<LayoutSettings>;
+  currentUser?: API.CurrentUser;
+  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
+}> {
+  const fetchUserInfo = async () => {
+    try {
+      const currentUser = await queryCurrentUser();
+      return currentUser;
+    } catch (error) {
+      history.push(loginPath);
+    }
+    return undefined;
+  };
+  // 如果是登录页面,不执行
+  if (history.location.pathname !== loginPath) {
+    const currentUser = await fetchUserInfo();
+    return {
+      fetchUserInfo,
+      currentUser,
+      settings: {},
+    };
+  }
+  return {
+    fetchUserInfo,
+    settings: {},
+  };
+}
+
+/**
+ * 异常处理程序
+    200: '服务器成功返回请求的数据。',
+    201: '新建或修改数据成功。',
+    202: '一个请求已经进入后台排队(异步任务)。',
+    204: '删除数据成功。',
+    400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+    401: '用户没有权限(令牌、用户名、密码错误)。',
+    403: '用户得到授权,但是访问是被禁止的。',
+    404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+    405: '请求方法不被允许。',
+    406: '请求的格式不可得。',
+    410: '请求的资源被永久删除,且不会再得到的。',
+    422: '当创建一个对象时,发生一个验证错误。',
+    500: '服务器发生错误,请检查服务器。',
+    502: '网关错误。',
+    503: '服务不可用,服务器暂时过载或维护。',
+    504: '网关超时。',
+ //-----English
+    200: The server successfully returned the requested data. ',
+    201: New or modified data is successful. ',
+    202: A request has entered the background queue (asynchronous task). ',
+    204: Data deleted successfully. ',
+    400: 'There was an error in the request sent, and the server did not create or modify data. ',
+    401: The user does not have permission (token, username, password error). ',
+    403: The user is authorized, but access is forbidden. ',
+    404: The request sent was for a record that did not exist. ',
+    405: The request method is not allowed. ',
+    406: The requested format is not available. ',
+    410':
+        'The requested resource is permanently deleted and will no longer be available. ',
+    422: When creating an object, a validation error occurred. ',
+    500: An error occurred on the server, please check the server. ',
+    502: Gateway error. ',
+    503: The service is unavailable. ',
+    504: The gateway timed out. ',
+ * @see https://beta-pro.ant.design/docs/request-cn
+ */
+export const request: RequestConfig = {
+  errorHandler: (error: any) => {
+    const { response } = error;
+
+    if (!response) {
+      notification.error({
+        description: '您的网络发生异常,无法连接服务器',
+        message: '网络异常',
+      });
+    }
+    throw error;
+  },
+};
+
+// ProLayout 支持的api https://procomponents.ant.design/components/layout
+export const layout: RunTimeLayoutConfig = ({ initialState }) => {
+  return {
+    rightContentRender: () => <RightContent />,
+    disableContentMargin: false,
+    waterMarkProps: {
+      content: initialState?.currentUser?.name,
+    },
+    footerRender: () => <Footer />,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      if (!initialState?.currentUser && location.pathname !== loginPath) {
+        history.push(loginPath);
+      }
+    },
+    links: isDev
+      ? [
+          <Link to="/umi/plugin/openapi" target="_blank">
+            <LinkOutlined />
+            <span>OpenAPI 文档</span>
+          </Link>,
+          <Link to="/~docs">
+            <BookOutlined />
+            <span>业务组件文档</span>
+          </Link>,
+        ]
+      : [],
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    ...initialState?.settings,
+  };
+};

+ 0 - 35
src/components/Authorized/Authorized.tsx

@@ -1,35 +0,0 @@
-import React from 'react';
-import { Result } from 'antd';
-import check from './CheckPermissions';
-import type { IAuthorityType } from './CheckPermissions';
-import type AuthorizedRoute from './AuthorizedRoute';
-import type Secured from './Secured';
-
-type AuthorizedProps = {
-  authority: IAuthorityType;
-  noMatch?: React.ReactNode;
-};
-
-type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
-  Secured: typeof Secured;
-  check: typeof check;
-  AuthorizedRoute: typeof AuthorizedRoute;
-};
-
-const Authorized: React.FunctionComponent<AuthorizedProps> = ({
-  children,
-  authority,
-  noMatch = (
-    <Result
-      status="403"
-      title="403"
-      subTitle="Sorry, you are not authorized to access this page."
-    />
-  ),
-}) => {
-  const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
-  const dom = check(authority, childrenRender, noMatch);
-  return <>{dom}</>;
-};
-
-export default Authorized as IAuthorizedType;

+ 0 - 33
src/components/Authorized/AuthorizedRoute.tsx

@@ -1,33 +0,0 @@
-import { Redirect, Route } from 'umi';
-
-import React from 'react';
-import Authorized from './Authorized';
-import type { IAuthorityType } from './CheckPermissions';
-
-type AuthorizedRouteProps = {
-  currentAuthority: string;
-  component: React.ComponentClass<any, any>;
-  render: (props: any) => React.ReactNode;
-  redirectPath: string;
-  authority: IAuthorityType;
-};
-
-const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({
-  component: Component,
-  render,
-  authority,
-  redirectPath,
-  ...rest
-}) => (
-  <Authorized
-    authority={authority}
-    noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
-  >
-    <Route
-      {...rest}
-      render={(props: any) => (Component ? <Component {...props} /> : render(props))}
-    />
-  </Authorized>
-);
-
-export default AuthorizedRoute;

+ 0 - 88
src/components/Authorized/CheckPermissions.tsx

@@ -1,88 +0,0 @@
-import React from 'react';
-import { CURRENT } from './renderAuthorize';
-// eslint-disable-next-line import/no-cycle
-import PromiseRender from './PromiseRender';
-
-export type IAuthorityType =
-  | undefined
-  | string
-  | string[]
-  | Promise<boolean>
-  | ((currentAuthority: string | string[]) => IAuthorityType);
-
-/**
- * @en-US
- * General permission check method
- * Common check permissions method
- * @param {Permission judgment} authority
- * @param {Your permission | Your permission description} currentAuthority
- * @param {Passing components} target
- * @param {no pass components | no pass components} Exception
- * -------------------------------------------------------
- * @zh-CN
- * 通用权限检查方法 Common check permissions method
- *
- * @param { 权限判定 | Permission judgment } authority
- * @param { 你的权限 | Your permission description } currentAuthority
- * @param { 通过的组件 | Passing components } target
- * @param { 未通过的组件 | no pass components } Exception
- */
-const checkPermissions = <T, K>(
-  authority: IAuthorityType,
-  currentAuthority: string | string[],
-  target: T,
-  Exception: K,
-): T | K | React.ReactNode => {
-  // No judgment permission. View all by default
-  // Retirement authority, return target;
-  if (!authority) {
-    return target;
-  }
-  // Array processing
-  if (Array.isArray(authority)) {
-    if (Array.isArray(currentAuthority)) {
-      if (currentAuthority.some((item) => authority.includes(item))) {
-        return target;
-      }
-    } else if (authority.includes(currentAuthority)) {
-      return target;
-    }
-    return Exception;
-  }
-  // Deal with string
-  if (typeof authority === 'string') {
-    if (Array.isArray(currentAuthority)) {
-      if (currentAuthority.some((item) => authority === item)) {
-        return target;
-      }
-    } else if (authority === currentAuthority) {
-      return target;
-    }
-    return Exception;
-  }
-  // Deal with promise
-  if (authority instanceof Promise) {
-    return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
-  }
-  // Deal with function
-  if (typeof authority === 'function') {
-    const bool = authority(currentAuthority);
-    // The return value after the function is executed is Promise
-    if (bool instanceof Promise) {
-      return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
-    }
-    if (bool) {
-      return target;
-    }
-    return Exception;
-  }
-  throw new Error('unsupported parameters');
-};
-
-export { checkPermissions };
-
-function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
-  return checkPermissions<T, K>(authority, CURRENT, target, Exception);
-}
-
-export default check;

+ 0 - 96
src/components/Authorized/PromiseRender.tsx

@@ -1,96 +0,0 @@
-import React from 'react';
-import { Spin } from 'antd';
-import isEqual from 'lodash/isEqual';
-import { isComponentClass } from './Secured';
-// eslint-disable-next-line import/no-cycle
-
-type PromiseRenderProps<T, K> = {
-  ok: T;
-  error: K;
-  promise: Promise<boolean>;
-};
-
-type PromiseRenderState = {
-  component: React.ComponentClass | React.FunctionComponent;
-};
-
-export default class PromiseRender<T, K> extends React.Component<
-  PromiseRenderProps<T, K>,
-  PromiseRenderState
-> {
-  state: PromiseRenderState = {
-    component: () => null,
-  };
-
-  componentDidMount(): void {
-    this.setRenderComponent(this.props);
-  }
-
-  shouldComponentUpdate = (
-    nextProps: PromiseRenderProps<T, K>,
-    nextState: PromiseRenderState,
-  ): boolean => {
-    const { component } = this.state;
-    if (!isEqual(nextProps, this.props)) {
-      this.setRenderComponent(nextProps);
-    }
-    if (nextState.component !== component) return true;
-    return false;
-  };
-
-  // set render Component : ok or error
-  setRenderComponent(props: PromiseRenderProps<T, K>): void {
-    const ok = this.checkIsInstantiation(props.ok);
-    const error = this.checkIsInstantiation(props.error);
-    props.promise
-      .then(() => {
-        this.setState({
-          component: ok,
-        });
-        return true;
-      })
-      .catch(() => {
-        this.setState({
-          component: error,
-        });
-      });
-  }
-
-  // Determine whether the incoming component has been instantiated
-  // AuthorizedRoute is already instantiated
-  // Authorized  render is already instantiated, children is no instantiated
-  // Secured is not instantiated
-  checkIsInstantiation = (
-    target: React.ReactNode | React.ComponentClass,
-  ): React.FunctionComponent => {
-    if (isComponentClass(target)) {
-      const Target = target as React.ComponentClass;
-      return (props: any) => <Target {...props} />;
-    }
-    if (React.isValidElement(target)) {
-      return (props: any) => React.cloneElement(target, props);
-    }
-    return () => target as React.ReactNode & null;
-  };
-
-  render() {
-    const { component: Component } = this.state;
-    const { ok, error, promise, ...rest } = this.props;
-
-    return Component ? (
-      <Component {...rest} />
-    ) : (
-      <div
-        style={{
-          width: '100%',
-          height: '100%',
-          margin: 'auto',
-          paddingTop: 50,
-          textAlign: 'center',
-        }}
-      >
-        <Spin size="large" />
-      </div>
-    );
-  }
-}

+ 0 - 80
src/components/Authorized/Secured.tsx

@@ -1,80 +0,0 @@
-import React from 'react';
-import CheckPermissions from './CheckPermissions';
-
-/**
- * @en-US No pages can be accessed by default,default is "NULL"
- * @zh-CN 默认不能访问任何页面 default is "NULL"
- *  */
-const Exception403 = () => 403;
-
-export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
-  if (!component) return false;
-  const proto = Object.getPrototypeOf(component);
-  if (proto === React.Component || proto === Function.prototype) return true;
-  return isComponentClass(proto);
-};
-
-// Determine whether the incoming component has been instantiated
-// AuthorizedRoute is already instantiated
-// Authorized  render is already instantiated, children is no instantiated
-// Secured is not instantiated
-const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
-  if (isComponentClass(target)) {
-    const Target = target as React.ComponentClass;
-    return (props: any) => <Target {...props} />;
-  }
-  if (React.isValidElement(target)) {
-    return (props: any) => React.cloneElement(target, props);
-  }
-  return () => target;
-};
-
-/**
- * @en-US
- * Used to determine whether you have permission to access this view permission
- * authority supports incoming string, () => boolean | Promise
- * e.g.'user' Only user user can access
- * e.g.'user,admin' user and admin can access
- * e.g. ()=>boolean return true to access, return false to not access
- * e.g. Promise then can be accessed, catch can not be accessed
- * e.g. authority support incoming string, () => boolean | Promise
- * e.g.'user' only user user can access
- * e.g.'user, admin' user and admin can access
- * e.g. () => boolean true to be able to visit, return false can not be accessed
- * e.g. Promise then can not access the visit to catch
- *-------------------------------------------------------------
- * @zh-CN
- * 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问
- * e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问
- * catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user
- * user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able
- * to visit, return false can not be accessed e.g. Promise then can not access the visit to catch
- *
- * @param {string | function | Promise} authority
- * @param {ReactNode} error non-required parameter
- */
-const authorize = (authority: string, error?: React.ReactNode) => {
-  /**
-   * @en-US
-   * conversion into a class
-   * Prevent the staticContext from being found to cause an error when the string is passed in
-   * String parameters can cause staticContext not found error
-   *-------------------------------------------------------------
-   * @zh-CN
-   * Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext
-   * not found error
-   */
-  let classError: boolean | React.FunctionComponent = false;
-  if (error) {
-    classError = (() => error) as React.FunctionComponent;
-  }
-  if (!authority) {
-    throw new Error('authority is required');
-  }
-  return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
-    const component = CheckPermissions(authority, target, classError || Exception403);
-    return checkIsInstantiation(component);
-  };
-};
-
-export default authorize;

+ 0 - 11
src/components/Authorized/index.tsx

@@ -1,11 +0,0 @@
-import Authorized from './Authorized';
-import Secured from './Secured';
-import check from './CheckPermissions';
-import renderAuthorize from './renderAuthorize';
-
-Authorized.Secured = Secured;
-Authorized.check = check;
-
-const RenderAuthorize = renderAuthorize(Authorized);
-
-export default RenderAuthorize;

+ 0 - 31
src/components/Authorized/renderAuthorize.ts

@@ -1,31 +0,0 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let CURRENT: string | string[] = 'NULL';
-
-type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
-/**
- * Use authority or getAuthority
- *
- * @param {string|()=>String} currentAuthority
- */
-const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
-  currentAuthority: CurrentAuthorityType,
-): T => {
-  if (currentAuthority) {
-    if (typeof currentAuthority === 'function') {
-      CURRENT = currentAuthority();
-    }
-    if (
-      Object.prototype.toString.call(currentAuthority) === '[object String]' ||
-      Array.isArray(currentAuthority)
-    ) {
-      CURRENT = currentAuthority as string[];
-    }
-  } else {
-    CURRENT = 'NULL';
-  }
-  return Authorized;
-};
-
-export { CURRENT };
-export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);

+ 37 - 0
src/components/Footer/index.tsx

@@ -0,0 +1,37 @@
+import { useIntl } from 'umi';
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-layout';
+
+export default () => {
+  const intl = useIntl();
+  const defaultMessage = intl.formatMessage({
+    id: 'app.copyright.produced',
+    defaultMessage: '蚂蚁集团体验技术部出品',
+  });
+
+  return (
+    <DefaultFooter
+      copyright={`2020 ${defaultMessage}`}
+      links={[
+        {
+          key: 'Ant Design Pro',
+          title: 'Ant Design Pro',
+          href: 'https://pro.ant.design',
+          blankTarget: true,
+        },
+        {
+          key: 'github',
+          title: <GithubOutlined />,
+          href: 'https://github.com/ant-design/ant-design-pro',
+          blankTarget: true,
+        },
+        {
+          key: 'Ant Design',
+          title: 'Ant Design',
+          href: 'https://ant.design',
+          blankTarget: true,
+        },
+      ]}
+    />
+  );
+};

+ 0 - 88
src/components/GlobalHeader/AvatarDropdown.tsx

@@ -1,88 +0,0 @@
-import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
-import { Avatar, Menu, Spin } from 'antd';
-import React from 'react';
-import type { ConnectProps } from 'umi';
-import { history, connect } from 'umi';
-import type { ConnectState } from '@/models/connect';
-import type { CurrentUser } from '@/models/user';
-import HeaderDropdown from '../HeaderDropdown';
-import styles from './index.less';
-
-export type GlobalHeaderRightProps = {
-  currentUser?: CurrentUser;
-  menu?: boolean;
-} & Partial<ConnectProps>;
-
-class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
-  onMenuClick = (event: { key: React.Key; keyPath: React.Key[]; item: React.ReactInstance }) => {
-    const { key } = event;
-
-    if (key === 'logout') {
-      const { dispatch } = this.props;
-
-      if (dispatch) {
-        dispatch({
-          type: 'login/logout',
-        });
-      }
-
-      return;
-    }
-
-    history.push(`/account/${key}`);
-  };
-
-  render(): React.ReactNode {
-    const {
-      currentUser = {
-        avatar: '',
-        name: '',
-      },
-      menu,
-    } = this.props;
-    const menuHeaderDropdown = (
-      <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
-        {menu && (
-          <Menu.Item key="center">
-            <UserOutlined />
-            个人中心
-          </Menu.Item>
-        )}
-        {menu && (
-          <Menu.Item key="settings">
-            <SettingOutlined />
-            个人设置
-          </Menu.Item>
-        )}
-        {menu && <Menu.Divider />}
-
-        <Menu.Item key="logout">
-          <LogoutOutlined />
-          退出登录
-        </Menu.Item>
-      </Menu>
-    );
-    return currentUser && currentUser.name ? (
-      <HeaderDropdown overlay={menuHeaderDropdown}>
-        <span className={`${styles.action} ${styles.account}`}>
-          <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
-          <span className={`${styles.name} anticon`}>{currentUser.name}</span>
-        </span>
-      </HeaderDropdown>
-    ) : (
-      <span className={`${styles.action} ${styles.account}`}>
-        <Spin
-          size="small"
-          style={{
-            marginLeft: 8,
-            marginRight: 8,
-          }}
-        />
-      </span>
-    );
-  }
-}
-
-export default connect(({ user }: ConnectState) => ({
-  currentUser: user.currentUser,
-}))(AvatarDropdown);

+ 0 - 168
src/components/GlobalHeader/NoticeIconView.tsx

@@ -1,168 +0,0 @@
-import { Component } from 'react';
-import type { ConnectProps } from 'umi';
-import { connect } from 'umi';
-import { Tag, message } from 'antd';
-import groupBy from 'lodash/groupBy';
-import moment from 'moment';
-import type { NoticeItem } from '@/models/global';
-import type { CurrentUser } from '@/models/user';
-import type { ConnectState } from '@/models/connect';
-import NoticeIcon from '../NoticeIcon';
-import styles from './index.less';
-
-export type GlobalHeaderRightProps = {
-  notices?: NoticeItem[];
-  currentUser?: CurrentUser;
-  fetchingNotices?: boolean;
-  onNoticeVisibleChange?: (visible: boolean) => void;
-  onNoticeClear?: (tabName?: string) => void;
-} & Partial<ConnectProps>;
-
-class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
-  componentDidMount() {
-    const { dispatch } = this.props;
-
-    if (dispatch) {
-      dispatch({
-        type: 'global/fetchNotices',
-      });
-    }
-  }
-
-  changeReadState = (clickedItem: NoticeItem): void => {
-    const { id } = clickedItem;
-    const { dispatch } = this.props;
-
-    if (dispatch) {
-      dispatch({
-        type: 'global/changeNoticeReadState',
-        payload: id,
-      });
-    }
-  };
-
-  handleNoticeClear = (title: string, key: string) => {
-    const { dispatch } = this.props;
-    message.success(`${'Emptied'} ${title}`);
-
-    if (dispatch) {
-      dispatch({
-        type: 'global/clearNotices',
-        payload: key,
-      });
-    }
-  };
-
-  getNoticeData = (): Record<string, NoticeItem[]> => {
-    const { notices = [] } = this.props;
-
-    if (!notices || notices.length === 0 || !Array.isArray(notices)) {
-      return {};
-    }
-
-    const newNotices = notices.map((notice) => {
-      const newNotice = { ...notice };
-
-      if (newNotice.datetime) {
-        newNotice.datetime = moment(notice.datetime as string).fromNow();
-      }
-
-      if (newNotice.id) {
-        newNotice.key = newNotice.id;
-      }
-
-      if (newNotice.extra && newNotice.status) {
-        const color = {
-          todo: '',
-          processing: 'blue',
-          urgent: 'red',
-          doing: 'gold',
-        }[newNotice.status];
-        newNotice.extra = (
-          <Tag
-            color={color}
-            style={{
-              marginRight: 0,
-            }}
-          >
-            {newNotice.extra}
-          </Tag>
-        );
-      }
-
-      return newNotice;
-    });
-    return groupBy(newNotices, 'type');
-  };
-
-  getUnreadData = (noticeData: Record<string, NoticeItem[]>) => {
-    const unreadMsg: Record<string, number> = {};
-    Object.keys(noticeData).forEach((key) => {
-      const value = noticeData[key];
-
-      if (!unreadMsg[key]) {
-        unreadMsg[key] = 0;
-      }
-
-      if (Array.isArray(value)) {
-        unreadMsg[key] = value.filter((item) => !item.read).length;
-      }
-    });
-    return unreadMsg;
-  };
-
-  render() {
-    const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
-    const noticeData = this.getNoticeData();
-    const unreadMsg = this.getUnreadData(noticeData);
-    return (
-      <NoticeIcon
-        className={styles.action}
-        count={currentUser && currentUser.unreadCount}
-        onItemClick={(item) => {
-          this.changeReadState(item as NoticeItem);
-        }}
-        loading={fetchingNotices}
-        clearText="Empty"
-        viewMoreText="See more"
-        onClear={this.handleNoticeClear}
-        onPopupVisibleChange={onNoticeVisibleChange}
-        onViewMore={() => message.info('Click on view more')}
-        clearClose
-      >
-        <NoticeIcon.Tab
-          tabKey="notification"
-          count={unreadMsg.notification}
-          list={noticeData.notification}
-          title="Notification"
-          emptyText="You have viewed all notifications"
-          showViewMore
-        />
-        <NoticeIcon.Tab
-          tabKey="message"
-          count={unreadMsg.message}
-          list={noticeData.message}
-          title="Message"
-          emptyText="You have read all messages"
-          showViewMore
-        />
-        <NoticeIcon.Tab
-          tabKey="event"
-          title="To do"
-          emptyText="You have completed all to-dos"
-          count={unreadMsg.event}
-          list={noticeData.event}
-          showViewMore
-        />
-      </NoticeIcon>
-    );
-  }
-}
-
-export default connect(({ user, global, loading }: ConnectState) => ({
-  currentUser: user.currentUser,
-  collapsed: global.collapsed,
-  fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
-  fetchingNotices: loading.effects['global/fetchNotices'],
-  notices: global.notices,
-}))(GlobalHeaderRight);

+ 0 - 83
src/components/GlobalHeader/RightContent.tsx

@@ -1,83 +0,0 @@
-import { Tooltip, Tag } from 'antd';
-import type { Settings as ProSettings } from '@ant-design/pro-layout';
-import { QuestionCircleOutlined } from '@ant-design/icons';
-import React from 'react';
-import type { ConnectProps } from 'umi';
-import { connect, SelectLang } from 'umi';
-import type { ConnectState } from '@/models/connect';
-import Avatar from './AvatarDropdown';
-import HeaderSearch from '../HeaderSearch';
-import styles from './index.less';
-
-export type GlobalHeaderRightProps = {
-  theme?: ProSettings['navTheme'] | 'realDark';
-} & Partial<ConnectProps> &
-  Partial<ProSettings>;
-
-const ENVTagColor = {
-  dev: 'orange',
-  test: 'green',
-  pre: '#87d068',
-};
-
-const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = (props) => {
-  const { theme, layout } = props;
-  let className = styles.right;
-
-  if (theme === 'dark' && layout === 'top') {
-    className = `${styles.right}  ${styles.dark}`;
-  }
-
-  return (
-    <div className={className}>
-      <HeaderSearch
-        className={`${styles.action} ${styles.search}`}
-        placeholder="Site Search"
-        defaultValue="umi ui"
-        options={[
-          { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
-          {
-            label: <a href="next.ant.design">Ant Design</a>,
-            value: 'Ant Design',
-          },
-          {
-            label: <a href="https://protable.ant.design/">Pro Table</a>,
-            value: 'Pro Table',
-          },
-          {
-            label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
-            value: 'Pro Layout',
-          },
-        ]}
-        // onSearch={value => {
-        //   //console.log('input', value);
-        // }}
-      />
-      <Tooltip title="Use documentation">
-        <a
-          style={{
-            color: 'inherit',
-          }}
-          target="_blank"
-          href="https://pro.ant.design/docs/getting-started"
-          rel="noopener noreferrer"
-          className={styles.action}
-        >
-          <QuestionCircleOutlined />
-        </a>
-      </Tooltip>
-      <Avatar />
-      {REACT_APP_ENV && (
-        <span>
-          <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
-        </span>
-      )}
-      <SelectLang className={styles.action} />
-    </div>
-  );
-};
-
-export default connect(({ settings }: ConnectState) => ({
-  theme: settings.navTheme,
-  layout: settings.layout,
-}))(GlobalHeaderRight);

+ 3 - 8
src/components/HeaderSearch/index.less

@@ -1,6 +1,8 @@
 @import '~antd/es/style/themes/default.less';
 
 .headerSearch {
+  display: inline-flex;
+  align-items: center;
   .input {
     width: 0;
     min-width: 0;
@@ -12,16 +14,9 @@
       background: transparent;
     }
     input {
-      padding-right: 0;
-      padding-left: 0;
-      border: 0;
       box-shadow: none !important;
     }
-    &,
-    &:hover,
-    &:focus {
-      border-bottom: 1px solid @border-color-base;
-    }
+
     &.show {
       width: 210px;
       margin-left: 8px;

+ 7 - 11
src/components/HeaderSearch/index.tsx

@@ -14,8 +14,8 @@ export type HeaderSearchProps = {
   className?: string;
   placeholder?: string;
   options: AutoCompleteProps['options'];
-  defaultOpen?: boolean;
-  open?: boolean;
+  defaultVisible?: boolean;
+  visible?: boolean;
   defaultValue?: string;
   value?: string;
 };
@@ -26,8 +26,8 @@ const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
     defaultValue,
     onVisibleChange,
     placeholder,
-    open,
-    defaultOpen,
+    visible,
+    defaultVisible,
     ...restProps
   } = props;
 
@@ -38,15 +38,14 @@ const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
     onChange: props.onChange,
   });
 
-  const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, {
-    value: props.open,
+  const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
+    value: props.visible,
     onChange: onVisibleChange,
   });
 
   const inputClass = classNames(styles.input, {
     [styles.show]: searchMode,
   });
-
   return (
     <div
       className={classNames(className, styles.headerSearch)}
@@ -74,14 +73,11 @@ const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
         key="AutoComplete"
         className={inputClass}
         value={value}
-        style={{
-          height: 28,
-          marginTop: -6,
-        }}
         options={restProps.options}
         onChange={setValue}
       >
         <Input
+          size="small"
           ref={inputRef}
           defaultValue={defaultValue}
           aria-label={placeholder}

+ 126 - 0
src/components/NoticeIcon/NoticeIcon.tsx

@@ -0,0 +1,126 @@
+import { BellOutlined } from '@ant-design/icons';
+import { Badge, Spin, Tabs } from 'antd';
+import useMergedState from 'rc-util/es/hooks/useMergedState';
+import React from 'react';
+import classNames from 'classnames';
+import type { NoticeIconTabProps } from './NoticeList';
+import NoticeList from './NoticeList';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+export type NoticeIconProps = {
+  count?: number;
+  bell?: React.ReactNode;
+  className?: string;
+  loading?: boolean;
+  onClear?: (tabName: string, tabKey: string) => void;
+  onItemClick?: (item: API.NoticeIconItem, tabProps: NoticeIconTabProps) => void;
+  onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
+  onTabChange?: (tabTile: string) => void;
+  style?: React.CSSProperties;
+  onPopupVisibleChange?: (visible: boolean) => void;
+  popupVisible?: boolean;
+  clearText?: string;
+  viewMoreText?: string;
+  clearClose?: boolean;
+  emptyImage?: string;
+  children?: React.ReactElement<NoticeIconTabProps>[];
+};
+
+const NoticeIcon: React.FC<NoticeIconProps> & {
+  Tab: typeof NoticeList;
+} = (props) => {
+  const getNotificationBox = (): React.ReactNode => {
+    const {
+      children,
+      loading,
+      onClear,
+      onTabChange,
+      onItemClick,
+      onViewMore,
+      clearText,
+      viewMoreText,
+    } = props;
+    if (!children) {
+      return null;
+    }
+    const panes: React.ReactNode[] = [];
+    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
+      if (!child) {
+        return;
+      }
+      const { list, title, count, tabKey, showClear, showViewMore } = child.props;
+      const len = list && list.length ? list.length : 0;
+      const msgCount = count || count === 0 ? count : len;
+      const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
+      panes.push(
+        <TabPane tab={tabTitle} key={tabKey}>
+          <NoticeList
+            clearText={clearText}
+            viewMoreText={viewMoreText}
+            list={list}
+            tabKey={tabKey}
+            onClear={(): void => onClear && onClear(title, tabKey)}
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
+            showClear={showClear}
+            showViewMore={showViewMore}
+            title={title}
+          />
+        </TabPane>,
+      );
+    });
+    return (
+      <>
+        <Spin spinning={loading} delay={300}>
+          <Tabs className={styles.tabs} onChange={onTabChange}>
+            {panes}
+          </Tabs>
+        </Spin>
+      </>
+    );
+  };
+
+  const { className, count, bell } = props;
+
+  const [visible, setVisible] = useMergedState<boolean>(false, {
+    value: props.popupVisible,
+    onChange: props.onPopupVisibleChange,
+  });
+  const noticeButtonClass = classNames(className, styles.noticeButton);
+  const notificationBox = getNotificationBox();
+  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
+  const trigger = (
+    <span className={classNames(noticeButtonClass, { opened: visible })}>
+      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
+        {NoticeBellIcon}
+      </Badge>
+    </span>
+  );
+  if (!notificationBox) {
+    return trigger;
+  }
+
+  return (
+    <HeaderDropdown
+      placement="bottomRight"
+      overlay={notificationBox}
+      overlayClassName={styles.popover}
+      trigger={['click']}
+      visible={visible}
+      onVisibleChange={setVisible}
+    >
+      {trigger}
+    </HeaderDropdown>
+  );
+};
+
+NoticeIcon.defaultProps = {
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+};
+
+NoticeIcon.Tab = NoticeList;
+
+export default NoticeIcon;

+ 8 - 11
src/components/NoticeIcon/NoticeList.tsx

@@ -2,28 +2,25 @@ import { Avatar, List } from 'antd';
 
 import React from 'react';
 import classNames from 'classnames';
-import type { NoticeIconData } from './index';
 import styles from './NoticeList.less';
 
 export type NoticeIconTabProps = {
   count?: number;
-  name?: string;
   showClear?: boolean;
   showViewMore?: boolean;
   style?: React.CSSProperties;
   title: string;
-  tabKey: string;
-  data?: NoticeIconData[];
-  onClick?: (item: NoticeIconData) => void;
+  tabKey: API.NoticeIconItemType;
+  onClick?: (item: API.NoticeIconItem) => void;
   onClear?: () => void;
   emptyText?: string;
   clearText?: string;
   viewMoreText?: string;
-  list: NoticeIconData[];
+  list: API.NoticeIconItem[];
   onViewMore?: (e: any) => void;
 };
-const NoticeList: React.SFC<NoticeIconTabProps> = ({
-  data = [],
+const NoticeList: React.FC<NoticeIconTabProps> = ({
+  list = [],
   onClick,
   onClear,
   title,
@@ -34,7 +31,7 @@ const NoticeList: React.SFC<NoticeIconTabProps> = ({
   viewMoreText,
   showViewMore = false,
 }) => {
-  if (!data || data.length === 0) {
+  if (!list || list.length === 0) {
     return (
       <div className={styles.notFound}>
         <img
@@ -47,9 +44,9 @@ const NoticeList: React.SFC<NoticeIconTabProps> = ({
   }
   return (
     <div>
-      <List<NoticeIconData>
+      <List<API.NoticeIconItem>
         className={styles.list}
-        dataSource={data}
+        dataSource={list}
         renderItem={(item, i) => {
           const itemCls = classNames(styles.item, {
             [styles.read]: item.read,

+ 136 - 126
src/components/NoticeIcon/index.tsx

@@ -1,142 +1,152 @@
-import { BellOutlined } from '@ant-design/icons';
-import { Badge, Spin, Tabs } from 'antd';
-import useMergedState from 'rc-util/es/hooks/useMergedState';
-import React from 'react';
-import classNames from 'classnames';
-import type { NoticeIconTabProps } from './NoticeList';
-import NoticeList from './NoticeList';
-
-import HeaderDropdown from '../HeaderDropdown';
+import { useEffect, useState } from 'react';
+import { Tag, message } from 'antd';
+import { groupBy } from 'lodash';
+import moment from 'moment';
+import { useModel } from 'umi';
+import { getNotices } from '@/services/ant-design-pro/api';
+
+import NoticeIcon from './NoticeIcon';
 import styles from './index.less';
 
-const { TabPane } = Tabs;
-
-export type NoticeIconData = {
-  avatar?: string | React.ReactNode;
-  title?: React.ReactNode;
-  description?: React.ReactNode;
-  datetime?: React.ReactNode;
-  extra?: React.ReactNode;
-  style?: React.CSSProperties;
-  key?: string | number;
-  read?: boolean;
+export type GlobalHeaderRightProps = {
+  fetchingNotices?: boolean;
+  onNoticeVisibleChange?: (visible: boolean) => void;
+  onNoticeClear?: (tabName?: string) => void;
 };
 
-export type NoticeIconProps = {
-  count?: number;
-  bell?: React.ReactNode;
-  className?: string;
-  loading?: boolean;
-  onClear?: (tabName: string, tabKey: string) => void;
-  onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void;
-  onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
-  onTabChange?: (tabTile: string) => void;
-  style?: React.CSSProperties;
-  onPopupVisibleChange?: (visible: boolean) => void;
-  popupVisible?: boolean;
-  clearText?: string;
-  viewMoreText?: string;
-  clearClose?: boolean;
-  emptyImage?: string;
-  children: React.ReactElement<NoticeIconTabProps>[];
+const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => {
+  if (!notices || notices.length === 0 || !Array.isArray(notices)) {
+    return {};
+  }
+
+  const newNotices = notices.map((notice) => {
+    const newNotice = { ...notice };
+
+    if (newNotice.datetime) {
+      newNotice.datetime = moment(notice.datetime as string).fromNow();
+    }
+
+    if (newNotice.id) {
+      newNotice.key = newNotice.id;
+    }
+
+    if (newNotice.extra && newNotice.status) {
+      const color = {
+        todo: '',
+        processing: 'blue',
+        urgent: 'red',
+        doing: 'gold',
+      }[newNotice.status];
+      newNotice.extra = (
+        <Tag
+          color={color}
+          style={{
+            marginRight: 0,
+          }}
+        >
+          {newNotice.extra}
+        </Tag>
+      ) as any;
+    }
+
+    return newNotice;
+  });
+  return groupBy(newNotices, 'type');
 };
 
-const NoticeIcon: React.FC<NoticeIconProps> & {
-  Tab: typeof NoticeList;
-} = (props) => {
-  const getNotificationBox = (): React.ReactNode => {
-    const {
-      children,
-      loading,
-      onClear,
-      onTabChange,
-      onItemClick,
-      onViewMore,
-      clearText,
-      viewMoreText,
-    } = props;
-    if (!children) {
-      return null;
+const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => {
+  const unreadMsg: Record<string, number> = {};
+  Object.keys(noticeData).forEach((key) => {
+    const value = noticeData[key];
+
+    if (!unreadMsg[key]) {
+      unreadMsg[key] = 0;
     }
-    const panes: React.ReactNode[] = [];
-    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
-      if (!child) {
-        return;
-      }
-      const { list, title, count, tabKey, showClear, showViewMore } = child.props;
-      const len = list && list.length ? list.length : 0;
-      const msgCount = count || count === 0 ? count : len;
-      const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
-      panes.push(
-        <TabPane tab={tabTitle} key={tabKey}>
-          <NoticeList
-            {...child.props}
-            clearText={clearText}
-            viewMoreText={viewMoreText}
-            data={list}
-            onClear={(): void => {
-              onClear?.(title, tabKey);
-            }}
-            onClick={(item): void => {
-              onItemClick?.(item, child.props);
-            }}
-            onViewMore={(event): void => {
-              onViewMore?.(child.props, event);
-            }}
-            showClear={showClear}
-            showViewMore={showViewMore}
-            title={title}
-          />
-        </TabPane>,
-      );
-    });
-    return (
-      <Spin spinning={loading} delay={300}>
-        <Tabs className={styles.tabs} onChange={onTabChange}>
-          {panes}
-        </Tabs>
-      </Spin>
+
+    if (Array.isArray(value)) {
+      unreadMsg[key] = value.filter((item) => !item.read).length;
+    }
+  });
+  return unreadMsg;
+};
+
+const NoticeIconView = () => {
+  const { initialState } = useModel('@@initialState');
+  const { currentUser } = initialState || {};
+  const [notices, setNotices] = useState<API.NoticeIconItem[]>([]);
+
+  useEffect(() => {
+    getNotices().then(({ data }) => setNotices(data || []));
+  }, []);
+
+  const noticeData = getNoticeData(notices);
+  const unreadMsg = getUnreadData(noticeData || {});
+
+  const changeReadState = (id: string) => {
+    setNotices(
+      notices.map((item) => {
+        const notice = { ...item };
+        if (notice.id === id) {
+          notice.read = true;
+        }
+        return notice;
+      }),
     );
   };
 
-  const { className, count, bell } = props;
-
-  const [visible, setVisible] = useMergedState<boolean>(false, {
-    value: props.popupVisible,
-    onChange: props.onPopupVisibleChange,
-  });
-  const noticeButtonClass = classNames(className, styles.noticeButton);
-  const notificationBox = getNotificationBox();
-  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
-  const trigger = (
-    <span className={classNames(noticeButtonClass, { opened: visible })}>
-      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
-        {NoticeBellIcon}
-      </Badge>
-    </span>
-  );
-  if (!notificationBox) {
-    return trigger;
-  }
+  const clearReadState = (title: string, key: string) => {
+    setNotices(
+      notices.map((item) => {
+        const notice = { ...item };
+        if (notice.type === key) {
+          notice.read = true;
+        }
+        return notice;
+      }),
+    );
+    message.success(`${'清空了'} ${title}`);
+  };
 
   return (
-    <HeaderDropdown
-      placement="bottomRight"
-      overlay={notificationBox}
-      overlayClassName={styles.popover}
-      trigger={['click']}
-      visible={visible}
-      onVisibleChange={setVisible}
+    <NoticeIcon
+      className={styles.action}
+      count={currentUser && currentUser.unreadCount}
+      onItemClick={(item) => {
+        changeReadState(item.id!);
+      }}
+      onClear={(title: string, key: string) => clearReadState(title, key)}
+      loading={false}
+      clearText="清空"
+      viewMoreText="查看更多"
+      onViewMore={() => message.info('Click on view more')}
+      clearClose
     >
-      {trigger}
-    </HeaderDropdown>
+      <NoticeIcon.Tab
+        tabKey="notification"
+        count={unreadMsg.notification}
+        list={noticeData.notification}
+        title="通知"
+        emptyText="你已查看所有通知"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="message"
+        count={unreadMsg.message}
+        list={noticeData.message}
+        title="消息"
+        emptyText="您已读完所有消息"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="event"
+        title="待办"
+        emptyText="你已完成所有待办"
+        count={unreadMsg.event}
+        list={noticeData.event}
+        showViewMore
+      />
+    </NoticeIcon>
   );
 };
 
-NoticeIcon.defaultProps = {
-  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
-};
-
-NoticeIcon.Tab = NoticeList;
-
-export default NoticeIcon;
+export default NoticeIconView;

+ 0 - 5
src/components/PageLoading/index.tsx

@@ -1,5 +0,0 @@
-import { PageLoading } from '@ant-design/pro-layout';
-
-// loading components from code split
-// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
-export default PageLoading;

+ 103 - 0
src/components/RightContent/AvatarDropdown.tsx

@@ -0,0 +1,103 @@
+import React, { useCallback } from 'react';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
+import { history, useModel } from 'umi';
+import { stringify } from 'querystring';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+import { outLogin } from '@/services/ant-design-pro/api';
+import type { MenuInfo } from 'rc-menu/lib/interface';
+
+export type GlobalHeaderRightProps = {
+  menu?: boolean;
+};
+
+/**
+ * 退出登录,并且将当前的 url 保存
+ */
+const loginOut = async () => {
+  await outLogin();
+  const { query = {}, pathname } = history.location;
+  const { redirect } = query;
+  // Note: There may be security issues, please note
+  if (window.location.pathname !== '/user/login' && !redirect) {
+    history.replace({
+      pathname: '/user/login',
+      search: stringify({
+        redirect: pathname,
+      }),
+    });
+  }
+};
+
+const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const onMenuClick = useCallback(
+    (event: MenuInfo) => {
+      const { key } = event;
+      if (key === 'logout' && initialState) {
+        setInitialState({ ...initialState, currentUser: undefined });
+        loginOut();
+        return;
+      }
+      history.push(`/account/${key}`);
+    },
+    [initialState, setInitialState],
+  );
+
+  const loading = (
+    <span className={`${styles.action} ${styles.account}`}>
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    </span>
+  );
+
+  if (!initialState) {
+    return loading;
+  }
+
+  const { currentUser } = initialState;
+
+  if (!currentUser || !currentUser.name) {
+    return loading;
+  }
+
+  const menuHeaderDropdown = (
+    <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
+      {menu && (
+        <Menu.Item key="center">
+          <UserOutlined />
+          个人中心
+        </Menu.Item>
+      )}
+      {menu && (
+        <Menu.Item key="settings">
+          <SettingOutlined />
+          个人设置
+        </Menu.Item>
+      )}
+      {menu && <Menu.Divider />}
+
+      <Menu.Item key="logout">
+        <LogoutOutlined />
+        退出登录
+      </Menu.Item>
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={menuHeaderDropdown}>
+      <span className={`${styles.action} ${styles.account}`}>
+        <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+        <span className={`${styles.name} anticon`}>{currentUser.name}</span>
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default AvatarDropdown;

+ 4 - 24
src/components/GlobalHeader/index.less

@@ -20,7 +20,7 @@
   .action {
     display: flex;
     align-items: center;
-    height: 100%;
+    height: 48px;
     padding: 0 12px;
     cursor: pointer;
     transition: all 0.3s;
@@ -42,7 +42,6 @@
   }
   .account {
     .avatar {
-      margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
       margin-right: 8px;
       color: @primary-color;
       vertical-align: top;
@@ -53,30 +52,11 @@
 
 .dark {
   .action {
-    color: rgba(255, 255, 255, 0.85);
-    > span {
-      color: rgba(255, 255, 255, 0.85);
+    &:hover {
+      background: #252a3d;
     }
-    &:hover,
     &:global(.opened) {
-      background: @primary-color;
-    }
-  }
-}
-
-:global(.ant-pro-global-header) {
-  .dark {
-    .action {
-      color: @text-color;
-      > span {
-        color: @text-color;
-      }
-      &:hover {
-        color: rgba(255, 255, 255, 0.85);
-        > span {
-          color: rgba(255, 255, 255, 0.85);
-        }
-      }
+      background: #252a3d;
     }
   }
 }

+ 62 - 0
src/components/RightContent/index.tsx

@@ -0,0 +1,62 @@
+import { Space } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import React from 'react';
+import { useModel, SelectLang } from 'umi';
+import Avatar from './AvatarDropdown';
+import HeaderSearch from '../HeaderSearch';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+
+const GlobalHeaderRight: React.FC = () => {
+  const { initialState } = useModel('@@initialState');
+
+  if (!initialState || !initialState.settings) {
+    return null;
+  }
+
+  const { navTheme, layout } = initialState.settings;
+  let className = styles.right;
+
+  if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
+    className = `${styles.right}  ${styles.dark}`;
+  }
+  return (
+    <Space className={className}>
+      <HeaderSearch
+        className={`${styles.action} ${styles.search}`}
+        placeholder="站内搜索"
+        defaultValue="umi ui"
+        options={[
+          { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+          {
+            label: <a href="next.ant.design">Ant Design</a>,
+            value: 'Ant Design',
+          },
+          {
+            label: <a href="https://protable.ant.design/">Pro Table</a>,
+            value: 'Pro Table',
+          },
+          {
+            label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+            value: 'Pro Layout',
+          },
+        ]}
+        // onSearch={value => {
+        //   console.log('input', value);
+        // }}
+      />
+      <span
+        className={styles.action}
+        onClick={() => {
+          window.open('https://pro.ant.design/docs/getting-started');
+        }}
+      >
+        <QuestionCircleOutlined />
+      </span>
+      <Avatar />
+      <SelectLang className={styles.action} />
+    </Space>
+  );
+};
+export default GlobalHeaderRight;

+ 272 - 0
src/components/index.md

@@ -0,0 +1,272 @@
+---
+title: 业务组件
+sidemenu: false
+---
+
+> 此功能由[dumi](https://d.umijs.org/zh-CN/guide/advanced#umi-%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%A8%A1%E5%BC%8F)提供,dumi 是一个 📖 为组件开发场景而生的文档工具,用过的都说好。
+
+# 业务组件
+
+这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。
+
+## Footer 页脚组件
+
+这个组件自带了一些 Pro 的配置,你一般都需要改掉它的信息。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import React from 'react';
+import Footer from '@/components/Footer';
+
+export default () => <Footer />;
+```
+
+## HeaderDropdown 头部下拉列表
+
+HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { Button, Menu } from 'antd';
+import React from 'react';
+import HeaderDropdown from '@/components/HeaderDropdown';
+
+export default () => {
+  const menuHeaderDropdown = (
+    <Menu selectedKeys={[]}>
+      <Menu.Item key="center">个人中心</Menu.Item>
+      <Menu.Item key="settings">个人设置</Menu.Item>
+      <Menu.Divider />
+      <Menu.Item key="logout">退出登录</Menu.Item>
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={menuHeaderDropdown}>
+      <Button>hover 展示菜单</Button>
+    </HeaderDropdown>
+  );
+};
+```
+
+## HeaderSearch 头部搜索框
+
+一个带补全数据的输入框,支持收起和展开 Input
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { Button, Menu } from 'antd';
+import React from 'react';
+import HeaderSearch from '@/components/HeaderSearch';
+
+export default () => {
+  return (
+    <HeaderSearch
+      placeholder="站内搜索"
+      defaultValue="umi ui"
+      options={[
+        { label: 'Ant Design Pro', value: 'Ant Design Pro' },
+        {
+          label: 'Ant Design',
+          value: 'Ant Design',
+        },
+        {
+          label: 'Pro Table',
+          value: 'Pro Table',
+        },
+        {
+          label: 'Pro Layout',
+          value: 'Pro Layout',
+        },
+      ]}
+      onSearch={(value) => {
+        console.log('input', value);
+      }}
+    />
+  );
+};
+```
+
+### API
+
+| 参数            | 说明                               | 类型                         | 默认值 |
+| --------------- | ---------------------------------- | ---------------------------- | ------ |
+| value           | 输入框的值                         | `string`                     | -      |
+| onChange        | 值修改后触发                       | `(value?: string) => void`   | -      |
+| onSearch        | 查询后触发                         | `(value?: string) => void`   | -      |
+| options         | 选项菜单的的列表                   | `{label,value}[]`            | -      |
+| defaultVisible  | 输入框默认是否显示,只有第一次生效 | `boolean`                    | -      |
+| visible         | 输入框是否显示                     | `boolean`                    | -      |
+| onVisibleChange | 输入框显示隐藏的回调函数           | `(visible: boolean) => void` | -      |
+
+## NoticeIcon 通知工具
+
+通知工具提供一个展示多种通知信息的界面。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { message } from 'antd';
+import React from 'react';
+import NoticeIcon from '@/components/NoticeIcon/NoticeIcon';
+
+export default () => {
+  const list = [
+    {
+      id: '000000001',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '你收到了 14 份新周报',
+      datetime: '2017-08-09',
+      type: 'notification',
+    },
+    {
+      id: '000000002',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+      title: '你推荐的 曲妮妮 已通过第三轮面试',
+      datetime: '2017-08-08',
+      type: 'notification',
+    },
+  ];
+  return (
+    <NoticeIcon
+      count={10}
+      onItemClick={(item) => {
+        message.info(`${item.title} 被点击了`);
+      }}
+      onClear={(title: string, key: string) => message.info('点击了清空更多')}
+      loading={false}
+      clearText="清空"
+      viewMoreText="查看更多"
+      onViewMore={() => message.info('点击了查看更多')}
+      clearClose
+    >
+      <NoticeIcon.Tab
+        tabKey="notification"
+        count={2}
+        list={list}
+        title="通知"
+        emptyText="你已查看所有通知"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="message"
+        count={2}
+        list={list}
+        title="消息"
+        emptyText="您已读完所有消息"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="event"
+        title="待办"
+        emptyText="你已完成所有待办"
+        count={2}
+        list={list}
+        showViewMore
+      />
+    </NoticeIcon>
+  );
+};
+```
+
+### NoticeIcon API
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| count | 有多少未读通知 | `number` | - |
+| bell | 铃铛的图表 | `ReactNode` | - |
+| onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - |
+| onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - |
+| onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - |
+| onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - |
+| popupVisible | 通知显示是否展示 | `boolean` | - |
+| onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - |
+| clearText | 清空按钮的文字 | `string` | - |
+| viewMoreText | 查看更多的按钮文字 | `string` | - |
+| clearClose | 展示清空按钮 | `boolean` | - |
+| emptyImage | 列表为空时的兜底展示 | `ReactNode` | - |
+
+### NoticeIcon.Tab API
+
+| 参数         | 说明               | 类型                                 | 默认值 |
+| ------------ | ------------------ | ------------------------------------ | ------ |
+| count        | 有多少未读通知     | `number`                             | -      |
+| title        | 通知 Tab 的标题    | `ReactNode`                          | -      |
+| showClear    | 展示清除按钮       | `boolean`                            | `true` |
+| showViewMore | 展示加载更         | `boolean`                            | `true` |
+| tabKey       | Tab 的唯一 key     | `string`                             | -      |
+| onClick      | 子项的单击事件     | `(item: API.NoticeIconData) => void` | -      |
+| onClear      | 清楚按钮的点击     | `()=>void`                           | -      |
+| emptyText    | 为空的时候测试     | `()=>void`                           | -      |
+| viewMoreText | 查看更多的按钮文字 | `string`                             | -      |
+| onViewMore   | 查看更多的按钮点击 | `( e: MouseEvent) => void`           | -      |
+| list         | 通知信息的列表     | `API.NoticeIconData`                 | -      |
+
+### NoticeIconData
+
+```tsx | pure
+export interface NoticeIconData {
+  id: string;
+  key: string;
+  avatar: string;
+  title: string;
+  datetime: string;
+  type: string;
+  read?: boolean;
+  description: string;
+  clickClose?: boolean;
+  extra: any;
+  status: string;
+}
+```
+
+## RightContent
+
+RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。
+
+```tsx | pure
+<Space>
+  <HeaderSearch
+    placeholder="站内搜索"
+    defaultValue="umi ui"
+    options={[
+      { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+      {
+        label: <a href="next.ant.design">Ant Design</a>,
+        value: 'Ant Design',
+      },
+      {
+        label: <a href="https://protable.ant.design/">Pro Table</a>,
+        value: 'Pro Table',
+      },
+      {
+        label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+        value: 'Pro Layout',
+      },
+    ]}
+  />
+  <Tooltip title="使用文档">
+    <span
+      className={styles.action}
+      onClick={() => {
+        window.location.href = 'https://pro.ant.design/docs/getting-started';
+      }}
+    >
+      <QuestionCircleOutlined />
+    </span>
+  </Tooltip>
+  <Avatar />
+  {REACT_APP_ENV && (
+    <span>
+      <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+    </span>
+  )}
+  <SelectLang className={styles.action} />
+</Space>
+```

+ 0 - 1
src/e2e/__mocks__/antd-pro-merge-less.js

@@ -1 +0,0 @@
-export default undefined;

+ 6 - 2
src/e2e/baseLayout.e2e.js

@@ -1,15 +1,18 @@
 const { uniq } = require('lodash');
 const RouterConfig = require('../../config/config').default.routes;
 
-const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
+const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
 
 function formatter(routes, parentPath = '') {
   const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
   let result = [];
   routes.forEach((item) => {
-    if (item.path) {
+    if (item.path && !item.path.startsWith('/')) {
       result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
     }
+    if (item.path && item.path.startsWith('/')) {
+      result.push(`${item.path}`.replace(/\/{1,}/g, '/'));
+    }
     if (item.routes) {
       result = result.concat(
         formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
@@ -49,6 +52,7 @@ describe('Ant Design Pro E2E test', () => {
     await page.waitForSelector('footer', {
       timeout: 2000,
     });
+
     const haveFooter = await page.evaluate(
       () => document.getElementsByTagName('footer').length > 0,
     );

+ 3 - 0
src/global.less

@@ -13,6 +13,9 @@ body,
 .ant-layout {
   min-height: 100vh;
 }
+.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
+  left: unset;
+}
 
 canvas {
   display: block;

+ 1 - 2
src/global.tsx

@@ -73,8 +73,7 @@ if (pwa) {
   });
 
   // remove all caches
-  // @ts-ignore
-  if (window.caches && window.caches.keys) {
+  if (window.caches && window.caches.keys()) {
     caches.keys().then((keys) => {
       keys.forEach((key) => {
         caches.delete(key);

+ 0 - 183
src/layouts/BasicLayout.tsx

@@ -1,183 +0,0 @@
-/**
- * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
- *
- * @see You can view component api by: https://github.com/ant-design/ant-design-pro-layout
- */
-import type {
-  MenuDataItem,
-  BasicLayoutProps as ProLayoutProps,
-  Settings,
-} from '@ant-design/pro-layout';
-import ProLayout, { DefaultFooter } from '@ant-design/pro-layout';
-import React, { useEffect, useMemo, useRef } from 'react';
-import type { Dispatch } from 'umi';
-import { Link, useIntl, connect, history } from 'umi';
-import { GithubOutlined } from '@ant-design/icons';
-import { Result, Button } from 'antd';
-import Authorized from '@/utils/Authorized';
-import RightContent from '@/components/GlobalHeader/RightContent';
-import type { ConnectState } from '@/models/connect';
-import { getMatchMenu } from '@umijs/route-utils';
-import logo from '../assets/logo.svg';
-
-const noMatch = (
-  <Result
-    status={403}
-    title="403"
-    subTitle="Sorry, you are not authorized to access this page."
-    extra={
-      <Button type="primary">
-        <Link to="/user/login">Go Login</Link>
-      </Button>
-    }
-  />
-);
-export type BasicLayoutProps = {
-  breadcrumbNameMap: Record<string, MenuDataItem>;
-  route: ProLayoutProps['route'] & {
-    authority: string[];
-  };
-  settings: Settings;
-  dispatch: Dispatch;
-} & ProLayoutProps;
-export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
-  breadcrumbNameMap: Record<string, MenuDataItem>;
-};
-/** Use Authorized check all menu item */
-
-const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
-  menuList.map((item) => {
-    const localItem = {
-      ...item,
-      children: item.children ? menuDataRender(item.children) : undefined,
-    };
-    return Authorized.check(item.authority, localItem, null) as MenuDataItem;
-  });
-
-const defaultFooterDom = (
-  <DefaultFooter
-    copyright={`${new Date().getFullYear()} Produced by Ant Group Experience Technology Department`}
-    links={[
-      {
-        key: 'Ant Design Pro',
-        title: 'Ant Design Pro',
-        href: 'https://pro.ant.design',
-        blankTarget: true,
-      },
-      {
-        key: 'github',
-        title: <GithubOutlined />,
-        href: 'https://github.com/ant-design/ant-design-pro',
-        blankTarget: true,
-      },
-      {
-        key: 'Ant Design',
-        title: 'Ant Design',
-        href: 'https://ant.design',
-        blankTarget: true,
-      },
-    ]}
-  />
-);
-
-const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
-  const {
-    dispatch,
-    children,
-    settings,
-    location = {
-      pathname: '/',
-    },
-  } = props;
-
-  const menuDataRef = useRef<MenuDataItem[]>([]);
-
-  useEffect(() => {
-    if (dispatch) {
-      dispatch({
-        type: 'user/fetchCurrent',
-      });
-    }
-  }, []);
-  /** Init variables */
-
-  const handleMenuCollapse = (payload: boolean): void => {
-    if (dispatch) {
-      dispatch({
-        type: 'global/changeLayoutCollapsed',
-        payload,
-      });
-    }
-  };
-  // get children authority
-  const authorized = useMemo(
-    () =>
-      getMatchMenu(location.pathname || '/', menuDataRef.current).pop() || {
-        authority: undefined,
-      },
-    [location.pathname],
-  );
-
-  const { formatMessage } = useIntl();
-
-  return (
-    <ProLayout
-      logo={logo}
-      formatMessage={formatMessage}
-      {...props}
-      {...settings}
-      onCollapse={handleMenuCollapse}
-      onMenuHeaderClick={() => history.push('/')}
-      menuItemRender={(menuItemProps, defaultDom) => {
-        if (
-          menuItemProps.isUrl ||
-          !menuItemProps.path ||
-          location.pathname === menuItemProps.path
-        ) {
-          return defaultDom;
-        }
-        return <Link to={menuItemProps.path}>{defaultDom}</Link>;
-      }}
-      breadcrumbRender={(routers = []) => [
-        {
-          path: '/',
-          breadcrumbName: formatMessage({ id: 'menu.home' }),
-        },
-        ...routers,
-      ]}
-      itemRender={(route, params, routes, paths) => {
-        const first = routes.indexOf(route) === 0;
-        return first ? (
-          <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
-        ) : (
-          <span>{route.breadcrumbName}</span>
-        );
-      }}
-      footerRender={() => {
-        if (settings.footerRender || settings.footerRender === undefined) {
-          return defaultFooterDom;
-        }
-        return null;
-      }}
-      menuDataRender={menuDataRender}
-      rightContentRender={() => <RightContent />}
-      postMenuData={(menuData) => {
-        menuDataRef.current = menuData || [];
-        return menuData || [];
-      }}
-      waterMarkProps={{
-        content: 'Ant Design Pro',
-        fontColor: 'rgba(24,144,255,0.15)',
-      }}
-    >
-      <Authorized authority={authorized!.authority} noMatch={noMatch}>
-        {children}
-      </Authorized>
-    </ProLayout>
-  );
-};
-
-export default connect(({ global, settings }: ConnectState) => ({
-  collapsed: global.collapsed,
-  settings,
-}))(BasicLayout);

+ 0 - 10
src/layouts/BlankLayout.tsx

@@ -1,10 +0,0 @@
-import React from 'react';
-import { Inspector } from 'react-dev-inspector';
-
-const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment;
-
-const Layout: React.FC = ({ children }) => {
-  return <InspectorWrapper>{children}</InspectorWrapper>;
-};
-
-export default Layout;

+ 0 - 58
src/layouts/SecurityLayout.tsx

@@ -1,58 +0,0 @@
-import React from 'react';
-import { PageLoading } from '@ant-design/pro-layout';
-import type { ConnectProps } from 'umi';
-import { Redirect, connect } from 'umi';
-import { stringify } from 'querystring';
-import type { ConnectState } from '@/models/connect';
-import type { CurrentUser } from '@/models/user';
-
-type SecurityLayoutProps = {
-  loading?: boolean;
-  currentUser?: CurrentUser;
-} & ConnectProps;
-
-type SecurityLayoutState = {
-  isReady: boolean;
-};
-
-class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
-  state: SecurityLayoutState = {
-    isReady: false,
-  };
-
-  componentDidMount() {
-    this.setState({
-      isReady: true,
-    });
-    const { dispatch } = this.props;
-    if (dispatch) {
-      dispatch({
-        type: 'user/fetchCurrent',
-      });
-    }
-  }
-
-  render() {
-    const { isReady } = this.state;
-    const { children, loading, currentUser } = this.props;
-    // You can replace it to your authentication rule (such as check token exists)
-    // You can replace it with your own login authentication rules (such as judging whether the token exists)
-    const isLogin = currentUser && currentUser.userid;
-    const queryString = stringify({
-      redirect: window.location.href,
-    });
-
-    if ((!isLogin && loading) || !isReady) {
-      return <PageLoading />;
-    }
-    if (!isLogin && window.location.pathname !== '/user/login') {
-      return <Redirect to={`/user/login?${queryString}`} />;
-    }
-    return children;
-  }
-}
-
-export default connect(({ user, loading }: ConnectState) => ({
-  currentUser: user.currentUser,
-  loading: loading.models.user,
-}))(SecurityLayout);

+ 0 - 70
src/layouts/UserLayout.tsx

@@ -1,70 +0,0 @@
-import type { MenuDataItem } from '@ant-design/pro-layout';
-import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout';
-import { Helmet, HelmetProvider } from 'react-helmet-async';
-import type { ConnectProps } from 'umi';
-import { Link, SelectLang, useIntl, connect, FormattedMessage } from 'umi';
-import React from 'react';
-import type { ConnectState } from '@/models/connect';
-import logo from '../assets/logo.svg';
-import styles from './UserLayout.less';
-
-export type UserLayoutProps = {
-  breadcrumbNameMap: Record<string, MenuDataItem>;
-} & Partial<ConnectProps>;
-
-const UserLayout: React.FC<UserLayoutProps> = (props) => {
-  const {
-    route = {
-      routes: [],
-    },
-  } = props;
-  const { routes = [] } = route;
-  const {
-    children,
-    location = {
-      pathname: '',
-    },
-  } = props;
-  const { formatMessage } = useIntl();
-  const { breadcrumb } = getMenuData(routes);
-  const title = getPageTitle({
-    pathname: location.pathname,
-    formatMessage,
-    breadcrumb,
-    ...props,
-  });
-  return (
-    <HelmetProvider>
-      <Helmet>
-        <title>{title}</title>
-        <meta name="description" content={title} />
-      </Helmet>
-
-      <div className={styles.container}>
-        <div className={styles.lang}>
-          <SelectLang />
-        </div>
-        <div className={styles.content}>
-          <div className={styles.top}>
-            <div className={styles.header}>
-              <Link to="/">
-                <img alt="logo" className={styles.logo} src={logo} />
-                <span className={styles.title}>Ant Design</span>
-              </Link>
-            </div>
-            <div className={styles.desc}>
-              <FormattedMessage
-                id="pages.layouts.userLayout.title"
-                defaultMessage="Ant Design. The most influential Web design specification in Xihu District."
-              />
-            </div>
-          </div>
-          {children}
-        </div>
-        <DefaultFooter />
-      </div>
-    </HelmetProvider>
-  );
-};
-
-export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);

+ 2 - 1
src/locales/en-US.ts

@@ -1,16 +1,17 @@
 import component from './en-US/component';
 import globalHeader from './en-US/globalHeader';
 import menu from './en-US/menu';
+import pages from './en-US/pages';
 import pwa from './en-US/pwa';
 import settingDrawer from './en-US/settingDrawer';
 import settings from './en-US/settings';
-import pages from './en-US/pages';
 
 export default {
   'navBar.lang': 'Languages',
   'layout.user.link.help': 'Help',
   'layout.user.link.privacy': 'Privacy',
   'layout.user.link.terms': 'Terms',
+  'app.copyright.produced': 'Produced by Ant Financial Experience Department',
   'app.preview.down.block': 'Download this page to your local project',
   'app.welcome.link.fetch-blocks': 'Get all block',
   'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',

+ 3 - 1
src/locales/en-US/pages.ts

@@ -3,6 +3,8 @@ export default {
     'Ant Design is the most influential web design specification in Xihu district',
   'pages.login.accountLogin.tab': 'Account Login',
   'pages.login.accountLogin.errorMessage': 'Incorrect username/password(admin/ant.design)',
+  'pages.login.failure': 'Login failed, please try again!',
+  'pages.login.success': 'Login successful!',
   'pages.login.username.placeholder': 'Username: admin or user',
   'pages.login.username.required': 'Please input your username!',
   'pages.login.password.placeholder': 'Password: ant.design',
@@ -18,7 +20,7 @@ export default {
   'pages.getCaptchaSecondText': 'sec(s)',
   'pages.login.rememberMe': 'Remember me',
   'pages.login.forgotPassword': 'Forgot Password ?',
-  'pages.login.submit': 'Submit',
+  'pages.login.submit': 'Login',
   'pages.login.loginWith': 'Login with :',
   'pages.login.registerAccount': 'Register Account',
   'pages.welcome.advancedComponent': 'Advanced Component',

+ 24 - 0
src/locales/fa-IR.ts

@@ -0,0 +1,24 @@
+import component from './fa-IR/component';
+import globalHeader from './fa-IR/globalHeader';
+import menu from './fa-IR/menu';
+import pwa from './fa-IR/pwa';
+import settingDrawer from './fa-IR/settingDrawer';
+import settings from './fa-IR/settings';
+import pages from './fa-IR/pages';
+
+export default {
+  'navBar.lang': 'زبان ها  ',
+  'layout.user.link.help': 'کمک',
+  'layout.user.link.privacy': 'حریم خصوصی',
+  'layout.user.link.terms': 'مقررات',
+  'app.preview.down.block': 'این صفحه را در پروژه محلی خود بارگیری کنید',
+  'app.welcome.link.fetch-blocks': 'دریافت تمام بلوک',
+  'app.welcome.link.block-list': 'به سرعت صفحات استاندارد مبتنی بر توسعه "بلوک" را بسازید',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+  ...pages,
+};

+ 5 - 0
src/locales/fa-IR/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': 'باز',
+  'component.tagSelect.collapse': 'بسته ',
+  'component.tagSelect.all': 'همه',
+};

+ 17 - 0
src/locales/fa-IR/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': 'جستجو ',
+  'component.globalHeader.search.example1': 'مثال 1 را جستجو کنید',
+  'component.globalHeader.search.example2': 'مثال 2 را جستجو کنید',
+  'component.globalHeader.search.example3': 'مثال 3 را جستجو کنید',
+  'component.globalHeader.help': 'کمک',
+  'component.globalHeader.notification': 'اعلان',
+  'component.globalHeader.notification.empty': 'شما همه اعلان ها را مشاهده کرده اید.',
+  'component.globalHeader.message': 'پیام',
+  'component.globalHeader.message.empty': 'شما همه پیام ها را مشاهده کرده اید.',
+  'component.globalHeader.event': 'رویداد',
+  'component.globalHeader.event.empty': 'شما همه رویدادها را مشاهده کرده اید.',
+  'component.noticeIcon.clear': 'پاک کردن',
+  'component.noticeIcon.cleared': 'پاک شد',
+  'component.noticeIcon.empty': 'بدون اعلان',
+  'component.noticeIcon.view-more': 'نمایش بیشتر',
+};

+ 52 - 0
src/locales/fa-IR/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': 'خوش آمدید',
+  'menu.more-blocks': 'بلوک های بیشتر',
+  'menu.home': 'خانه',
+  'menu.admin': 'مدیر',
+  'menu.admin.sub-page': 'زیر صفحه',
+  'menu.login': 'ورود',
+  'menu.register': 'ثبت نام',
+  'menu.register.result': 'ثبت نام نتیجه',
+  'menu.dashboard': 'داشبورد',
+  'menu.dashboard.analysis': 'تحلیل و بررسی',
+  'menu.dashboard.monitor': 'نظارت',
+  'menu.dashboard.workplace': 'محل کار',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': 'فرم',
+  'menu.form.basic-form': 'فرم اساسی',
+  'menu.form.step-form': 'فرم مرحله',
+  'menu.form.step-form.info': 'فرم مرحله (نوشتن اطلاعات انتقال)',
+  'menu.form.step-form.confirm': 'فرم مرحله (تأیید اطلاعات انتقال)',
+  'menu.form.step-form.result': 'فرم مرحله (تمام شده)',
+  'menu.form.advanced-form': 'فرم پیشرفته',
+  'menu.list': 'لیست',
+  'menu.list.table-list': 'جدول جستجو',
+  'menu.list.basic-list': 'لیست اصلی',
+  'menu.list.card-list': 'لیست کارت',
+  'menu.list.search-list': 'لیست جستجو',
+  'menu.list.search-list.articles': 'لیست جستجو (مقالات)',
+  'menu.list.search-list.projects': 'لیست جستجو (پروژه ها)',
+  'menu.list.search-list.applications': 'لیست جستجو (برنامه ها)',
+  'menu.profile': 'مشخصات',
+  'menu.profile.basic': 'مشخصات عمومی',
+  'menu.profile.advanced': 'مشخصات پیشرفته',
+  'menu.result': 'نتیجه',
+  'menu.result.success': 'موفق',
+  'menu.result.fail': 'ناموفق',
+  'menu.exception': 'استثنا',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': 'راه اندازی',
+  'menu.account': 'حساب',
+  'menu.account.center': 'مرکز حساب',
+  'menu.account.settings': 'تنظیمات حساب',
+  'menu.account.trigger': 'خطای راه اندازی',
+  'menu.account.logout': 'خروج',
+  'menu.editor': 'ویرایشگر گرافیک',
+  'menu.editor.flow': 'ویرایشگر جریان',
+  'menu.editor.mind': 'ویرایشگر ذهن',
+  'menu.editor.koni': 'ویرایشگر Koni',
+};

+ 67 - 0
src/locales/fa-IR/pages.ts

@@ -0,0 +1,67 @@
+export default {
+  'pages.layouts.userLayout.title': 'طراحی مورچه تأثیرگذارترین مشخصات طراحی وب در منطقه Xihu است',
+  'pages.login.accountLogin.tab': 'ورود به حساب کاربری',
+  'pages.login.accountLogin.errorMessage': 'نام کاربری / رمزعبور نادرست (مدیر / ant.design)',
+  'pages.login.username.placeholder': 'نام کاربری: مدیر یا کاربر',
+  'pages.login.username.required': 'لطفا نام کاربری خود را وارد کنید!',
+  'pages.login.password.placeholder': 'رمز عبور: ant.design',
+  'pages.login.password.required': 'لطفاً رمز ورود خود را وارد کنید!',
+  'pages.login.phoneLogin.tab': 'ورود به سیستم تلفن',
+  'pages.login.phoneLogin.errorMessage': 'خطای کد تأیید',
+  'pages.login.phoneNumber.placeholder': 'شماره تلفن',
+  'pages.login.phoneNumber.required': 'لطفاً شماره تلفن خود را وارد کنید!',
+  'pages.login.phoneNumber.invalid': 'شماره تلفن نامعتبر است!',
+  'pages.login.captcha.placeholder': 'کد تایید',
+  'pages.login.captcha.required': 'لطفا کد تأیید را وارد کنید!',
+  'pages.login.phoneLogin.getVerificationCode': 'دریافت کد',
+  'pages.getCaptchaSecondText': 'ثانیه',
+  'pages.login.rememberMe': 'مرا به خاطر بسپار',
+  'pages.login.forgotPassword': 'رمز عبور را فراموش کرده اید ?',
+  'pages.login.submit': 'ارسال',
+  'pages.login.loginWith': 'وارد شوید با :',
+  'pages.login.registerAccount': 'ثبت نام',
+  'pages.welcome.advancedComponent': 'مولفه پیشرفته',
+  'pages.welcome.link': 'خوش آمدید',
+  'pages.welcome.advancedLayout': 'چیدمان پیشرفته',
+  'pages.welcome.alertMessage': 'اجزای سنگین تر سریعتر و قوی تر آزاد شده اند.',
+  'pages.admin.subPage.title': 'این صفحه فقط توسط مدیر قابل مشاهده است',
+  'pages.admin.subPage.alertMessage':
+    'رابط کاربری Umi اکنون منتشر شده است ، برای شروع تجربه استفاده از npm run ui خوش آمدید.',
+  'pages.searchTable.createForm.newRule': 'قانون جدید',
+  'pages.searchTable.updateForm.ruleConfig': 'پیکربندی قانون',
+  'pages.searchTable.updateForm.basicConfig': 'اطلاعات اولیه',
+  'pages.searchTable.updateForm.ruleName.nameLabel': ' نام قانون',
+  'pages.searchTable.updateForm.ruleName.nameRules': 'لطفاً نام قانون را وارد کنید!',
+  'pages.searchTable.updateForm.ruleDesc.descLabel': 'شرح قانون',
+  'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'لطفاً حداقل پنج حرف وارد کنید',
+  'pages.searchTable.updateForm.ruleDesc.descRules':
+    'لطفاً حداقل یک قانون حاوی پنج کاراکتر شرح دهید!',
+  'pages.searchTable.updateForm.ruleProps.title': 'پیکربندی خصوصیات',
+  'pages.searchTable.updateForm.object': 'نظارت بر شی',
+  'pages.searchTable.updateForm.ruleProps.templateLabel': 'الگوی قانون',
+  'pages.searchTable.updateForm.ruleProps.typeLabel': 'نوع قانون',
+  'pages.searchTable.updateForm.schedulingPeriod.title': 'تنظیم دوره زمان بندی',
+  'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'زمان شروع',
+  'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'لطفاً زمان شروع را انتخاب کنید!',
+  'pages.searchTable.titleDesc': 'شرح',
+  'pages.searchTable.ruleName': 'نام قانون لازم است',
+  'pages.searchTable.titleCallNo': 'تعداد تماس های خدماتی',
+  'pages.searchTable.titleStatus': 'وضعیت',
+  'pages.searchTable.nameStatus.default': 'پیش فرض',
+  'pages.searchTable.nameStatus.running': 'در حال دویدن',
+  'pages.searchTable.nameStatus.online': 'برخط',
+  'pages.searchTable.nameStatus.abnormal': 'غیرطبیعی',
+  'pages.searchTable.titleUpdatedAt': 'آخرین برنامه ریزی در',
+  'pages.searchTable.exception': 'لطفا دلیل استثنا را وارد کنید!',
+  'pages.searchTable.titleOption': 'گزینه',
+  'pages.searchTable.config': 'پیکربندی',
+  'pages.searchTable.subscribeAlert': 'مشترک شدن در هشدارها',
+  'pages.searchTable.title': 'فرم درخواست',
+  'pages.searchTable.new': 'جدید',
+  'pages.searchTable.chosen': 'انتخاب شده',
+  'pages.searchTable.item': 'مورد',
+  'pages.searchTable.totalServiceCalls': 'تعداد کل تماس های خدماتی',
+  'pages.searchTable.tenThousand': '0000',
+  'pages.searchTable.batchDeletion': 'حذف دسته ای',
+  'pages.searchTable.batchApproval': 'تصویب دسته ای',
+};

+ 7 - 0
src/locales/fa-IR/pwa.ts

@@ -0,0 +1,7 @@
+export default {
+  'app.pwa.offline': 'شما اکنون آفلاین هستید',
+  'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است',
+  'app.pwa.serviceworker.updated.hint':
+    'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید',
+  'app.pwa.serviceworker.updated.ok': 'تازه سازی',
+};

+ 32 - 0
src/locales/fa-IR/settingDrawer.ts

@@ -0,0 +1,32 @@
+export default {
+  'app.setting.pagestyle': 'تنظیم نوع صفحه',
+  'app.setting.pagestyle.dark': 'سبک تیره',
+  'app.setting.pagestyle.light': 'سبک سبک',
+  'app.setting.content-width': 'عرض محتوا',
+  'app.setting.content-width.fixed': 'ثابت',
+  'app.setting.content-width.fluid': 'شناور',
+  'app.setting.themecolor': 'رنگ تم',
+  'app.setting.themecolor.dust': 'گرد و غبار قرمز',
+  'app.setting.themecolor.volcano': 'آتشفشان',
+  'app.setting.themecolor.sunset': 'غروب نارنجی',
+  'app.setting.themecolor.cyan': 'فیروزه ای',
+  'app.setting.themecolor.green': 'سبز قطبی',
+  'app.setting.themecolor.daybreak': 'آبی روشن(پیشفرض)',
+  'app.setting.themecolor.geekblue': 'چسب گیک',
+  'app.setting.themecolor.purple': 'بنفش طلایی',
+  'app.setting.navigationmode': 'حالت پیمایش',
+  'app.setting.sidemenu': 'طرح منوی کناری',
+  'app.setting.topmenu': 'طرح منوی بالایی',
+  'app.setting.fixedheader': 'سرصفحه ثابت',
+  'app.setting.fixedsidebar': 'نوار کناری ثابت',
+  'app.setting.fixedsidebar.hint': 'کار بر روی منوی کناری',
+  'app.setting.hideheader': 'هدر پنهان هنگام پیمایش',
+  'app.setting.hideheader.hint': 'وقتی Hidden Header فعال باشد کار می کند',
+  'app.setting.othersettings': 'تنظیمات دیگر',
+  'app.setting.weakmode': 'حالت ضعیف',
+  'app.setting.copy': 'تنظیمات کپی',
+  'app.setting.copyinfo':
+    'موفقیت در کپی کردن , لطفا defaultSettings را در src / models / setting.js جایگزین کنید',
+  'app.setting.production.hint':
+    'صفحه تنظیم فقط در محیط توسعه نمایش داده می شود ، لطفاً دستی تغییر دهید',
+};

+ 60 - 0
src/locales/fa-IR/settings.ts

@@ -0,0 +1,60 @@
+export default {
+  'app.settings.menuMap.basic': 'تنظیمات پایه ',
+  'app.settings.menuMap.security': 'تنظیمات امنیتی',
+  'app.settings.menuMap.binding': 'صحافی حساب',
+  'app.settings.menuMap.notification': 'اعلان پیام جدید',
+  'app.settings.basic.avatar': 'آواتار',
+  'app.settings.basic.change-avatar': 'آواتار را تغییر دهید',
+  'app.settings.basic.email': 'ایمیل',
+  'app.settings.basic.email-message': 'لطفا ایمیل خود را وارد کنید!',
+  'app.settings.basic.nickname': 'نام مستعار',
+  'app.settings.basic.nickname-message': 'لطفاً نام مستعار خود را وارد کنید!',
+  'app.settings.basic.profile': 'پروفایل شخصی',
+  'app.settings.basic.profile-message': 'لطفاً مشخصات شخصی خود را وارد کنید!',
+  'app.settings.basic.profile-placeholder': 'معرفی مختصر خودتان',
+  'app.settings.basic.country': 'کشور / منطقه',
+  'app.settings.basic.country-message': 'لطفاً کشور خود را وارد کنید!',
+  'app.settings.basic.geographic': 'استان یا شهر',
+  'app.settings.basic.geographic-message': 'لطفاً اطلاعات جغرافیایی خود را وارد کنید!',
+  'app.settings.basic.address': 'آدرس خیابان',
+  'app.settings.basic.address-message': 'لطفا آدرس خود را وارد کنید!',
+  'app.settings.basic.phone': 'شماره تلفن',
+  'app.settings.basic.phone-message': 'لطفاً تلفن خود را وارد کنید!',
+  'app.settings.basic.update': 'به روز رسانی اطلاعات',
+  'app.settings.security.strong': 'قوی',
+  'app.settings.security.medium': 'متوسط',
+  'app.settings.security.weak': 'ضعیف',
+  'app.settings.security.password': 'رمز عبور حساب کاربری',
+  'app.settings.security.password-description': 'قدرت رمز عبور فعلی',
+  'app.settings.security.phone': 'تلفن امنیتی',
+  'app.settings.security.phone-description': 'تلفن مقید',
+  'app.settings.security.question': 'سوال امنیتی',
+  'app.settings.security.question-description':
+    'سوال امنیتی تنظیم نشده است و سیاست امنیتی می تواند به طور موثر از امنیت حساب محافظت کند',
+  'app.settings.security.email': 'ایمیل پشتیبان',
+  'app.settings.security.email-description': 'ایمیل مقید',
+  'app.settings.security.mfa': 'دستگاه MFA',
+  'app.settings.security.mfa-description':
+    'دستگاه MFA بسته نشده ، پس از اتصال ، می تواند دو بار تأیید شود',
+  'app.settings.security.modify': 'تغییر',
+  'app.settings.security.set': 'تنظیم',
+  'app.settings.security.bind': 'بستن',
+  'app.settings.binding.taobao': 'اتصال Taobao',
+  'app.settings.binding.taobao-description': 'حساب Taobao در حال حاضر بسته نشده است',
+  'app.settings.binding.alipay': 'اتصال Alipay',
+  'app.settings.binding.alipay-description': 'حساب Alipay در حال حاضر بسته نشده است',
+  'app.settings.binding.dingding': 'اتصال DingTalk',
+  'app.settings.binding.dingding-description': 'حساب DingTalk در حال حاضر محدود نشده است',
+  'app.settings.binding.bind': 'بستن',
+  'app.settings.notification.password': 'رمز عبور حساب کاربری',
+  'app.settings.notification.password-description':
+    'پیام های سایر کاربران در قالب یک نامه ایستگاهی اعلام خواهد شد',
+  'app.settings.notification.messages': 'پیام های سیستم',
+  'app.settings.notification.messages-description':
+    'پیام های سیستم به صورت نامه ایستگاه مطلع می شوند',
+  'app.settings.notification.todo': 'اعلان کارها',
+  'app.settings.notification.todo-description':
+    'لیست کارها به صورت نامه ای از ایستگاه اطلاع داده می شود',
+  'app.settings.open': 'باز کن',
+  'app.settings.close': 'بستن',
+};

+ 2 - 0
src/locales/pt-BR.ts

@@ -4,6 +4,7 @@ import menu from './pt-BR/menu';
 import pwa from './pt-BR/pwa';
 import settingDrawer from './pt-BR/settingDrawer';
 import settings from './pt-BR/settings';
+import pages from './pt-BR/pages';
 
 export default {
   'navBar.lang': 'Idiomas',
@@ -17,4 +18,5 @@ export default {
   ...settings,
   ...pwa,
   ...component,
+  ...pages,
 };

+ 70 - 0
src/locales/pt-BR/pages.ts

@@ -0,0 +1,70 @@
+export default {
+  'pages.layouts.userLayout.title':
+    'Ant Design é a especificação de web design mais influente no distrito de Xihu',
+  'pages.login.accountLogin.tab': 'Login da conta',
+  'pages.login.accountLogin.errorMessage': 'usuário/senha incorreto(admin/ant.design)',
+  'pages.login.username.placeholder': 'Usuário: admin or user',
+  'pages.login.username.required': 'Por favor insira seu usuário!',
+  'pages.login.password.placeholder': 'Senha: ant.design',
+  'pages.login.password.required': 'Por favor insira sua senha!',
+  'pages.login.phoneLogin.tab': 'Login com Telefone',
+  'pages.login.phoneLogin.errorMessage': 'Erro de Código de Verificação',
+  'pages.login.phoneNumber.placeholder': 'Telefone',
+  'pages.login.phoneNumber.required': 'Por favor entre com seu telefone!',
+  'pages.login.phoneNumber.invalid': 'Telefone é inválido!',
+  'pages.login.captcha.placeholder': 'Código de Verificação',
+  'pages.login.captcha.required': 'Por favor entre com o código de verificação!',
+  'pages.login.phoneLogin.getVerificationCode': 'Obter Código',
+  'pages.getCaptchaSecondText': 'seg(s)',
+  'pages.login.rememberMe': 'Lembre-me',
+  'pages.login.forgotPassword': 'Perdeu a Senha ?',
+  'pages.login.submit': 'Enviar',
+  'pages.login.loginWith': 'Login com :',
+  'pages.login.registerAccount': 'Registra Conta',
+  'pages.welcome.advancedComponent': 'Componente Avançado',
+  'pages.welcome.link': 'Bem-vindo',
+  'pages.welcome.advancedLayout': 'Layout Avançado',
+  'pages.welcome.alertMessage': 'Componentes pesados mais rápidos e mais fortes foram lançados.',
+  'pages.admin.subPage.title': 'Esta página só pode ser vista pelo Admin',
+  'pages.admin.subPage.alertMessage':
+    'O Umi ui foi lançado, bem-vindo ao usar o npm run ui para iniciar a experiência.',
+  'pages.searchTable.createForm.newRule': 'Neva Regra',
+  'pages.searchTable.updateForm.ruleConfig': 'Configuração de Regra',
+  'pages.searchTable.updateForm.basicConfig': 'Informação básica',
+  'pages.searchTable.updateForm.ruleName.nameLabel': 'Nome da Regra',
+  'pages.searchTable.updateForm.ruleName.nameRules': 'Por favor entre com o nome da regra!',
+  'pages.searchTable.updateForm.ruleDesc.descLabel': 'Descrição da Regra',
+  'pages.searchTable.updateForm.ruleDesc.descPlaceholder':
+    'Por favor insira ao menos cinco caracteres',
+  'pages.searchTable.updateForm.ruleDesc.descRules':
+    'Insira uma descrição de regra de pelo menos cinco caracteres!',
+  'pages.searchTable.updateForm.ruleProps.title': 'Configurar Propriedades',
+  'pages.searchTable.updateForm.object': 'Objeto de Monitoramento',
+  'pages.searchTable.updateForm.ruleProps.templateLabel': 'Modelo de Regra',
+  'pages.searchTable.updateForm.ruleProps.typeLabel': 'Tipo de Regra',
+  'pages.searchTable.updateForm.schedulingPeriod.title': 'Definir Período de Agendamento',
+  'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Hora de Início',
+  'pages.searchTable.updateForm.schedulingPeriod.timeRules':
+    'Por favor selecione um horáriod e início!',
+  'pages.searchTable.titleDesc': 'Descrição',
+  'pages.searchTable.ruleName': 'O nome da regra é obrigatório',
+  'pages.searchTable.titleCallNo': 'Número de chamadas de serviço',
+  'pages.searchTable.titleStatus': 'Status',
+  'pages.searchTable.nameStatus.default': 'padrão',
+  'pages.searchTable.nameStatus.running': 'executando',
+  'pages.searchTable.nameStatus.online': 'online',
+  'pages.searchTable.nameStatus.abnormal': 'anormal',
+  'pages.searchTable.titleUpdatedAt': 'Última programação em',
+  'pages.searchTable.exception': 'Por favor, indique o motivo da exceção!',
+  'pages.searchTable.titleOption': 'Opção',
+  'pages.searchTable.config': 'Configuração',
+  'pages.searchTable.subscribeAlert': 'Inscreva-se para receber alertas',
+  'pages.searchTable.title': 'Formulário de Consulta',
+  'pages.searchTable.new': 'Novo',
+  'pages.searchTable.chosen': 'selecionado',
+  'pages.searchTable.item': 'item',
+  'pages.searchTable.totalServiceCalls': 'Número total de chamadas de serviço',
+  'pages.searchTable.tenThousand': '0000',
+  'pages.searchTable.batchDeletion': 'deleção em lote',
+  'pages.searchTable.batchApproval': 'aprovação em lote',
+};

+ 1 - 0
src/locales/zh-CN.ts

@@ -11,6 +11,7 @@ export default {
   'layout.user.link.help': '帮助',
   'layout.user.link.privacy': '隐私',
   'layout.user.link.terms': '条款',
+  'app.copyright.produced': '蚂蚁集团体验技术部出品',
   'app.preview.down.block': '下载此页面到本地项目',
   'app.welcome.link.fetch-blocks': '获取全部区块',
   'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',

+ 3 - 1
src/locales/zh-CN/pages.ts

@@ -2,6 +2,8 @@ export default {
   'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
   'pages.login.accountLogin.tab': '账户密码登录',
   'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)',
+  'pages.login.failure': '登录失败,请重试!',
+  'pages.login.success': '登录成功!',
   'pages.login.username.placeholder': '用户名: admin or user',
   'pages.login.username.required': '用户名是必填项!',
   'pages.login.password.placeholder': '密码: ant.design',
@@ -17,7 +19,7 @@ export default {
   'pages.getCaptchaSecondText': '秒后重新获取',
   'pages.login.rememberMe': '自动登录',
   'pages.login.forgotPassword': '忘记密码 ?',
-  'pages.login.submit': '提交',
+  'pages.login.submit': '登录',
   'pages.login.loginWith': '其他登录方式 :',
   'pages.login.registerAccount': '注册账户',
   'pages.welcome.advancedComponent': '高级表格',

+ 0 - 30
src/models/connect.d.ts

@@ -1,30 +0,0 @@
-import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout';
-import { GlobalModelState } from './global';
-import { UserModelState } from './user';
-import type { StateType } from './login';
-
-export { GlobalModelState, UserModelState };
-
-export type Loading = {
-  global: boolean;
-  effects: Record<string, boolean | undefined>;
-  models: {
-    global?: boolean;
-    menu?: boolean;
-    setting?: boolean;
-    user?: boolean;
-    login?: boolean;
-  };
-};
-
-export type ConnectState = {
-  global: GlobalModelState;
-  loading: Loading;
-  settings: ProSettings;
-  user: UserModelState;
-  login: StateType;
-};
-
-export type Route = {
-  routes?: Route[];
-} & MenuDataItem;

+ 0 - 126
src/models/global.ts

@@ -1,126 +0,0 @@
-import type { Reducer, Effect } from 'umi';
-
-import type { NoticeIconData } from '@/components/NoticeIcon';
-import { queryNotices } from '@/services/user';
-import type { ConnectState } from './connect.d';
-
-export type NoticeItem = {
-  id: string;
-  type: string;
-  status: string;
-} & NoticeIconData;
-
-export type GlobalModelState = {
-  collapsed: boolean;
-  notices: NoticeItem[];
-};
-
-export type GlobalModelType = {
-  namespace: 'global';
-  state: GlobalModelState;
-  effects: {
-    fetchNotices: Effect;
-    clearNotices: Effect;
-    changeNoticeReadState: Effect;
-  };
-  reducers: {
-    changeLayoutCollapsed: Reducer<GlobalModelState>;
-    saveNotices: Reducer<GlobalModelState>;
-    saveClearedNotices: Reducer<GlobalModelState>;
-  };
-};
-
-const GlobalModel: GlobalModelType = {
-  namespace: 'global',
-
-  state: {
-    collapsed: false,
-    notices: [],
-  },
-
-  effects: {
-    *fetchNotices(_, { call, put, select }) {
-      const data = yield call(queryNotices);
-      yield put({
-        type: 'saveNotices',
-        payload: data,
-      });
-      const unreadCount: number = yield select(
-        (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
-      );
-      yield put({
-        type: 'user/changeNotifyCount',
-        payload: {
-          totalCount: data.length,
-          unreadCount,
-        },
-      });
-    },
-    *clearNotices({ payload }, { put, select }) {
-      yield put({
-        type: 'saveClearedNotices',
-        payload,
-      });
-      const count: number = yield select((state: ConnectState) => state.global.notices.length);
-      const unreadCount: number = yield select(
-        (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
-      );
-      yield put({
-        type: 'user/changeNotifyCount',
-        payload: {
-          totalCount: count,
-          unreadCount,
-        },
-      });
-    },
-    *changeNoticeReadState({ payload }, { put, select }) {
-      const notices: NoticeItem[] = yield select((state: ConnectState) =>
-        state.global.notices.map((item) => {
-          const notice = { ...item };
-          if (notice.id === payload) {
-            notice.read = true;
-          }
-          return notice;
-        }),
-      );
-
-      yield put({
-        type: 'saveNotices',
-        payload: notices,
-      });
-
-      yield put({
-        type: 'user/changeNotifyCount',
-        payload: {
-          totalCount: notices.length,
-          unreadCount: notices.filter((item) => !item.read).length,
-        },
-      });
-    },
-  },
-
-  reducers: {
-    changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
-      return {
-        ...state,
-        collapsed: payload,
-      };
-    },
-    saveNotices(state, { payload }): GlobalModelState {
-      return {
-        collapsed: false,
-        ...state,
-        notices: payload,
-      };
-    },
-    saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
-      return {
-        ...state,
-        collapsed: false,
-        notices: state.notices.filter((item): boolean => item.type !== payload),
-      };
-    },
-  },
-};
-
-export default GlobalModel;

+ 0 - 93
src/models/login.ts

@@ -1,93 +0,0 @@
-import { stringify } from 'querystring';
-import type { Reducer, Effect } from 'umi';
-import { history } from 'umi';
-
-import { fakeAccountLogin } from '@/services/login';
-import { setAuthority } from '@/utils/authority';
-import { getPageQuery } from '@/utils/utils';
-import { message } from 'antd';
-
-export type StateType = {
-  status?: 'ok' | 'error';
-  type?: string;
-  currentAuthority?: 'user' | 'guest' | 'admin';
-};
-
-export type LoginModelType = {
-  namespace: string;
-  state: StateType;
-  effects: {
-    login: Effect;
-    logout: Effect;
-  };
-  reducers: {
-    changeLoginStatus: Reducer<StateType>;
-  };
-};
-
-const Model: LoginModelType = {
-  namespace: 'login',
-
-  state: {
-    status: undefined,
-  },
-
-  effects: {
-    *login({ payload }, { call, put }) {
-      const response = yield call(fakeAccountLogin, payload);
-      yield put({
-        type: 'changeLoginStatus',
-        payload: response,
-      });
-      // Login successfully
-      if (response.status === 'ok') {
-        const urlParams = new URL(window.location.href);
-        const params = getPageQuery();
-        message.success('🎉 🎉 🎉  登录成功!');
-        let { redirect } = params as { redirect: string };
-        if (redirect) {
-          const redirectUrlParams = new URL(redirect);
-          if (redirectUrlParams.origin === urlParams.origin) {
-            redirect = redirect.substr(urlParams.origin.length);
-            if (window.routerBase !== '/') {
-              redirect = redirect.replace(window.routerBase, '/');
-            }
-            if (redirect.match(/^\/.*#/)) {
-              redirect = redirect.substr(redirect.indexOf('#') + 1);
-            }
-          } else {
-            window.location.href = '/';
-            return;
-          }
-        }
-        history.replace(redirect || '/');
-      }
-    },
-
-    logout() {
-      const { redirect } = getPageQuery();
-      // Note: There may be security issues, please note
-      if (window.location.pathname !== '/user/login' && !redirect) {
-        history.replace({
-          pathname: '/user/login',
-          search: stringify({
-            redirect: window.location.href,
-          }),
-        });
-      }
-    },
-  },
-
-  reducers: {
-    changeLoginStatus(state, { payload }) {
-      setAuthority(payload.currentAuthority);
-      return {
-        ...state,
-        status: payload.status,
-        type: payload.type,
-      };
-    },
-  },
-};
-
-export default Model;

+ 0 - 38
src/models/setting.ts

@@ -1,38 +0,0 @@
-import type { Reducer } from 'umi';
-import type { DefaultSettings } from '../../config/defaultSettings';
-import defaultSettings from '../../config/defaultSettings';
-
-export type SettingModelType = {
-  namespace: 'settings';
-  state: DefaultSettings;
-  reducers: {
-    changeSetting: Reducer<DefaultSettings>;
-  };
-};
-
-const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => {
-  const root = document.getElementById('root');
-  if (root) {
-    root.className = colorWeak ? 'colorWeak' : '';
-  }
-};
-
-const SettingModel: SettingModelType = {
-  namespace: 'settings',
-  state: defaultSettings,
-  reducers: {
-    changeSetting(state = defaultSettings, { payload }) {
-      const { colorWeak, contentWidth } = payload;
-
-      if (state.contentWidth !== contentWidth && window.dispatchEvent) {
-        window.dispatchEvent(new Event('resize'));
-      }
-      updateColorWeak(!!colorWeak);
-      return {
-        ...state,
-        ...payload,
-      };
-    },
-  },
-};
-export default SettingModel;

+ 0 - 85
src/models/user.ts

@@ -1,85 +0,0 @@
-import type { Effect, Reducer } from 'umi';
-
-import { queryCurrent, query as queryUsers } from '@/services/user';
-
-export type CurrentUser = {
-  avatar?: string;
-  name?: string;
-  title?: string;
-  group?: string;
-  signature?: string;
-  tags?: {
-    key: string;
-    label: string;
-  }[];
-  userid?: string;
-  unreadCount?: number;
-};
-
-export type UserModelState = {
-  currentUser?: CurrentUser;
-};
-
-export type UserModelType = {
-  namespace: 'user';
-  state: UserModelState;
-  effects: {
-    fetch: Effect;
-    fetchCurrent: Effect;
-  };
-  reducers: {
-    saveCurrentUser: Reducer<UserModelState>;
-    changeNotifyCount: Reducer<UserModelState>;
-  };
-};
-
-const UserModel: UserModelType = {
-  namespace: 'user',
-
-  state: {
-    currentUser: {},
-  },
-
-  effects: {
-    *fetch(_, { call, put }) {
-      const response = yield call(queryUsers);
-      yield put({
-        type: 'save',
-        payload: response,
-      });
-    },
-    *fetchCurrent(_, { call, put }) {
-      const response = yield call(queryCurrent);
-      yield put({
-        type: 'saveCurrentUser',
-        payload: response,
-      });
-    },
-  },
-
-  reducers: {
-    saveCurrentUser(state, action) {
-      return {
-        ...state,
-        currentUser: action.payload || {},
-      };
-    },
-    changeNotifyCount(
-      state = {
-        currentUser: {},
-      },
-      action,
-    ) {
-      return {
-        ...state,
-        currentUser: {
-          ...state.currentUser,
-          notifyCount: action.payload.totalCount,
-          unreadCount: action.payload.unreadCount,
-        },
-      };
-    },
-  },
-};
-
-export default UserModel;

+ 2 - 4
src/pages/TableList/components/UpdateForm.tsx

@@ -10,21 +10,19 @@ import {
 } from '@ant-design/pro-form';
 import { useIntl, FormattedMessage } from 'umi';
 
-import type { TableListItem } from '../data.d';
-
 export type FormValueType = {
   target?: string;
   template?: string;
   type?: string;
   time?: string;
   frequency?: string;
-} & Partial<TableListItem>;
+} & Partial<API.RuleListItem>;
 
 export type UpdateFormProps = {
   onCancel: (flag?: boolean, formVals?: FormValueType) => void;
   onSubmit: (values: FormValueType) => Promise<void>;
   updateModalVisible: boolean;
-  values: Partial<TableListItem>;
+  values: Partial<API.RuleListItem>;
 };
 
 const UpdateForm: React.FC<UpdateFormProps> = (props) => {

+ 0 - 36
src/pages/TableList/data.d.ts

@@ -1,36 +0,0 @@
-export type TableListItem = {
-  key: number;
-  disabled?: boolean;
-  href: string;
-  avatar: string;
-  name: string;
-  owner: string;
-  desc: string;
-  callNo: number;
-  status: number;
-  updatedAt: Date;
-  createdAt: Date;
-  progress: number;
-};
-
-export type TableListPagination = {
-  total: number;
-  pageSize: number;
-  current: number;
-};
-
-export type TableListData = {
-  list: TableListItem[];
-  pagination: Partial<TableListPagination>;
-};
-
-export type TableListParams = {
-  status?: string;
-  name?: string;
-  desc?: string;
-  key?: number;
-  pageSize?: number;
-  currentPage?: number;
-  filter?: Record<string, any[]>;
-  sorter?: Record<string, any>;
-};

+ 14 - 15
src/pages/TableList/index.tsx

@@ -10,16 +10,15 @@ import type { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
 import ProDescriptions from '@ant-design/pro-descriptions';
 import type { FormValueType } from './components/UpdateForm';
 import UpdateForm from './components/UpdateForm';
-import type { TableListItem } from './data.d';
-import { queryRule, updateRule, addRule, removeRule } from './service';
+import { rule, addRule, updateRule, removeRule } from '@/services/ant-design-pro/api';
 
 /**
  * @en-US Add node
  * @zh-CN 添加节点
  * @param fields
  */
-const handleAdd = async (fields: TableListItem) => {
-  const hide = message.loading('Adding');
+const handleAdd = async (fields: API.RuleListItem) => {
+  const hide = message.loading('正在添加');
   try {
     await addRule({ ...fields });
     hide();
@@ -63,8 +62,8 @@ const handleUpdate = async (fields: FormValueType) => {
  *
  * @param selectedRows
  */
-const handleRemove = async (selectedRows: TableListItem[]) => {
-  const hide = message.loading('Deleting');
+const handleRemove = async (selectedRows: API.RuleListItem[]) => {
+  const hide = message.loading('正在删除');
   if (!selectedRows) return true;
   try {
     await removeRule({
@@ -95,8 +94,8 @@ const TableList: React.FC = () => {
   const [showDetail, setShowDetail] = useState<boolean>(false);
 
   const actionRef = useRef<ActionType>();
-  const [currentRow, setCurrentRow] = useState<TableListItem>();
-  const [selectedRowsState, setSelectedRows] = useState<TableListItem[]>([]);
+  const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
+  const [selectedRowsState, setSelectedRows] = useState<API.RuleListItem[]>([]);
 
   /**
    * @en-US International configuration
@@ -104,7 +103,7 @@ const TableList: React.FC = () => {
    * */
   const intl = useIntl();
 
-  const columns: ProColumns<TableListItem>[] = [
+  const columns: ProColumns<API.RuleListItem>[] = [
     {
       title: (
         <FormattedMessage
@@ -240,7 +239,7 @@ const TableList: React.FC = () => {
 
   return (
     <PageContainer>
-      <ProTable<TableListItem>
+      <ProTable<API.RuleListItem, API.PageParams>
         headerTitle={intl.formatMessage({
           id: 'pages.searchTable.title',
           defaultMessage: 'Enquiry form',
@@ -261,7 +260,7 @@ const TableList: React.FC = () => {
             <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
           </Button>,
         ]}
-        request={(params, sorter, filter) => queryRule({ ...params, sorter, filter })}
+        request={rule}
         columns={columns}
         rowSelection={{
           onChange: (_, selectedRows) => {
@@ -282,7 +281,7 @@ const TableList: React.FC = () => {
                   id="pages.searchTable.totalServiceCalls"
                   defaultMessage="Total number of service calls"
                 />{' '}
-                {selectedRowsState.reduce((pre, item) => pre + item.callNo, 0)}{' '}
+                {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}{' '}
                 <FormattedMessage id="pages.searchTable.tenThousand" defaultMessage="万" />
               </span>
             </div>
@@ -317,7 +316,7 @@ const TableList: React.FC = () => {
         visible={createModalVisible}
         onVisibleChange={handleModalVisible}
         onFinish={async (value) => {
-          const success = await handleAdd(value as TableListItem);
+          const success = await handleAdd(value as API.RuleListItem);
           if (success) {
             handleModalVisible(false);
             if (actionRef.current) {
@@ -372,7 +371,7 @@ const TableList: React.FC = () => {
         closable={false}
       >
         {currentRow?.name && (
-          <ProDescriptions<TableListItem>
+          <ProDescriptions<API.RuleListItem>
             column={2}
             title={currentRow?.name}
             request={async () => ({
@@ -381,7 +380,7 @@ const TableList: React.FC = () => {
             params={{
               id: currentRow?.name,
             }}
-            columns={columns as ProDescriptionsItemProps<TableListItem>[]}
+            columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
           />
         )}
       </Drawer>

+ 0 - 38
src/pages/TableList/service.ts

@@ -1,38 +0,0 @@
-import request from '@/utils/request';
-import type { TableListParams, TableListItem } from './data.d';
-
-export async function queryRule(params?: TableListParams) {
-  return request('/api/rule', {
-    params,
-  });
-}
-
-export async function removeRule(params: { key: number[] }) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'delete',
-    },
-  });
-}
-
-export async function addRule(params: TableListItem) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'post',
-    },
-  });
-}
-
-export async function updateRule(params: TableListParams) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'update',
-    },
-  });
-}

+ 0 - 44
src/pages/User/login/index.less

@@ -1,44 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.main {
-  width: 328px;
-  margin: 0 auto;
-  @media screen and (max-width: @screen-sm) {
-    width: 95%;
-    max-width: 328px;
-  }
-
-  :global {
-    .@{ant-prefix}-tabs-nav-list {
-      margin: auto;
-      font-size: 16px;
-    }
-  }
-
-  .icon {
-    margin-left: 16px;
-    color: rgba(0, 0, 0, 0.2);
-    font-size: 24px;
-    vertical-align: middle;
-    cursor: pointer;
-    transition: color 0.3s;
-
-    &:hover {
-      color: @primary-color;
-    }
-  }
-
-  .other {
-    margin-top: 24px;
-    line-height: 22px;
-    text-align: left;
-    .register {
-      float: right;
-    }
-  }
-
-  .prefixIcon {
-    color: @primary-color;
-    font-size: @font-size-base;
-  }
-}

+ 0 - 263
src/pages/User/login/index.tsx

@@ -1,263 +0,0 @@
-import {
-  AlipayCircleOutlined,
-  LockOutlined,
-  MailOutlined,
-  MobileOutlined,
-  TaobaoCircleOutlined,
-  UserOutlined,
-  WeiboCircleOutlined,
-} from '@ant-design/icons';
-import { Alert, Space, message, Tabs } from 'antd';
-import React, { useState } from 'react';
-import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form';
-import { useIntl, connect, FormattedMessage } from 'umi';
-import { getFakeCaptcha } from '@/services/login';
-import type { Dispatch } from 'umi';
-import type { StateType } from '@/models/login';
-import type { LoginParamsType } from '@/services/login';
-import type { ConnectState } from '@/models/connect';
-
-import styles from './index.less';
-
-export type LoginProps = {
-  dispatch: Dispatch;
-  userLogin: StateType;
-  submitting?: boolean;
-};
-
-const LoginMessage: React.FC<{
-  content: string;
-}> = ({ content }) => (
-  <Alert
-    style={{
-      marginBottom: 24,
-    }}
-    message={content}
-    type="error"
-    showIcon
-  />
-);
-
-const Login: React.FC<LoginProps> = (props) => {
-  const { userLogin = {}, submitting } = props;
-  const { status, type: loginType } = userLogin;
-  const [type, setType] = useState<string>('account');
-  const intl = useIntl();
-
-  const handleSubmit = (values: LoginParamsType) => {
-    const { dispatch } = props;
-    dispatch({
-      type: 'login/login',
-      payload: { ...values, type },
-    });
-  };
-  return (
-    <div className={styles.main}>
-      <ProForm
-        initialValues={{
-          autoLogin: true,
-        }}
-        submitter={{
-          render: (_, dom) => dom.pop(),
-          submitButtonProps: {
-            loading: submitting,
-            size: 'large',
-            style: {
-              width: '100%',
-            },
-          },
-        }}
-        onFinish={(values) => {
-          handleSubmit(values as LoginParamsType);
-          return Promise.resolve();
-        }}
-      >
-        <Tabs activeKey={type} onChange={setType}>
-          <Tabs.TabPane
-            key="account"
-            tab={intl.formatMessage({
-              id: 'pages.login.accountLogin.tab',
-              defaultMessage: 'Account password login',
-            })}
-          />
-          <Tabs.TabPane
-            key="mobile"
-            tab={intl.formatMessage({
-              id: 'pages.login.phoneLogin.tab',
-              defaultMessage: 'Mobile phone number login',
-            })}
-          />
-        </Tabs>
-
-        {status === 'error' && loginType === 'account' && !submitting && (
-          <LoginMessage
-            content={intl.formatMessage({
-              id: 'pages.login.accountLogin.errorMessage',
-              defaultMessage: 'Incorrect account or password(admin/ant.design)',
-            })}
-          />
-        )}
-        {type === 'account' && (
-          <>
-            <ProFormText
-              name="userName"
-              fieldProps={{
-                size: 'large',
-                prefix: <UserOutlined className={styles.prefixIcon} />,
-              }}
-              placeholder={intl.formatMessage({
-                id: 'pages.login.username.placeholder',
-                defaultMessage: 'Username: admin or user',
-              })}
-              rules={[
-                {
-                  required: true,
-                  message: (
-                    <FormattedMessage
-                      id="pages.login.username.required"
-                      defaultMessage="Please enter user name!"
-                    />
-                  ),
-                },
-              ]}
-            />
-            <ProFormText.Password
-              name="password"
-              fieldProps={{
-                size: 'large',
-                prefix: <LockOutlined className={styles.prefixIcon} />,
-              }}
-              placeholder={intl.formatMessage({
-                id: 'pages.login.password.placeholder',
-                defaultMessage: 'Password: ant.design',
-              })}
-              rules={[
-                {
-                  required: true,
-                  message: (
-                    <FormattedMessage
-                      id="pages.login.password.required"
-                      defaultMessage="Please enter password!"
-                    />
-                  ),
-                },
-              ]}
-            />
-          </>
-        )}
-
-        {status === 'error' && loginType === 'mobile' && !submitting && (
-          <LoginMessage content="Verification code error" />
-        )}
-        {type === 'mobile' && (
-          <>
-            <ProFormText
-              fieldProps={{
-                size: 'large',
-                prefix: <MobileOutlined className={styles.prefixIcon} />,
-              }}
-              name="mobile"
-              placeholder={intl.formatMessage({
-                id: 'pages.login.phoneNumber.placeholder',
-                defaultMessage: 'Phone number',
-              })}
-              rules={[
-                {
-                  required: true,
-                  message: (
-                    <FormattedMessage
-                      id="pages.login.phoneNumber.required"
-                      defaultMessage="Please enter phone number!"
-                    />
-                  ),
-                },
-                {
-                  pattern: /^1\d{10}$/,
-                  message: (
-                    <FormattedMessage
-                      id="pages.login.phoneNumber.invalid"
-                      defaultMessage="Malformed phone number!"
-                    />
-                  ),
-                },
-              ]}
-            />
-            <ProFormCaptcha
-              fieldProps={{
-                size: 'large',
-                prefix: <MailOutlined className={styles.prefixIcon} />,
-              }}
-              captchaProps={{
-                size: 'large',
-              }}
-              placeholder={intl.formatMessage({
-                id: 'pages.login.captcha.placeholder',
-                defaultMessage: 'Please enter verification code',
-              })}
-              captchaTextRender={(timing, count) => {
-                if (timing) {
-                  return `${count} ${intl.formatMessage({
-                    id: 'pages.getCaptchaSecondText',
-                    defaultMessage: 'Get verification code',
-                  })}`;
-                }
-                return intl.formatMessage({
-                  id: 'pages.login.phoneLogin.getVerificationCode',
-                  defaultMessage: 'Get verification code',
-                });
-              }}
-              name="captcha"
-              rules={[
-                {
-                  required: true,
-                  message: (
-                    <FormattedMessage
-                      id="pages.login.captcha.required"
-                      defaultMessage="Please enter verification code!"
-                    />
-                  ),
-                },
-              ]}
-              onGetCaptcha={async (mobile) => {
-                const result = await getFakeCaptcha(mobile);
-                if (result === false) {
-                  return;
-                }
-                message.success(
-                  'Get the verification code successfully! The verification code is: 1234',
-                );
-              }}
-            />
-          </>
-        )}
-        <div
-          style={{
-            marginBottom: 24,
-          }}
-        >
-          <ProFormCheckbox noStyle name="autoLogin">
-            <FormattedMessage id="pages.login.rememberMe" defaultMessage="Auto login" />
-          </ProFormCheckbox>
-          <a
-            style={{
-              float: 'right',
-            }}
-          >
-            <FormattedMessage id="pages.login.forgotPassword" defaultMessage="Forget password" />
-          </a>
-        </div>
-      </ProForm>
-      <Space className={styles.other}>
-        <FormattedMessage id="pages.login.loginWith" defaultMessage="Other login methods" />
-        <AlipayCircleOutlined className={styles.icon} />
-        <TaobaoCircleOutlined className={styles.icon} />
-        <WeiboCircleOutlined className={styles.icon} />
-      </Space>
-    </div>
-  );
-};
-
-export default connect(({ login, loading }: ConnectState) => ({
-  userLogin: login,
-  submitting: loading.effects['login/login'],
-}))(Login);

+ 0 - 1
src/pages/document.ejs

@@ -36,7 +36,6 @@
           padding: 0;
         }
         #root {
-          background-image: url('<%= context.config.publicPath +"home_bg.png"%>');
           background-repeat: no-repeat;
           background-size: 100% auto;
         }

+ 43 - 0
src/layouts/UserLayout.less

@@ -69,3 +69,46 @@
   color: @text-color-secondary;
   font-size: @font-size-base;
 }
+
+.main {
+  width: 328px;
+  margin: 0 auto;
+  @media screen and (max-width: @screen-sm) {
+    width: 95%;
+    max-width: 328px;
+  }
+
+  :global {
+    .@{ant-prefix}-tabs-nav-list {
+      margin: auto;
+      font-size: 16px;
+    }
+  }
+
+  .icon {
+    margin-left: 16px;
+    color: rgba(0, 0, 0, 0.2);
+    font-size: 24px;
+    vertical-align: middle;
+    cursor: pointer;
+    transition: color 0.3s;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+
+  .other {
+    margin-top: 24px;
+    line-height: 22px;
+    text-align: left;
+    .register {
+      float: right;
+    }
+  }
+
+  .prefixIcon {
+    color: @primary-color;
+    font-size: @font-size-base;
+  }
+}

+ 318 - 0
src/pages/user/Login/index.tsx

@@ -0,0 +1,318 @@
+import {
+  AlipayCircleOutlined,
+  LockOutlined,
+  MobileOutlined,
+  TaobaoCircleOutlined,
+  UserOutlined,
+  WeiboCircleOutlined,
+} from '@ant-design/icons';
+import { Alert, Space, message, Tabs } from 'antd';
+import React, { useState } from 'react';
+import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form';
+import { useIntl, Link, history, FormattedMessage, SelectLang, useModel } from 'umi';
+import Footer from '@/components/Footer';
+import { login } from '@/services/ant-design-pro/api';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+
+import styles from './index.less';
+
+const LoginMessage: React.FC<{
+  content: string;
+}> = ({ content }) => (
+  <Alert
+    style={{
+      marginBottom: 24,
+    }}
+    message={content}
+    type="error"
+    showIcon
+  />
+);
+
+/** 此方法会跳转到 redirect 参数所在的位置 */
+const goto = () => {
+  if (!history) return;
+  setTimeout(() => {
+    const { query } = history.location;
+    const { redirect } = query as { redirect: string };
+    history.push(redirect || '/');
+  }, 10);
+};
+
+const Login: React.FC = () => {
+  const [submitting, setSubmitting] = useState(false);
+  const [userLoginState, setUserLoginState] = useState<API.LoginResult>({});
+  const [type, setType] = useState<string>('account');
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const intl = useIntl();
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      setInitialState({
+        ...initialState,
+        currentUser: userInfo,
+      });
+    }
+  };
+
+  const handleSubmit = async (values: API.LoginParams) => {
+    setSubmitting(true);
+    try {
+      // 登录
+      const msg = await login({ ...values, type });
+      if (msg.status === 'ok') {
+        const defaultloginSuccessMessage = intl.formatMessage({
+          id: 'pages.login.success',
+          defaultMessage: '登录成功!',
+        });
+        message.success(defaultloginSuccessMessage);
+        await fetchUserInfo();
+        goto();
+        return;
+      }
+      // 如果失败去设置用户错误信息
+      setUserLoginState(msg);
+    } catch (error) {
+      const defaultloginFailureMessage = intl.formatMessage({
+        id: 'pages.login.failure',
+        defaultMessage: '登录失败,请重试!',
+      });
+
+      message.error(defaultloginFailureMessage);
+    }
+    setSubmitting(false);
+  };
+  const { status, type: loginType } = userLoginState;
+
+  return (
+    <div className={styles.container}>
+      <div className={styles.lang} data-lang>
+        {SelectLang && <SelectLang />}
+      </div>
+      <div className={styles.content}>
+        <div className={styles.top}>
+          <div className={styles.header}>
+            <Link to="/">
+              <img alt="logo" className={styles.logo} src="/logo.svg" />
+              <span className={styles.title}>Ant Design</span>
+            </Link>
+          </div>
+          <div className={styles.desc}>
+            {intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
+          </div>
+        </div>
+
+        <div className={styles.main}>
+          <ProForm
+            initialValues={{
+              autoLogin: true,
+            }}
+            submitter={{
+              searchConfig: {
+                submitText: intl.formatMessage({
+                  id: 'pages.login.submit',
+                  defaultMessage: '登录',
+                }),
+              },
+              render: (_, dom) => dom.pop(),
+              submitButtonProps: {
+                loading: submitting,
+                size: 'large',
+                style: {
+                  width: '100%',
+                },
+              },
+            }}
+            onFinish={async (values) => {
+              handleSubmit(values as API.LoginParams);
+            }}
+          >
+            <Tabs activeKey={type} onChange={setType}>
+              <Tabs.TabPane
+                key="account"
+                tab={intl.formatMessage({
+                  id: 'pages.login.accountLogin.tab',
+                  defaultMessage: '账户密码登录',
+                })}
+              />
+              <Tabs.TabPane
+                key="mobile"
+                tab={intl.formatMessage({
+                  id: 'pages.login.phoneLogin.tab',
+                  defaultMessage: '手机号登录',
+                })}
+              />
+            </Tabs>
+
+            {status === 'error' && loginType === 'account' && (
+              <LoginMessage
+                content={intl.formatMessage({
+                  id: 'pages.login.accountLogin.errorMessage',
+                  defaultMessage: '账户或密码错误(admin/ant.design)',
+                })}
+              />
+            )}
+            {type === 'account' && (
+              <>
+                <ProFormText
+                  name="username"
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <UserOutlined className={styles.prefixIcon} />,
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.username.placeholder',
+                    defaultMessage: '用户名: admin or user',
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.username.required"
+                          defaultMessage="请输入用户名!"
+                        />
+                      ),
+                    },
+                  ]}
+                />
+                <ProFormText.Password
+                  name="password"
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <LockOutlined className={styles.prefixIcon} />,
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.password.placeholder',
+                    defaultMessage: '密码: ant.design',
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.password.required"
+                          defaultMessage="请输入密码!"
+                        />
+                      ),
+                    },
+                  ]}
+                />
+              </>
+            )}
+
+            {status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+            {type === 'mobile' && (
+              <>
+                <ProFormText
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <MobileOutlined className={styles.prefixIcon} />,
+                  }}
+                  name="mobile"
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.phoneNumber.placeholder',
+                    defaultMessage: '手机号',
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.phoneNumber.required"
+                          defaultMessage="请输入手机号!"
+                        />
+                      ),
+                    },
+                    {
+                      pattern: /^1\d{10}$/,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.phoneNumber.invalid"
+                          defaultMessage="手机号格式错误!"
+                        />
+                      ),
+                    },
+                  ]}
+                />
+                <ProFormCaptcha
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <LockOutlined className={styles.prefixIcon} />,
+                  }}
+                  captchaProps={{
+                    size: 'large',
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.captcha.placeholder',
+                    defaultMessage: '请输入验证码',
+                  })}
+                  captchaTextRender={(timing, count) => {
+                    if (timing) {
+                      return `${count} ${intl.formatMessage({
+                        id: 'pages.getCaptchaSecondText',
+                        defaultMessage: '获取验证码',
+                      })}`;
+                    }
+                    return intl.formatMessage({
+                      id: 'pages.login.phoneLogin.getVerificationCode',
+                      defaultMessage: '获取验证码',
+                    });
+                  }}
+                  name="captcha"
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.captcha.required"
+                          defaultMessage="请输入验证码!"
+                        />
+                      ),
+                    },
+                  ]}
+                  onGetCaptcha={async (phone) => {
+                    const result = await getFakeCaptcha({
+                      phone,
+                    });
+                    if (result === false) {
+                      return;
+                    }
+                    message.success('获取验证码成功!验证码为:1234');
+                  }}
+                />
+              </>
+            )}
+            <div
+              style={{
+                marginBottom: 24,
+              }}
+            >
+              <ProFormCheckbox noStyle name="autoLogin">
+                <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
+              </ProFormCheckbox>
+              <a
+                style={{
+                  float: 'right',
+                }}
+              >
+                <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
+              </a>
+            </div>
+          </ProForm>
+          <Space className={styles.other}>
+            <FormattedMessage id="pages.login.loginWith" defaultMessage="其他登录方式" />
+            <AlipayCircleOutlined className={styles.icon} />
+            <TaobaoCircleOutlined className={styles.icon} />
+            <WeiboCircleOutlined className={styles.icon} />
+          </Space>
+        </div>
+      </div>
+      <Footer />
+    </div>
+  );
+};
+
+export default Login;

+ 1 - 1
src/service-worker.js

@@ -4,7 +4,7 @@
 /* globals workbox */
 workbox.core.setCacheNameDetails({
   prefix: 'antd-pro',
-  suffix: 'v1',
+  suffix: 'v5',
 });
 // Control all opened tabs ASAP
 workbox.clientsClaim();

+ 83 - 0
src/services/ant-design-pro/api.ts

@@ -0,0 +1,83 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from 'umi';
+
+/** 获取当前的用户 GET /api/currentUser */
+export async function currentUser(options?: { [key: string]: any }) {
+  return request<API.CurrentUser>('/api/currentUser', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
+/** 退出登录接口 POST /api/login/outLogin */
+export async function outLogin(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/api/login/outLogin', {
+    method: 'POST',
+    ...(options || {}),
+  });
+}
+
+/** 登录接口 POST /api/login/account */
+export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
+  return request<API.LoginResult>('/api/login/account', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 GET /api/notices */
+export async function getNotices(options?: { [key: string]: any }) {
+  return request<API.NoticeIconList>('/api/notices', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
+/** 获取规则列表 GET /api/rule */
+export async function rule(
+  params: {
+    // query
+    /** 当前的页码 */
+    current?: number;
+    /** 页面的容量 */
+    pageSize?: number;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.RuleList>('/api/rule', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** 新建规则 PUT /api/rule */
+export async function updateRule(options?: { [key: string]: any }) {
+  return request<API.RuleListItem>('/api/rule', {
+    method: 'PUT',
+    ...(options || {}),
+  });
+}
+
+/** 新建规则 POST /api/rule */
+export async function addRule(options?: { [key: string]: any }) {
+  return request<API.RuleListItem>('/api/rule', {
+    method: 'POST',
+    ...(options || {}),
+  });
+}
+
+/** 删除规则 DELETE /api/rule */
+export async function removeRule(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/api/rule', {
+    method: 'DELETE',
+    ...(options || {}),
+  });
+}

+ 10 - 0
src/services/ant-design-pro/index.ts

@@ -0,0 +1,10 @@
+// @ts-ignore
+/* eslint-disable */
+// API 更新时间:
+// API 唯一标识:
+import * as api from './api';
+import * as login from './login';
+export default {
+  api,
+  login,
+};

+ 21 - 0
src/services/ant-design-pro/login.ts

@@ -0,0 +1,21 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from 'umi';
+
+/** 发送验证码 POST /api/login/captcha */
+export async function getFakeCaptcha(
+  params: {
+    // query
+    /** 手机号 */
+    phone?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.FakeCaptcha>('/api/login/captcha', {
+    method: 'POST',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}

+ 101 - 0
src/services/ant-design-pro/typings.d.ts

@@ -0,0 +1,101 @@
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace API {
+  type CurrentUser = {
+    name?: string;
+    avatar?: string;
+    userid?: string;
+    email?: string;
+    signature?: string;
+    title?: string;
+    group?: string;
+    tags?: { key?: string; label?: string }[];
+    notifyCount?: number;
+    unreadCount?: number;
+    country?: string;
+    access?: string;
+    geographic?: {
+      province?: { label?: string; key?: string };
+      city?: { label?: string; key?: string };
+    };
+    address?: string;
+    phone?: string;
+  };
+
+  type LoginResult = {
+    status?: string;
+    type?: string;
+    currentAuthority?: string;
+  };
+
+  type PageParams = {
+    current?: number;
+    pageSize?: number;
+  };
+
+  type RuleListItem = {
+    key?: number;
+    disabled?: boolean;
+    href?: string;
+    avatar?: string;
+    name?: string;
+    owner?: string;
+    desc?: string;
+    callNo?: number;
+    status?: number;
+    updatedAt?: string;
+    createdAt?: string;
+    progress?: number;
+  };
+
+  type RuleList = {
+    data?: RuleListItem[];
+    /** 列表的内容总数 */
+    total?: number;
+    success?: boolean;
+  };
+
+  type FakeCaptcha = {
+    code?: number;
+    status?: string;
+  };
+
+  type LoginParams = {
+    username?: string;
+    password?: string;
+    autoLogin?: boolean;
+    type?: string;
+  };
+
+  type ErrorResponse = {
+    /** 业务约定的错误码 */
+    errorCode: string;
+    /** 业务上的错误信息 */
+    errorMessage?: string;
+    /** 业务上的请求是否成功 */
+    success?: boolean;
+  };
+
+  type NoticeIconList = {
+    data?: NoticeIconItem[];
+    /** 列表的内容总数 */
+    total?: number;
+    success?: boolean;
+  };
+
+  type NoticeIconItemType = 'notification' | 'message' | 'event';
+
+  type NoticeIconItem = {
+    id?: string;
+    extra?: string;
+    key?: string;
+    read?: boolean;
+    avatar?: string;
+    title?: string;
+    status?: string;
+    datetime?: string;
+    description?: string;
+    type?: NoticeIconItemType;
+  };
+}

+ 0 - 19
src/services/login.ts

@@ -1,19 +0,0 @@
-import request from '@/utils/request';
-
-export type LoginParamsType = {
-  userName: string;
-  password: string;
-  mobile: string;
-  captcha: string;
-};
-
-export async function fakeAccountLogin(params: LoginParamsType) {
-  return request('/api/login/account', {
-    method: 'POST',
-    data: params,
-  });
-}
-
-export async function getFakeCaptcha(mobile: string) {
-  return request(`/api/login/captcha?mobile=${mobile}`);
-}

+ 12 - 0
src/services/swagger/index.ts

@@ -0,0 +1,12 @@
+// @ts-ignore
+/* eslint-disable */
+// API 更新时间:
+// API 唯一标识:
+import * as pet from './pet';
+import * as store from './store';
+import * as user from './user';
+export default {
+  pet,
+  store,
+  user,
+};

+ 166 - 0
src/services/swagger/pet.ts

@@ -0,0 +1,166 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from 'umi';
+
+/** Update an existing pet PUT /pet */
+export async function updatePet(body: API.Pet, options?: { [key: string]: any }) {
+  return request<any>('/pet', {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Add a new pet to the store POST /pet */
+export async function addPet(body: API.Pet, options?: { [key: string]: any }) {
+  return request<any>('/pet', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
+export async function findPetsByStatus(
+  params: {
+    // query
+    /** Status values that need to be considered for filter */
+    status: 'available' | 'pending' | 'sold'[];
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.Pet[]>('/pet/findByStatus', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+
+    ...(options || {}),
+  });
+}
+
+/** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use         tag1, tag2, tag3 for testing. GET /pet/findByTags */
+export async function findPetsByTags(
+  params: {
+    // query
+    /** Tags to filter by */
+    tags: string[];
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.Pet[]>('/pet/findByTags', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+
+    ...(options || {}),
+  });
+}
+
+/** Find pet by ID Returns a single pet GET /pet/${param0} */
+export async function getPetById(
+  params: {
+    // path
+    /** ID of pet to return */
+    petId: number;
+  },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0 } = params;
+  return request<API.Pet>(`/pet/${param0}`, {
+    method: 'GET',
+    params: { ...params },
+
+    ...(options || {}),
+  });
+}
+
+/** Updates a pet in the store with form data POST /pet/${param0} */
+export async function updatePetWithForm(
+  params: {
+    // path
+    /** ID of pet that needs to be updated */
+    petId: number;
+  },
+  body: { name?: string; status?: string },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0 } = params;
+  const formData = new FormData();
+
+  Object.keys(body).forEach((ele) => {
+    const item = (body as any)[ele];
+
+    if (item !== undefined && item !== null) {
+      formData.append(ele, typeof item === 'object' ? JSON.stringify(item) : item);
+    }
+  });
+
+  return request<any>(`/pet/${param0}`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    params: { ...params },
+    data: formData,
+    ...(options || {}),
+  });
+}
+
+/** Deletes a pet DELETE /pet/${param0} */
+export async function deletePet(
+  params: {
+    // header
+    api_key?: string;
+    // path
+    /** Pet id to delete */
+    petId: number;
+  },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0 } = params;
+  return request<any>(`/pet/${param0}`, {
+    method: 'DELETE',
+    params: { ...params },
+    ...(options || {}),
+  });
+}
+
+/** uploads an image POST /pet/${param0}/uploadImage */
+export async function uploadFile(
+  params: {
+    // path
+    /** ID of pet to update */
+    petId: number;
+  },
+  body: { additionalMetadata?: string; file?: string },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0 } = params;
+  const formData = new FormData();
+
+  Object.keys(body).forEach((ele) => {
+    const item = (body as any)[ele];
+
+    if (item !== undefined && item !== null) {
+      formData.append(ele, typeof item === 'object' ? JSON.stringify(item) : item);
+    }
+  });
+
+  return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+    params: { ...params },
+    data: formData,
+    ...(options || {}),
+  });
+}

+ 54 - 0
src/services/swagger/store.ts

@@ -0,0 +1,54 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from 'umi';
+
+/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
+export async function getInventory(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/store/inventory', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
+/** Place an order for a pet POST /store/order */
+export async function placeOrder(body: API.Order, options?: { [key: string]: any }) {
+  return request<API.Order>('/store/order', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10.         Other values will generated exceptions GET /store/order/${param0} */
+export async function getOrderById(
+  params: {
+    // path
+    /** ID of pet that needs to be fetched */
+    orderId: number;
+  },
+  options?: { [key: string]: any },
+) {
+  const { orderId: param0 } = params;
+  return request<API.Order>(`/store/order/${param0}`, {
+    method: 'GET',
+    params: { ...params },
+    ...(options || {}),
+  });
+}
+
+/** Delete purchase order by ID For valid response try integer IDs with positive integer value.         Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
+export async function deleteOrder(
+  params: {
+    // path
+    /** ID of the order that needs to be deleted */
+    orderId: number;
+  },
+  options?: { [key: string]: any },
+) {
+  const { orderId: param0 } = params;
+  return request<any>(`/store/order/${param0}`, {
+    method: 'DELETE',
+    params: { ...params },
+    ...(options || {}),
+  });
+}

+ 52 - 0
src/services/swagger/typings.d.ts

@@ -0,0 +1,52 @@
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace API {
+  type Order = {
+    id?: number;
+    petId?: number;
+    quantity?: number;
+    shipDate?: string;
+    /** Order Status */
+    status?: 'placed' | 'approved' | 'delivered';
+    complete?: boolean;
+  };
+
+  type Category = {
+    id?: number;
+    name?: string;
+  };
+
+  type User = {
+    id?: number;
+    username?: string;
+    firstName?: string;
+    lastName?: string;
+    email?: string;
+    password?: string;
+    phone?: string;
+    /** User Status */
+    userStatus?: number;
+  };
+
+  type Tag = {
+    id?: number;
+    name?: string;
+  };
+
+  type Pet = {
+    id?: number;
+    category?: Category;
+    name: string;
+    photoUrls: string[];
+    tags?: Tag[];
+    /** pet status in the store */
+    status?: 'available' | 'pending' | 'sold';
+  };
+
+  type ApiResponse = {
+    code?: number;
+    type?: string;
+    message?: string;
+  };
+}

+ 114 - 0
src/services/swagger/user.ts

@@ -0,0 +1,114 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from 'umi';
+
+/** Create user This can only be done by the logged in user. POST /user */
+export async function createUser(body: API.User, options?: { [key: string]: any }) {
+  return request<any>('/user', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Creates list of users with given input array POST /user/createWithArray */
+export async function createUsersWithArrayInput(
+  body: API.User[],
+  options?: { [key: string]: any },
+) {
+  return request<any>('/user/createWithArray', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Creates list of users with given input array POST /user/createWithList */
+export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) {
+  return request<any>('/user/createWithList', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Logs user into the system GET /user/login */
+export async function loginUser(
+  params: {
+    // query
+    /** The user name for login */
+    username: string;
+    /** The password for login in clear text */
+    password: string;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<string>('/user/login', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** Logs out current logged in user session GET /user/logout */
+export async function logoutUser(options?: { [key: string]: any }) {
+  return request<any>('/user/logout', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
+/** Get user by user name GET /user/${param0} */
+export async function getUserByName(
+  params: {
+    // path
+    /** The name that needs to be fetched. Use user1 for testing.  */
+    username: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { username: param0 } = params;
+  return request<API.User>(`/user/${param0}`, {
+    method: 'GET',
+    params: { ...params },
+    ...(options || {}),
+  });
+}
+
+/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
+export async function updateUser(
+  params: {
+    // path
+    /** name that need to be updated */
+    username: string;
+  },
+  body: API.User,
+  options?: { [key: string]: any },
+) {
+  const { username: param0 } = params;
+  return request<any>(`/user/${param0}`, {
+    method: 'PUT',
+    params: { ...params },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
+export async function deleteUser(
+  params: {
+    // path
+    /** The name that needs to be deleted */
+    username: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { username: param0 } = params;
+  return request<any>(`/user/${param0}`, {
+    method: 'DELETE',
+    params: { ...params },
+    ...(options || {}),
+  });
+}

+ 0 - 13
src/services/user.ts

@@ -1,13 +0,0 @@
-import request from '@/utils/request';
-
-export async function query(): Promise<any> {
-  return request('/api/users');
-}
-
-export async function queryCurrent(): Promise<any> {
-  return request('/api/currentUser');
-}
-
-export async function queryNotices(): Promise<any> {
-  return request('/api/notices');
-}

+ 0 - 21
src/typings.d.ts

@@ -17,27 +17,6 @@ declare module 'mockjs';
 declare module 'react-fittext';
 declare module 'bizcharts-plugin-slider';
 
-// google analytics interface
-type GAFieldsObject = {
-  eventCategory: string;
-  eventAction: string;
-  eventLabel?: string;
-  eventValue?: number;
-  nonInteraction?: boolean;
-};
-
-interface Window {
-  ga: (
-    command: 'send',
-    hitType: 'event' | 'pageview',
-    fieldsObject: GAFieldsObject | string,
-  ) => void;
-  reloadAuthorized: () => void;
-  routerBase: string;
-}
-
-declare let ga: () => void;
-
 // preview.pro.ant.design only do not use in your production ;
 // preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
 declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;

+ 0 - 16
src/utils/Authorized.ts

@@ -1,16 +0,0 @@
-import RenderAuthorize from '@/components/Authorized';
-import { getAuthority } from './authority';
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let Authorized = RenderAuthorize(getAuthority());
-
-// Reload the rights component
-const reloadAuthorized = (): void => {
-  Authorized = RenderAuthorize(getAuthority());
-};
-
-/** Hard code block need it。 */
-window.reloadAuthorized = reloadAuthorized;
-
-export { reloadAuthorized };
-export default Authorized;

+ 0 - 32
src/utils/authority.ts

@@ -1,32 +0,0 @@
-import { reloadAuthorized } from './Authorized';
-
-// use localStorage to store the authority info, which might be sent from server in actual project.
-export function getAuthority(str?: string): string | string[] {
-  const authorityString =
-    typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str;
-  // authorityString could be admin, "admin", ["admin"]
-  let authority;
-  try {
-    if (authorityString) {
-      authority = JSON.parse(authorityString);
-    }
-  } catch (e) {
-    authority = authorityString;
-  }
-  if (typeof authority === 'string') {
-    return [authority];
-  }
-  // preview.pro.ant.design only do not use in your production.
-  // preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
-  if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
-    return ['admin'];
-  }
-  return authority;
-}
-
-export function setAuthority(authority: string | string[]): void {
-  const proAuthority = typeof authority === 'string' ? [authority] : authority;
-  localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
-  // auto reload
-  reloadAuthorized();
-}

+ 0 - 55
src/utils/request.ts

@@ -1,55 +0,0 @@
-/** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */
-import { extend } from 'umi-request';
-import { notification } from 'antd';
-
-const codeMessage: Record<number, string> = {
-  200: '服务器成功返回请求的数据。',
-  201: '新建或修改数据成功。',
-  202: '一个请求已经进入后台排队(异步任务)。',
-  204: '删除数据成功。',
-  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
-  401: '用户没有权限(令牌、用户名、密码错误)。',
-  403: '用户得到授权,但是访问是被禁止的。',
-  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
-  406: '请求的格式不可得。',
-  410: '请求的资源被永久删除,且不会再得到的。',
-  422: '当创建一个对象时,发生一个验证错误。',
-  500: '服务器发生错误,请检查服务器。',
-  502: '网关错误。',
-  503: '服务不可用,服务器暂时过载或维护。',
-  504: '网关超时。',
-};
-
-/**
- * @zh-CN 异常处理程序
- * @en-US Exception handler
- */
-const errorHandler = (error: { response: Response }): Response => {
-  const { response } = error;
-  if (response && response.status) {
-    const errorText = codeMessage[response.status] || response.statusText;
-    const { status, url } = response;
-
-    notification.error({
-      message: `Request error ${status}: ${url}`,
-      description: errorText,
-    });
-  } else if (!response) {
-    notification.error({
-      description: 'Your network is abnormal and cannot connect to the server',
-      message: 'Network anomaly',
-    });
-  }
-  return response;
-};
-
-/**
- * @en-US Configure the default parameters for request
- * @zh-CN 配置request请求时的默认参数
- */
-const request = extend({
-  errorHandler, // default error handling
-  credentials: 'include', // Does the default request bring cookies
-});
-
-export default request;

+ 0 - 16
src/utils/utils.less

@@ -1,16 +0,0 @@
-// mixins for clearfix
-// ------------------------
-.clearfix() {
-  zoom: 1;
-  &::before,
-  &::after {
-    display: table;
-    content: ' ';
-  }
-  &::after {
-    clear: both;
-    height: 0;
-    font-size: 0;
-    visibility: hidden;
-  }
-}

+ 0 - 37
src/utils/utils.test.ts

@@ -1,37 +0,0 @@
-import { isUrl } from './utils';
-
-describe('isUrl tests', (): void => {
-  it('should return false for invalid and corner case inputs', (): void => {
-    expect(isUrl([] as any)).toBeFalsy();
-    expect(isUrl({} as any)).toBeFalsy();
-    expect(isUrl(false as any)).toBeFalsy();
-    expect(isUrl(true as any)).toBeFalsy();
-    expect(isUrl(NaN as any)).toBeFalsy();
-    expect(isUrl(null as any)).toBeFalsy();
-    expect(isUrl(undefined as any)).toBeFalsy();
-    expect(isUrl('')).toBeFalsy();
-  });
-
-  it('should return false for invalid URLs', (): void => {
-    expect(isUrl('foo')).toBeFalsy();
-    expect(isUrl('bar')).toBeFalsy();
-    expect(isUrl('bar/test')).toBeFalsy();
-    expect(isUrl('http:/example.com/')).toBeFalsy();
-    expect(isUrl('ttp://example.com/')).toBeFalsy();
-  });
-
-  it('should return true for valid URLs', (): void => {
-    expect(isUrl('http://example.com/')).toBeTruthy();
-    expect(isUrl('https://example.com/')).toBeTruthy();
-    expect(isUrl('http://example.com/test/123')).toBeTruthy();
-    expect(isUrl('https://example.com/test/123')).toBeTruthy();
-    expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('http://www.example.com/')).toBeTruthy();
-    expect(isUrl('https://www.example.com/')).toBeTruthy();
-    expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
-    expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
-    expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
-  });
-});

+ 0 - 24
src/utils/utils.ts

@@ -1,24 +0,0 @@
-import { parse } from 'querystring';
-
-/* eslint no-useless-escape:0 import/prefer-default-export:0 */
-const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
-
-export const isUrl = (path: string): boolean => reg.test(path);
-
-export const isAntDesignPro = (): boolean => {
-  if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
-    return true;
-  }
-  return window.location.hostname === 'preview.pro.ant.design';
-};
-
-// For the official demo site, it is used to turn off features that are not needed in the real development environment
-export const isAntDesignProOrDev = (): boolean => {
-  const { NODE_ENV } = process.env;
-  if (NODE_ENV === 'development') {
-    return true;
-  }
-  return isAntDesignPro();
-};
-
-export const getPageQuery = () => parse(window.location.href.split('?')[1]);

+ 3 - 5
tests/run-tests.js

@@ -13,7 +13,7 @@ env.PROGRESS = 'none';
 // flag to prevent multiple test
 let once = false;
 
-const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
+const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'serve'], {
   env,
 });
 
@@ -30,10 +30,7 @@ console.log('Starting development server for e2e tests...');
 startServer.stdout.on('data', (data) => {
   console.log(data.toString());
   // hack code , wait umi
-  if (
-    (!once && data.toString().indexOf('Compiled successfully') >= 0) ||
-    data.toString().indexOf('Theme generated successfully') >= 0
-  ) {
+  if (!once && data.toString().indexOf('Serving your umi project!') >= 0) {
     // eslint-disable-next-line
     once = true;
     console.log('Development server is started, ready to run tests.');
@@ -45,6 +42,7 @@ startServer.stdout.on('data', (data) => {
       },
     );
     testCmd.on('exit', (code) => {
+      console.log(code);
       startServer.kill();
       process.exit(code);
     });

+ 10 - 0
tests/setupTests.js

@@ -0,0 +1,10 @@
+// do some test init
+
+const localStorageMock = {
+  getItem: jest.fn(),
+  setItem: jest.fn(),
+  removeItem: jest.fn(),
+  clear: jest.fn(),
+};
+
+global.localStorage = localStorageMock;