Browse Source

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

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

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

* 🎨 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>
pull/8660/head
陈帅 5 years ago
committed by GitHub
parent
commit
a2d434f023
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/ci.yml
  2. 7
      .github/workflows/deploy.yml
  3. 6
      .github/workflows/preview-build.yml
  4. 2
      .github/workflows/preview-deploy.yml
  5. 14
      .github/workflows/rebase.yml
  6. 2
      README.md
  7. 4
      config/config.dev.ts
  8. 35
      config/config.ts
  9. 20
      config/defaultSettings.ts
  10. 593
      config/oneapi.json
  11. 85
      config/routes.ts
  12. 1
      jest.config.js
  13. 27
      mock/listTableList.ts
  14. 196
      mock/notices.ts
  15. 139
      mock/user.ts
  16. 52
      package.json
  17. BIN
      public/home_bg.png
  18. 0
      public/logo.svg
  19. 9
      src/access.ts
  20. 136
      src/app.tsx
  21. 35
      src/components/Authorized/Authorized.tsx
  22. 33
      src/components/Authorized/AuthorizedRoute.tsx
  23. 88
      src/components/Authorized/CheckPermissions.tsx
  24. 96
      src/components/Authorized/PromiseRender.tsx
  25. 80
      src/components/Authorized/Secured.tsx
  26. 11
      src/components/Authorized/index.tsx
  27. 31
      src/components/Authorized/renderAuthorize.ts
  28. 37
      src/components/Footer/index.tsx
  29. 88
      src/components/GlobalHeader/AvatarDropdown.tsx
  30. 168
      src/components/GlobalHeader/NoticeIconView.tsx
  31. 83
      src/components/GlobalHeader/RightContent.tsx
  32. 11
      src/components/HeaderSearch/index.less
  33. 18
      src/components/HeaderSearch/index.tsx
  34. 126
      src/components/NoticeIcon/NoticeIcon.tsx
  35. 19
      src/components/NoticeIcon/NoticeList.tsx
  36. 262
      src/components/NoticeIcon/index.tsx
  37. 5
      src/components/PageLoading/index.tsx
  38. 103
      src/components/RightContent/AvatarDropdown.tsx
  39. 28
      src/components/RightContent/index.less
  40. 62
      src/components/RightContent/index.tsx
  41. 272
      src/components/index.md
  42. 1
      src/e2e/__mocks__/antd-pro-merge-less.js
  43. 8
      src/e2e/baseLayout.e2e.js
  44. 3
      src/global.less
  45. 3
      src/global.tsx
  46. 183
      src/layouts/BasicLayout.tsx
  47. 10
      src/layouts/BlankLayout.tsx
  48. 58
      src/layouts/SecurityLayout.tsx
  49. 70
      src/layouts/UserLayout.tsx
  50. 3
      src/locales/en-US.ts
  51. 4
      src/locales/en-US/pages.ts
  52. 24
      src/locales/fa-IR.ts
  53. 5
      src/locales/fa-IR/component.ts
  54. 17
      src/locales/fa-IR/globalHeader.ts
  55. 52
      src/locales/fa-IR/menu.ts
  56. 67
      src/locales/fa-IR/pages.ts
  57. 7
      src/locales/fa-IR/pwa.ts
  58. 32
      src/locales/fa-IR/settingDrawer.ts
  59. 60
      src/locales/fa-IR/settings.ts
  60. 2
      src/locales/pt-BR.ts
  61. 70
      src/locales/pt-BR/pages.ts
  62. 1
      src/locales/zh-CN.ts
  63. 4
      src/locales/zh-CN/pages.ts
  64. 30
      src/models/connect.d.ts
  65. 126
      src/models/global.ts
  66. 93
      src/models/login.ts
  67. 38
      src/models/setting.ts
  68. 85
      src/models/user.ts
  69. 6
      src/pages/TableList/components/UpdateForm.tsx
  70. 36
      src/pages/TableList/data.d.ts
  71. 29
      src/pages/TableList/index.tsx
  72. 38
      src/pages/TableList/service.ts
  73. 44
      src/pages/User/login/index.less
  74. 263
      src/pages/User/login/index.tsx
  75. 1
      src/pages/document.ejs
  76. 43
      src/pages/user/Login/index.less
  77. 318
      src/pages/user/Login/index.tsx
  78. 2
      src/service-worker.js
  79. 83
      src/services/ant-design-pro/api.ts
  80. 10
      src/services/ant-design-pro/index.ts
  81. 21
      src/services/ant-design-pro/login.ts
  82. 101
      src/services/ant-design-pro/typings.d.ts
  83. 19
      src/services/login.ts
  84. 12
      src/services/swagger/index.ts
  85. 166
      src/services/swagger/pet.ts
  86. 54
      src/services/swagger/store.ts
  87. 52
      src/services/swagger/typings.d.ts
  88. 114
      src/services/swagger/user.ts
  89. 13
      src/services/user.ts
  90. 21
      src/typings.d.ts
  91. 16
      src/utils/Authorized.ts
  92. 32
      src/utils/authority.ts
  93. 55
      src/utils/request.ts
  94. 16
      src/utils/utils.less
  95. 37
      src/utils/utils.test.ts
  96. 24
      src/utils/utils.ts
  97. 8
      tests/run-tests.js
  98. 10
      tests/setupTests.js

4
.github/workflows/ci.yml

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

7
.github/workflows/deploy.yml

@ -12,10 +12,13 @@ jobs:
uses: actions/checkout@master uses: actions/checkout@master
- name: install - name: install
run: npm install run: yarn
- name: plugins - 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 - name: site
run: npm run site run: npm run site

6
.github/workflows/preview-build.yml

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

2
.github/workflows/preview-deploy.yml

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

14
.github/workflows/rebase.yml

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

2
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 - FAQ: http://pro.ant.design/docs/faq
- Mirror Site in China: http://ant-design-pro.gitee.io - 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) [Try Ant Design Pro 5.0.0](https://beta-pro.ant.design/docs/upgrade-v5-cn)

4
config/config.dev.ts

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

35
config/config.ts

@ -1,5 +1,7 @@
// https://umijs.org/config/ // https://umijs.org/config/
import { defineConfig } from 'umi'; import { defineConfig } from 'umi';
import { join } from 'path';
import defaultSettings from './defaultSettings'; import defaultSettings from './defaultSettings';
import proxy from './proxy'; import proxy from './proxy';
import routes from './routes'; import routes from './routes';
@ -12,9 +14,13 @@ export default defineConfig({
dva: { dva: {
hmr: true, hmr: true,
}, },
history: { layout: {
type: 'browser', // https://umijs.org/zh-CN/plugins/plugin-layout
locale: true,
siderWidth: 208,
...defaultSettings,
}, },
// https://umijs.org/zh-CN/plugins/plugin-locale
locale: { locale: {
// default zh-CN // default zh-CN
default: 'zh-CN', default: 'zh-CN',
@ -23,7 +29,7 @@ export default defineConfig({
baseNavigator: true, baseNavigator: true,
}, },
dynamicImport: { dynamicImport: {
loading: '@/components/PageLoading/index', loading: '@ant-design/pro-layout/es/PageLoading',
}, },
targets: { targets: {
ie: 11, ie: 11,
@ -34,14 +40,33 @@ export default defineConfig({
theme: { theme: {
'primary-color': defaultSettings.primaryColor, 'primary-color': defaultSettings.primaryColor,
}, },
// esbuild is father build tools
// https://umijs.org/plugins/plugin-esbuild
esbuild: {},
title: false, title: false,
ignoreMomentLocale: true, ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'], proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: { manifest: {
basePath: '/', basePath: '/',
}, },
// 快速刷新功能 https://umijs.org/config#fastrefresh // Fast Refresh 热更新
fastRefresh: {}, 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: {}, webpack5: {},
exportStatic: {},
}); });

20
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> & { const Settings: LayoutSettings & {
pwa: boolean; pwa?: boolean;
}; logo?: string;
} = {
const proSettings: DefaultSettings = { navTheme: 'light',
navTheme: 'dark',
// 拂晓蓝 // 拂晓蓝
primaryColor: '#1890ff', primaryColor: '#1890ff',
layout: 'side', layout: 'mix',
contentWidth: 'Fluid', contentWidth: 'Fluid',
fixedHeader: false, fixedHeader: false,
fixSiderbar: true, fixSiderbar: true,
colorWeak: false, colorWeak: false,
title: 'Ant Design Pro', title: 'Ant Design Pro',
pwa: false, pwa: false,
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
iconfontUrl: '', iconfontUrl: '',
}; };
export type { DefaultSettings }; export default Settings;
export default proSettings;

593
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"
}
}
}
}
}
}

85
config/routes.ts

@ -1,72 +1,51 @@
export default [ export default [
{ {
path: '/', path: '/user',
component: '../layouts/BlankLayout', layout: false,
routes: [ routes: [
{ {
path: '/user', path: '/user',
component: '../layouts/UserLayout',
routes: [ routes: [
{ {
name: 'login', name: 'login',
path: '/user/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: '/', path: '/admin/sub-page',
component: '../layouts/SecurityLayout', name: 'sub-page',
routes: [ icon: 'smile',
{ component: './Welcome',
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',
},
],
}, },
], ],
}, },
{
name: 'list.table-list',
icon: 'table',
path: '/list',
component: './TableList',
},
{
path: '/',
redirect: '/welcome',
},
{ {
component: './404', component: './404',
}, },

1
jest.config.js

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

27
mock/listTableList.ts

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

196
mock/notices.ts

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

139
mock/user.ts

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

52
package.json

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

BIN
public/home_bg.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

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

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

9
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
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,
};
};

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;

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;

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;

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

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;

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;

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
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,
},
]}
/>
);
};

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

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

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

11
src/components/HeaderSearch/index.less

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

18
src/components/HeaderSearch/index.tsx

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

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

19
src/components/NoticeIcon/NoticeList.tsx

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

262
src/components/NoticeIcon/index.tsx

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

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

28
src/components/GlobalHeader/index.less → src/components/RightContent/index.less

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

62
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
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>
```

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

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

8
src/e2e/baseLayout.e2e.js

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

3
src/global.less

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

3
src/global.tsx

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

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

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;

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

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

3
src/locales/en-US.ts

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

4
src/locales/en-US/pages.ts

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

24
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
src/locales/fa-IR/component.ts

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

17
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
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
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
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
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
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
src/locales/pt-BR.ts

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

70
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
src/locales/zh-CN.ts

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

4
src/locales/zh-CN/pages.ts

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

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;

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;

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;

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;

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;

6
src/pages/TableList/components/UpdateForm.tsx

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

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

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

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',
},
});
}

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

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

1
src/pages/document.ejs

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

43
src/layouts/UserLayout.less → src/pages/user/Login/index.less

@ -69,3 +69,46 @@
color: @text-color-secondary; color: @text-color-secondary;
font-size: @font-size-base; 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
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;

2
src/service-worker.js

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

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

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
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
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
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
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
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 || {}),
});
}

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');
}

21
src/typings.d.ts

@ -17,27 +17,6 @@ declare module 'mockjs';
declare module 'react-fittext'; declare module 'react-fittext';
declare module 'bizcharts-plugin-slider'; 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 only do not use in your production ;
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project. // 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; declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;

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;

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();
}

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;

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

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();
});
});

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

8
tests/run-tests.js

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

10
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;
Loading…
Cancel
Save