Browse Source

feat: 升级到 antd@5 (#10377)

* add test lib

* add more fix

* prettier

* prettier

* prettier

* fix(test):更好的类型优化

* chore: update lock

* update

* remove e2e

* try remove less

* update to antd@5

* chore: support antd@5

* fix: update snapshot

* fix: update snapshot

* remove  fabric

* feat: request record

* fix: support jest

* fix ci

* fix ci

* fix ci

* fix ci

* fix ci

* fix ci

* fix ci

* fix ci

* remove plugin

Co-authored-by: xiefengnian.xfn <xiefengnian.xfn@antgroup.com>
pull/10440/head
陈帅 3 years ago
committed by GitHub
parent
commit
5177d3382c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .eslintrc.js
  2. 1
      .github/workflows/ci.yml
  3. 27
      .github/workflows/coverage.yml
  4. 4
      .github/workflows/pnpm.yml
  5. 1
      .gitignore
  6. 22
      .prettierrc.js
  7. 7
      config/config.ts
  8. 23
      jest.config.ts
  9. 10
      mock/listTableList.ts
  10. 24
      mock/notices.ts
  11. 324
      mock/requestRecord.mock.js
  12. 44
      package.json
  13. 22
      playwright.config.ts
  14. 16237
      pnpm-lock.yaml
  15. 6
      src/app.tsx
  16. 1
      src/components/Footer/index.tsx
  17. 16
      src/components/HeaderDropdown/index.less
  18. 18
      src/components/HeaderDropdown/index.tsx
  19. 25
      src/components/HeaderSearch/index.less
  20. 101
      src/components/HeaderSearch/index.tsx
  21. 126
      src/components/NoticeIcon/NoticeIcon.tsx
  22. 103
      src/components/NoticeIcon/NoticeList.less
  23. 112
      src/components/NoticeIcon/NoticeList.tsx
  24. 35
      src/components/NoticeIcon/index.less
  25. 152
      src/components/NoticeIcon/index.tsx
  26. 83
      src/components/RightContent/AvatarDropdown.tsx
  27. 82
      src/components/RightContent/index.less
  28. 67
      src/components/RightContent/index.tsx
  29. 267
      src/components/index.md
  30. 44
      src/e2e/baseLayout.e2e.spec.ts
  31. 9
      src/global.less
  32. 2
      src/global.tsx
  33. 4
      src/pages/TableList/components/UpdateForm.tsx
  34. 22
      src/pages/TableList/index.tsx
  35. 1110
      src/pages/User/Login/__snapshots__/login.test.tsx.snap
  36. 48
      src/pages/User/Login/index.less
  37. 91
      src/pages/User/Login/index.tsx
  38. 95
      src/pages/User/Login/login.test.tsx
  39. 30
      src/pages/Welcome.tsx
  40. 2
      src/requestErrorConfig.ts
  41. 4
      src/typings.d.ts
  42. 47
      tests/run-tests.js
  43. 10
      tests/setupTests.js
  44. 64
      tests/setupTests.jsx
  45. 40
      tsconfig.json
  46. 1
      types/cache/cache.json
  47. 386
      types/cache/login.cache.json
  48. 324
      types/cache/mock/login.mock.cache.js
  49. 0
      types/cache/mock/mock.cache.js
  50. 120
      types/index.d.ts

3
.eslintrc.js

@ -1,7 +1,6 @@
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
extends: [require.resolve('@umijs/lint/dist/config/eslint')],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
page: true,
REACT_APP_ENV: true,
},

1
.github/workflows/ci.yml

@ -23,7 +23,6 @@ jobs:
- run: yarn run lint
- run: yarn run tsc
- run: yarn run build
- run: yarn run test:e2e
env:
CI: true
PROGRESS: none

27
.github/workflows/coverage.yml

@ -0,0 +1,27 @@
name: coverage CI
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- run: echo ${{github.ref}}
- run: curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
- run: pnpm config set store-dir ~/.pnpm-store
- run: pnpm install --strict-peer-dependencies=false
- run: yarn run test:coverage
env:
CI: true
PROGRESS: none
NODE_ENV: test
NODE_OPTIONS: --max_old_space_size=4096
- run: bash <(curl -s https://codecov.io/bash)

4
.github/workflows/pnpm.yml

@ -10,7 +10,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node_version: [16.x, 14.x]
node_version: [16.x]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v1
@ -25,7 +25,7 @@ jobs:
- run: pnpm run lint
- run: pnpm run tsc
- run: pnpm run build
- run: pnpm run test:e2e
- run: pnpm run test
env:
CI: true
PROGRESS: none

1
.gitignore

@ -18,7 +18,6 @@ yarn-error.log
.idea
yarn.lock
package-lock.json
pnpm-lock.yaml
*bak
.vscode

22
.prettierrc.js

@ -1,5 +1,21 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
proseWrap: 'never',
endOfLine: 'lf',
overrides: [
{
files: '.prettierrc',
options: {
parser: 'json',
},
},
{
files: 'document.ejs',
options: {
parser: 'html',
},
},
],
};

7
config/config.ts

@ -5,7 +5,7 @@ import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
const { REACT_APP_ENV } = process.env;
const { REACT_APP_ENV = 'dev' } = process.env;
export default defineConfig({
/**
@ -54,7 +54,7 @@ export default defineConfig({
* @doc https://umijs.org/docs/guides/proxy
* @doc https://umijs.org/docs/api/config#proxy
*/
proxy: proxy[REACT_APP_ENV || 'dev'],
proxy: proxy[REACT_APP_ENV as keyof typeof proxy],
/**
* @name
* @description state
@ -131,6 +131,7 @@ export default defineConfig({
},
],
mfsu: {
exclude :['@playwright/test']
strategy: 'normal',
},
requestRecord: {},
});

23
jest.config.ts

@ -0,0 +1,23 @@
import { configUmiAlias, createConfig } from '@umijs/max/test';
export default async () => {
const config = await configUmiAlias({
...createConfig({
target: 'browser',
}),
});
console.log();
return {
...config,
testEnvironmentOptions: {
...(config?.testEnvironmentOptions || {}),
url: 'http://localhost:8000',
},
setupFiles: [...(config.setupFiles || []), './tests/setupTests.jsx'],
globals: {
...config.globals,
localStorage: null,
},
};
};

10
mock/listTableList.ts

@ -52,16 +52,18 @@ function getRule(req: Request, res: Response, u: string) {
const sorter = JSON.parse(params.sorter);
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
(Object.keys(sorter) as Array<keyof API.RuleListItem>).forEach((key) => {
let nextSort = next?.[key] as number;
let preSort = prev?.[key] as number;
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
if (preSort - nextSort > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
if (preSort - nextSort > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
@ -76,7 +78,7 @@ function getRule(req: Request, res: Response, u: string) {
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
return (Object.keys(filter) as Array<keyof API.RuleListItem>).some((key) => {
if (!filter[key]) {
return true;
}

24
mock/notices.ts

@ -5,21 +5,24 @@ const getNotices = (req: Request, res: Response) => {
data: [
{
id: '000000001',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: 'notification',
},
{
id: '000000002',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/hX-PTavYIq4AAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/hX-PTavYIq4AAAAAAAAAAAAAFl94AQBr',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: 'notification',
},
{
id: '000000003',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/jHX5R5l3QjQAAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/jHX5R5l3QjQAAAAAAAAAAAAAFl94AQBr',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
@ -27,21 +30,24 @@ const getNotices = (req: Request, res: Response) => {
},
{
id: '000000004',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Wr4mQqx6jfwAAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Wr4mQqx6jfwAAAAAAAAAAAAAFl94AQBr',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Mzj_TbcWUj4AAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Mzj_TbcWUj4AAAAAAAAAAAAAFl94AQBr',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/eXLzRbPqQE4AAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/eXLzRbPqQE4AAAAAAAAAAAAAFl94AQBr',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
@ -50,7 +56,8 @@ const getNotices = (req: Request, res: Response) => {
},
{
id: '000000007',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/w5mRQY2AmEEAAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/w5mRQY2AmEEAAAAAAAAAAAAAFl94AQBr',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
@ -59,7 +66,8 @@ const getNotices = (req: Request, res: Response) => {
},
{
id: '000000008',
avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/wPadR5M9918AAAAAAAAAAAAAFl94AQBr',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/wPadR5M9918AAAAAAAAAAAAAFl94AQBr',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',

324
mock/requestRecord.mock.js

@ -0,0 +1,324 @@
module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{ key: '0', label: '很有想法的' },
{ key: '1', label: '专注设计' },
{ key: '2', label: '辣~' },
{ key: '3', label: '大长腿' },
{ key: '4', label: '川妹子' },
{ key: '5', label: '海纳百川' },
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: { label: '浙江省', key: '330000' },
city: { label: '杭州市', key: '330100' },
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
},
'GET /api/rule': {
data: [
{
key: 99,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 503,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 98,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 164,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 97,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 174,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 96,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 914,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 7,
},
{
key: 95,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 698,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 94,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 488,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 14,
},
{
key: 93,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 580,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 77,
},
{
key: 92,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 244,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 58,
},
{
key: 91,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 959,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 66,
},
{
key: 90,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 958,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 72,
},
{
key: 89,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 301,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 2,
},
{
key: 88,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 277,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 87,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 810,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 86,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 780,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 22,
},
{
key: 85,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 705,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 84,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 203,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 79,
},
{
key: 83,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 491,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 59,
},
{
key: 82,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 73,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 100,
},
{
key: 81,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 406,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 61,
},
{
key: 80,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 112,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 20,
},
],
total: 100,
success: true,
pageSize: 20,
current: 1,
},
'POST /api/login/outLogin': { data: {}, success: true },
'POST /api/login/account': {
status: 'ok',
type: 'account',
currentAuthority: 'admin',
},
};

44
package.json

@ -1,6 +1,6 @@
{
"name": "ant-design-pro",
"version": "6.0.0-beta.1",
"version": "6.0.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
@ -11,24 +11,28 @@
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
"lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
"openapi": "max openapi",
"playwright": "playwright install && playwright test",
"prepare": "husky install",
"prettier": "prettier -c --write \"src/**/*\"",
"prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
"preview": "npm run build && max preview --port 8000",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"test:e2e": "node ./tests/run-tests.js",
"tsc": "tsc --noEmit"
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
"tsc": "tsc --noEmit",
"record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
@ -43,46 +47,48 @@
],
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-components": "^2.3.13",
"@ant-design/pro-components": "^2.3.37",
"@ant-design/use-emotion-css": "1.0.4",
"@umijs/route-utils": "^2.1.3",
"antd": "^4.23.3",
"antd": "^5.0.0",
"classnames": "^2.3.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"omit.js": "^2.0.2",
"rc-menu": "^9.6.4",
"rc-util": "^5.24.4",
"react": "^17.0.0",
"react": "^18.0.0",
"react-dev-inspector": "^1.8.1",
"react-dom": "^17.0.0",
"react-dom": "^18.0.0",
"react-helmet-async": "^1.3.0"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.1.0",
"@playwright/test": "^1.26.1",
"@testing-library/react": "^13.4.0",
"@types/classnames": "^2.3.1",
"@types/express": "^4.17.14",
"@types/history": "^4.7.11",
"@types/jest": "^29.2.1",
"@types/lodash": "^4.14.186",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.5",
"@umijs/fabric": "^2.11.1",
"@umijs/max": "^4.0.24",
"@umijs/openapi": "^1.7.0",
"@umijs/lint": "^4.0.34",
"@umijs/max": "^4.0.33",
"cross-env": "^7.0.3",
"cross-port-killer": "^1.4.0",
"detect-installer": "^1.0.2",
"eslint": "^7.32.0",
"eslint": "^8.0.0",
"express": "^4.18.2",
"gh-pages": "^3.2.0",
"husky": "^7.0.4",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.2.2",
"lint-staged": "^10.0.0",
"mockjs": "^1.1.0",
"prettier": "^2.7.1",
"swagger-ui-dist": "^4.14.2",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"umi-presets-pro": "^1.0.5",
"umi-serve": "^1.9.11"
"umi-presets-pro": "^2.0.0"
},
"engines": {
"node": ">=12.0.0"

22
playwright.config.ts

@ -1,22 +0,0 @@
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
};
export default config;

16237
pnpm-lock.yaml

File diff suppressed because it is too large

6
src/app.tsx

@ -8,7 +8,7 @@ import { history, Link } from '@umijs/max';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
import React from 'react';
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';
@ -95,12 +95,11 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children, props) => {
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
{!props.location?.pathname?.includes('/login') && (
<SettingDrawer
disableUrlParams
enableDarkTheme
@ -112,7 +111,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
}));
}}
/>
)}
</>
);
},

1
src/components/Footer/index.tsx

@ -1,6 +1,7 @@
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import React from 'react';
const Footer: React.FC = () => {
const intl = useIntl();

16
src/components/HeaderDropdown/index.less

@ -1,16 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
.container > * {
background-color: @popover-bg;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.container {
width: 100% !important;
}
.container > * {
border-radius: 0 !important;
}
}

18
src/components/HeaderDropdown/index.tsx

@ -1,21 +1,29 @@
import { Dropdown } from 'antd';
import type { DropDownProps } from 'antd/es/dropdown';
import classNames from 'classnames';
import React from 'react';
import styles from './index.less';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import classNames from 'classnames';
export type HeaderDropdownProps = {
overlayClassName?: string;
overlay: React.ReactNode | (() => React.ReactNode) | any;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => {
const className = useEmotionCss(({ token }) => {
return {
[`@media screen and (max-width: ${token.screenXS})`]: {
width: '100%',
},
};
});
return (
<Dropdown
overlayClassName={classNames(styles.container, cls)}
overlayClassName={classNames(className, cls)}
getPopupContainer={(target) => target.parentElement || document.body}
{...restProps}
/>
);
};
export default HeaderDropdown;

25
src/components/HeaderSearch/index.less

@ -1,25 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
.headerSearch {
display: inline-flex;
align-items: center;
.input {
width: 0;
min-width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
transition: width 0.3s, margin-left 0.3s;
:global(.ant-select-selection) {
background: transparent;
}
input {
box-shadow: none !important;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
}

101
src/components/HeaderSearch/index.tsx

@ -1,101 +0,0 @@
import { SearchOutlined } from '@ant-design/icons';
import type { InputRef } from 'antd';
import { AutoComplete, Input } from 'antd';
import type { AutoCompleteProps } from 'antd/es/auto-complete';
import classNames from 'classnames';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import React, { useRef } from 'react';
import styles from './index.less';
export type HeaderSearchProps = {
onSearch?: (value?: string) => void;
onChange?: (value?: string) => void;
onVisibleChange?: (b: boolean) => void;
className?: string;
placeholder?: string;
options: AutoCompleteProps['options'];
defaultVisible?: boolean;
visible?: boolean;
defaultValue?: string;
value?: string;
};
const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
const {
className,
defaultValue,
onVisibleChange,
placeholder,
visible,
defaultVisible,
...restProps
} = props;
const inputRef = useRef<InputRef | null>(null);
const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
value: props.value,
onChange: props.onChange,
});
const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
value: props.visible,
onChange: onVisibleChange,
});
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<div
className={classNames(className, styles.headerSearch)}
onClick={() => {
setSearchMode(true);
if (inputRef.current) {
inputRef.current.focus();
}
}}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
if (onVisibleChange) {
onVisibleChange(searchMode);
}
}
}}
>
<SearchOutlined
key="Icon"
style={{
cursor: 'pointer',
}}
/>
<AutoComplete
key="AutoComplete"
className={inputClass}
value={value}
options={restProps.options}
onChange={(completeValue) => setValue(completeValue)}
>
<Input
size="small"
ref={inputRef}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (restProps.onSearch) {
restProps.onSearch(value);
}
}
}}
onBlur={() => {
setSearchMode(false);
}}
/>
</AutoComplete>
</div>
);
};
export default HeaderSearch;

126
src/components/NoticeIcon/NoticeIcon.tsx

@ -1,126 +0,0 @@
import { BellOutlined } from '@ant-design/icons';
import { Badge, Spin, Tabs } from 'antd';
import classNames from 'classnames';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import React from 'react';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
import type { NoticeIconTabProps } from './NoticeList';
import NoticeList from './NoticeList';
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;

103
src/components/NoticeIcon/NoticeList.less

@ -1,103 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
.list {
max-height: 400px;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
.item {
padding-right: 24px;
padding-left: 24px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
.meta {
width: 100%;
}
.avatar {
margin-top: 4px;
background: @component-background;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
margin-bottom: 8px;
font-weight: normal;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: @line-height-base;
}
.extra {
float: right;
margin-top: -1.5px;
margin-right: 0;
color: @text-color-secondary;
font-weight: normal;
}
}
.loadMore {
padding: 8px 0;
color: @primary-6;
text-align: center;
cursor: pointer;
&.loadedAll {
color: rgba(0, 0, 0, 0.25);
cursor: unset;
}
}
}
.notFound {
padding: 73px 0 88px;
color: @text-color-secondary;
text-align: center;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
.bottomBar {
height: 46px;
color: @text-color;
line-height: 46px;
text-align: center;
border-top: 1px solid @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
transition: all 0.3s;
div {
display: inline-block;
width: 50%;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:only-child {
width: 100%;
}
&:not(:only-child):last-child {
border-left: 1px solid @border-color-split;
}
}
}

112
src/components/NoticeIcon/NoticeList.tsx

@ -1,112 +0,0 @@
import { Avatar, List } from 'antd';
import classNames from 'classnames';
import React from 'react';
import styles from './NoticeList.less';
export type NoticeIconTabProps = {
count?: number;
showClear?: boolean;
showViewMore?: boolean;
style?: React.CSSProperties;
title: string;
tabKey: API.NoticeIconItemType;
onClick?: (item: API.NoticeIconItem) => void;
onClear?: () => void;
emptyText?: string;
clearText?: string;
viewMoreText?: string;
list: API.NoticeIconItem[];
onViewMore?: (e: any) => void;
};
const NoticeList: React.FC<NoticeIconTabProps> = ({
list = [],
onClick,
onClear,
title,
onViewMore,
emptyText,
showClear = true,
clearText,
viewMoreText,
showViewMore = false,
}) => {
if (!list || list.length === 0) {
return (
<div className={styles.notFound}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt="not found"
/>
<div>{emptyText}</div>
</div>
);
}
return (
<div>
<List<API.NoticeIconItem>
className={styles.list}
dataSource={list}
renderItem={(item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
<Avatar className={styles.avatar} src={item.avatar} />
) : (
<span className={styles.iconElement}>{item.avatar}</span>
)
) : null;
return (
<div
onClick={() => {
onClick?.(item);
}}
>
<List.Item className={itemCls} key={item.key || i}>
<List.Item.Meta
className={styles.meta}
avatar={leftIcon}
title={
<div className={styles.title}>
{item.title}
<div className={styles.extra}>{item.extra}</div>
</div>
}
description={
<div>
<div className={styles.description}>{item.description}</div>
<div className={styles.datetime}>{item.datetime}</div>
</div>
}
/>
</List.Item>
</div>
);
}}
/>
<div className={styles.bottomBar}>
{showClear ? (
<div onClick={onClear}>
{clearText} {title}
</div>
) : null}
{showViewMore ? (
<div
onClick={(e) => {
if (onViewMore) {
onViewMore(e);
}
}}
>
{viewMoreText}
</div>
) : null}
</div>
</div>
);
};
export default NoticeList;

35
src/components/NoticeIcon/index.less

@ -1,35 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
.popover {
position: relative;
width: 336px;
}
.noticeButton {
display: inline-block;
cursor: pointer;
transition: all 0.3s;
}
.icon {
padding: 4px;
vertical-align: middle;
}
.badge {
font-size: 16px;
}
.tabs {
:global {
.ant-tabs-nav-list {
margin: auto;
}
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-nav {
margin-bottom: 0;
}
}
}

152
src/components/NoticeIcon/index.tsx

@ -1,152 +0,0 @@
import { getNotices } from '@/services/ant-design-pro/api';
import { useModel, useRequest } from '@umijs/max';
import { message, Tag } from 'antd';
import { groupBy } from 'lodash';
import moment from 'moment';
import { useEffect, useState } from 'react';
import styles from './index.less';
import NoticeIcon from './NoticeIcon';
export type GlobalHeaderRightProps = {
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onNoticeClear?: (tabName?: string) => void;
};
const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => {
if (!notices || notices.length === 0 || !Array.isArray(notices)) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime as string).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = {
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newNotice.status];
newNotice.extra = (
<Tag
color={color}
style={{
marginRight: 0,
}}
>
{newNotice.extra}
</Tag>
) as any;
}
return newNotice;
});
return groupBy(newNotices, 'type');
};
const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => {
const unreadMsg: Record<string, number> = {};
Object.keys(noticeData).forEach((key) => {
const value = noticeData[key];
if (!unreadMsg[key]) {
unreadMsg[key] = 0;
}
if (Array.isArray(value)) {
unreadMsg[key] = value.filter((item) => !item.read).length;
}
});
return unreadMsg;
};
const NoticeIconView: React.FC = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const [notices, setNotices] = useState<API.NoticeIconItem[]>([]);
const { data } = useRequest(getNotices);
useEffect(() => {
setNotices(data || []);
}, [data]);
const noticeData = getNoticeData(notices);
const unreadMsg = getUnreadData(noticeData || {});
const changeReadState = (id: string) => {
setNotices(
notices.map((item) => {
const notice = { ...item };
if (notice.id === id) {
notice.read = true;
}
return notice;
}),
);
};
const clearReadState = (title: string, key: string) => {
setNotices(
notices.map((item) => {
const notice = { ...item };
if (notice.type === key) {
notice.read = true;
}
return notice;
}),
);
message.success(`${'清空了'} ${title}`);
};
return (
<NoticeIcon
className={styles.action}
count={currentUser && currentUser.unreadCount}
onItemClick={(item) => {
changeReadState(item.id!);
}}
onClear={(title: string, key: string) => clearReadState(title, key)}
loading={false}
clearText="清空"
viewMoreText="查看更多"
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
tabKey="notification"
count={unreadMsg.notification}
list={noticeData.notification}
title="通知"
emptyText="你已查看所有通知"
showViewMore
/>
<NoticeIcon.Tab
tabKey="message"
count={unreadMsg.message}
list={noticeData.message}
title="消息"
emptyText="您已读完所有消息"
showViewMore
/>
<NoticeIcon.Tab
tabKey="event"
title="待办"
emptyText="你已完成所有待办"
count={unreadMsg.event}
list={noticeData.event}
showViewMore
/>
</NoticeIcon>
);
};
export default NoticeIconView;

83
src/components/RightContent/AvatarDropdown.tsx

@ -1,19 +1,59 @@
import { outLogin } from '@/services/ant-design-pro/api';
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { history, useModel } from '@umijs/max';
import { Avatar, Menu, Spin } from 'antd';
import type { ItemType } from 'antd/es/menu/hooks/useItems';
import { Avatar, Spin } from 'antd';
import { setAlpha } from '@ant-design/pro-components';
import { stringify } from 'querystring';
import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react';
import { flushSync } from 'react-dom';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export type GlobalHeaderRightProps = {
menu?: boolean;
};
const Name = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const nameClassName = useEmotionCss(({ token }) => {
return {
width: '70px',
height: '48px',
overflow: 'hidden',
lineHeight: '48px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
[`@media only screen and (max-width: ${token.screenMD}px)`]: {
display: 'none',
},
};
});
return <span className={`${nameClassName} anticon`}>{currentUser?.name}</span>;
};
const AvatarLogo = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const avatarClassName = useEmotionCss(({ token }) => {
return {
marginRight: '8px',
color: token.colorPrimary,
verticalAlign: 'top',
background: setAlpha(token.colorBgContainer, 0.85),
[`@media only screen and (max-width: ${token.screenMD}px)`]: {
margin: 0,
},
};
});
return <Avatar size="small" className={avatarClassName} src={currentUser?.avatar} alt="avatar" />;
};
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
/**
* 退 url
@ -34,6 +74,21 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
});
}
};
const actionClassName = useEmotionCss(({ token }) => {
return {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick = useCallback(
@ -52,7 +107,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
);
const loading = (
<span className={`${styles.action} ${styles.account}`}>
<span className={actionClassName}>
<Spin
size="small"
style={{
@ -73,7 +128,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
return loading;
}
const menuItems: ItemType[] = [
const menuItems = [
...(menu
? [
{
@ -98,15 +153,17 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
},
];
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick} items={menuItems} />
);
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>
<HeaderDropdown
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
>
<span className={actionClassName}>
<AvatarLogo />
<Name />
</span>
</HeaderDropdown>
);

82
src/components/RightContent/index.less

@ -1,82 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.name {
width: 70px;
height: 48px;
overflow: hidden;
line-height: 48px;
white-space: nowrap;
text-overflow: ellipsis;
}
.action {
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> span {
vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
}
@media only screen and (max-width: @screen-md) {
:global(.ant-divider-vertical) {
vertical-align: unset;
}
.name {
display: none;
}
.right {
position: absolute;
top: 0;
right: 12px;
.account {
.avatar {
margin-right: 0;
}
}
.search {
display: none;
}
}
}

67
src/components/RightContent/index.tsx

@ -1,53 +1,48 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { SelectLang, useModel } from '@umijs/max';
import { Space } from 'antd';
import React from 'react';
import HeaderSearch from '../HeaderSearch';
import Avatar from './AvatarDropdown';
import styles from './index.less';
export type SiderTheme = 'light' | 'dark';
const GlobalHeaderRight: React.FC = () => {
const className = useEmotionCss(() => {
return {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
gap: 8,
};
});
const actionClassName = useEmotionCss(({ token }) => {
return {
display: 'flex',
float: 'right',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
cursor: 'pointer',
padding: '0 12px',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
const { initialState } = useModel('@@initialState');
if (!initialState || !initialState.settings) {
return null;
}
const { navTheme, layout } = initialState.settings;
let className = styles.right;
if ((navTheme === 'realDark' && 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);
// }}
/>
<div className={className}>
<span
className={styles.action}
className={actionClassName}
onClick={() => {
window.open('https://pro.ant.design/docs/getting-started');
}}
@ -55,8 +50,8 @@ const GlobalHeaderRight: React.FC = () => {
<QuestionCircleOutlined />
</span>
<Avatar />
<SelectLang className={styles.action} />
</Space>
<SelectLang className={actionClassName} />
</div>
);
};
export default GlobalHeaderRight;

267
src/components/index.md

@ -1,267 +0,0 @@
---
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 Footer from '@/components/Footer';
export default () => <Footer />;
```
## HeaderDropdown 头部下拉列表
HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
```tsx
/**
* background: '#f0f2f5'
*/
import HeaderDropdown from '@/components/HeaderDropdown';
import { Button, Menu } from 'antd';
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 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 NoticeIcon from '@/components/NoticeIcon/NoticeIcon';
import { message } from 'antd';
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 type 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>
```

44
src/e2e/baseLayout.e2e.spec.ts

@ -1,44 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
const RouterConfig = require('../../config/routes').default;
const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
function formatter(routes: any, parentPath = ''): string[] {
const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
let result: string[] = [];
routes.forEach((item: { path: string; routes: string }) => {
if (item.path && !item.path.startsWith('/')) {
result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.path && item.path.startsWith('/')) {
result.push(`${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.routes) {
result = result.concat(
formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
);
}
});
return [...new Set(result.filter((item) => !!item))];
}
const testPage = (path: string, page: Page) => async () => {
await page.evaluate(() => {
localStorage.setItem('antd-pro-authority', '["admin"]');
});
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(() => document.getElementsByTagName('footer').length > 0);
expect(haveFooter).toBeTruthy();
};
const routers = formatter(RouterConfig);
routers.forEach((route) => {
test(`test route page ${route}`, async ({ page }) => {
await testPage(route, page);
});
});

9
src/global.less

@ -1,9 +1,12 @@
@import '~antd/es/style/variable.less';
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}
.colorWeak {
@ -32,7 +35,7 @@ ol {
list-style: none;
}
@media (max-width: @screen-xs) {
@media (max-width: 768px) {
.ant-table {
width: 100%;
overflow-x: auto;

2
src/global.tsx

@ -58,7 +58,7 @@ if (pwa) {
<Button
type="primary"
onClick={() => {
notification.close(key);
notification.destroy(key);
reloadSW();
}}
>

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

@ -21,7 +21,7 @@ export type FormValueType = {
export type UpdateFormProps = {
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
onSubmit: (values: FormValueType) => Promise<void>;
updateModalVisible: boolean;
updateModalOpen: boolean;
values: Partial<API.RuleListItem>;
};
@ -42,7 +42,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
id: 'pages.searchTable.updateForm.ruleConfig',
defaultMessage: '规则配置',
})}
visible={props.updateModalVisible}
open={props.updateModalOpen}
footer={submitter}
onCancel={() => {
props.onCancel();

22
src/pages/TableList/index.tsx

@ -88,12 +88,12 @@ const TableList: React.FC = () => {
* @en-US Pop-up window of new window
* @zh-CN
* */
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
const [createModalOpen, handleModalOpen] = useState<boolean>(false);
/**
* @en-US The pop-up window of the distribution update window
* @zh-CN
* */
const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [showDetail, setShowDetail] = useState<boolean>(false);
@ -225,7 +225,7 @@ const TableList: React.FC = () => {
<a
key="config"
onClick={() => {
handleUpdateModalVisible(true);
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
@ -258,7 +258,7 @@ const TableList: React.FC = () => {
type="primary"
key="primary"
onClick={() => {
handleModalVisible(true);
handleModalOpen(true);
}}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
@ -317,12 +317,12 @@ const TableList: React.FC = () => {
defaultMessage: 'New rule',
})}
width="400px"
visible={createModalVisible}
onVisibleChange={handleModalVisible}
open={createModalOpen}
onOpenChange={handleModalOpen}
onFinish={async (value) => {
const success = await handleAdd(value as API.RuleListItem);
if (success) {
handleModalVisible(false);
handleModalOpen(false);
if (actionRef.current) {
actionRef.current.reload();
}
@ -350,7 +350,7 @@ const TableList: React.FC = () => {
onSubmit={async (value) => {
const success = await handleUpdate(value);
if (success) {
handleUpdateModalVisible(false);
handleUpdateModalOpen(false);
setCurrentRow(undefined);
if (actionRef.current) {
actionRef.current.reload();
@ -358,18 +358,18 @@ const TableList: React.FC = () => {
}
}}
onCancel={() => {
handleUpdateModalVisible(false);
handleUpdateModalOpen(false);
if (!showDetail) {
setCurrentRow(undefined);
}
}}
updateModalVisible={updateModalVisible}
updateModalOpen={updateModalOpen}
values={currentRow || {}}
/>
<Drawer
width={600}
visible={showDetail}
open={showDetail}
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);

1110
src/pages/User/Login/__snapshots__/login.test.tsx.snap

File diff suppressed because it is too large

48
src/pages/User/Login/index.less

@ -1,48 +0,0 @@
@import (reference) '~antd/es/style/themes/index';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr');
background-size: cover;
}
.content {
padding: 32px 0 24px;
}
}
.icon {
margin-left: 8px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}

91
src/pages/User/Login/index.tsx

@ -15,11 +15,57 @@ import {
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { FormattedMessage, history, SelectLang, useIntl, useModel } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import styles from './index.less';
const ActionIcons = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
marginLeft: '8px',
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '24px',
verticalAlign: 'middle',
cursor: 'pointer',
transition: 'color 0.3s',
'&:hover': {
color: token.colorPrimaryActive,
},
};
});
return (
<>
<AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
<WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
</>
);
};
const Lang = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
width: 42,
height: 42,
lineHeight: '42px',
position: 'fixed',
right: 16,
borderRadius: token.borderRadius,
':hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
return (
<div className={langClassName} data-lang>
{SelectLang && <SelectLang />}
</div>
);
};
const LoginMessage: React.FC<{
content: string;
@ -41,6 +87,18 @@ const Login: React.FC = () => {
const [type, setType] = useState<string>('account');
const { initialState, setInitialState } = useModel('@@initialState');
const containerClassName = useEmotionCss(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
};
});
const intl = useIntl();
const fetchUserInfo = async () => {
@ -85,12 +143,19 @@ const Login: React.FC = () => {
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={containerClassName}>
<Lang />
<div
style={{
flex: '1',
padding: '32px 0',
}}
>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" src="/logo.svg" />}
title="Ant Design"
subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
@ -103,9 +168,7 @@ const Login: React.FC = () => {
id="pages.login.loginWith"
defaultMessage="其他登录方式"
/>,
<AlipayCircleOutlined key="AlipayCircleOutlined" className={styles.icon} />,
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={styles.icon} />,
<WeiboCircleOutlined key="WeiboCircleOutlined" className={styles.icon} />,
<ActionIcons key="icons" />,
]}
onFinish={async (values) => {
await handleSubmit(values as API.LoginParams);
@ -147,7 +210,7 @@ const Login: React.FC = () => {
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
prefix: <UserOutlined />,
}}
placeholder={intl.formatMessage({
id: 'pages.login.username.placeholder',
@ -169,7 +232,7 @@ const Login: React.FC = () => {
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
prefix: <LockOutlined />,
}}
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
@ -196,7 +259,7 @@ const Login: React.FC = () => {
<ProFormText
fieldProps={{
size: 'large',
prefix: <MobileOutlined className={styles.prefixIcon} />,
prefix: <MobileOutlined />,
}}
name="mobile"
placeholder={intl.formatMessage({
@ -227,7 +290,7 @@ const Login: React.FC = () => {
<ProFormCaptcha
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
prefix: <LockOutlined />,
}}
captchaProps={{
size: 'large',
@ -264,7 +327,7 @@ const Login: React.FC = () => {
const result = await getFakeCaptcha({
phone,
});
if (result === false) {
if (!result) {
return;
}
message.success('获取验证码成功!验证码为:1234');

95
src/pages/User/Login/login.test.tsx

@ -0,0 +1,95 @@
import { render, fireEvent, act } from '@testing-library/react';
import React from 'react';
import { TestBrowser } from '@@/testBrowser';
import { startMock } from '@@/requestRecordMock';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
let server: {
close: () => void;
};
describe('Login Page', () => {
beforeAll(async () => {
server = await startMock({
port: 8000,
scene: 'login',
});
});
afterAll(() => {
server?.close();
});
it('should show login form', async () => {
const historyRef = React.createRef<any>();
const rootContainer = render(
<TestBrowser
historyRef={historyRef}
location={{
pathname: '/user/login',
}}
/>,
);
await rootContainer.findAllByText('Ant Design');
act(() => {
historyRef.current?.push('/user/login');
});
expect(rootContainer.baseElement?.querySelector('.ant-pro-form-login-desc')?.textContent).toBe(
'Ant Design is the most influential web design specification in Xihu district',
);
expect(rootContainer.asFragment()).toMatchSnapshot();
rootContainer.unmount();
});
it('should login success', async () => {
const historyRef = React.createRef<any>();
const rootContainer = render(
<TestBrowser
historyRef={historyRef}
location={{
pathname: '/user/login',
}}
/>,
);
await rootContainer.findAllByText('Ant Design');
const userNameInput = await rootContainer.findByPlaceholderText('Username: admin or user');
act(() => {
fireEvent.change(userNameInput, { target: { value: 'admin' } });
});
const passwordInput = await rootContainer.findByPlaceholderText('Password: ant.design');
act(() => {
fireEvent.change(passwordInput, { target: { value: 'ant.design' } });
});
await (await rootContainer.findByText('Login')).click();
// 等待接口返回结果
await waitTime(5000);
await rootContainer.findAllByText('Ant Design Pro');
expect(rootContainer.asFragment()).toMatchSnapshot();
await waitTime(2000);
rootContainer.unmount();
});
});

30
src/pages/Welcome.tsx

@ -1,5 +1,6 @@
import { PageContainer } from '@ant-design/pro-components';
import { Card } from 'antd';
import { useModel } from '@umijs/max';
import { Card, theme } from 'antd';
import React from 'react';
/**
@ -13,17 +14,21 @@ const InfoCard: React.FC<{
desc: string;
href: string;
}> = ({ title, href, index, desc }) => {
const { useToken } = theme;
const { token } = useToken();
return (
<div
style={{
backgroundColor: '#FFFFFF',
boxShadow: '0 2px 4px 0 rgba(35,49,128,0.02), 0 4px 8px 0 rgba(49,69,179,0.02)',
backgroundColor: token.colorBgContainer,
boxShadow: token.boxShadow,
borderRadius: '8px',
fontSize: '14px',
color: 'rgba(0,0,0,0.65)',
textAlign: 'justify',
color: token.colorTextSecondary,
lineHeight: '22px',
padding: '16px 19px',
minWidth: '220px',
flex: 1,
}}
>
@ -53,7 +58,7 @@ const InfoCard: React.FC<{
<div
style={{
fontSize: '16px',
color: 'rgba(0, 0, 0, 0.85)',
color: token.colorText,
paddingBottom: 8,
}}
>
@ -63,7 +68,7 @@ const InfoCard: React.FC<{
<div
style={{
fontSize: '14px',
color: 'rgba(0,0,0,0.65)',
color: token.colorTextSecondary,
textAlign: 'justify',
lineHeight: '22px',
marginBottom: 8,
@ -79,6 +84,8 @@ const InfoCard: React.FC<{
};
const Welcome: React.FC = () => {
const { token } = theme.useToken();
const { initialState } = useModel('@@initialState');
return (
<PageContainer>
<Card
@ -87,7 +94,9 @@ const Welcome: React.FC = () => {
}}
bodyStyle={{
backgroundImage:
'radial-gradient(circle at 97% 10%, #EBF2FF 0%, #F5F8FF 28%, #EBF1FF 124%)',
initialState?.settings?.navTheme === 'realDark'
? 'background-image: linear-gradient(75deg, #1A1B1F 0%, #191C1F 100%)'
: 'background-image: linear-gradient(75deg, #FBFDFF 0%, #F5F7FF 100%)',
}}
>
<div
@ -102,7 +111,7 @@ const Welcome: React.FC = () => {
<div
style={{
fontSize: '20px',
color: '#1A1A1A',
color: token.colorTextHeading,
}}
>
使 Ant Design Pro
@ -110,7 +119,7 @@ const Welcome: React.FC = () => {
<p
style={{
fontSize: '14px',
color: 'rgba(0,0,0,0.65)',
color: token.colorTextSecondary,
lineHeight: '22px',
marginTop: 16,
marginBottom: 32,
@ -123,6 +132,7 @@ const Welcome: React.FC = () => {
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 16,
}}
>

2
src/requestErrorConfig.ts

@ -51,7 +51,7 @@ export const errorConfig: RequestConfig = {
// do nothing
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
message.warning(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);

4
src/typings.d.ts

@ -17,8 +17,4 @@ declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';
// preview.pro.ant.design only do not use in your production ;
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

47
tests/run-tests.js

@ -1,47 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { spawn } = require('child_process');
const { kill } = require('cross-port-killer');
const env = Object.create(process.env);
env.BROWSER = 'none';
env.TEST = true;
env.UMI_UI = 'none';
env.PROGRESS = 'none';
// flag to prevent multiple test
let once = false;
const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'serve'], {
env,
});
startServer.stderr.on('data', (data) => {
// eslint-disable-next-line
console.log(data.toString());
});
startServer.on('exit', () => {
kill(process.env.PORT || 8000);
});
console.log('Starting development server for e2e tests...');
startServer.stdout.on('data', (data) => {
console.log(data.toString());
// hack code , wait umi
if (!once && data.toString().indexOf('Serving your umi project!') >= 0) {
// eslint-disable-next-line
once = true;
console.log('Development server is started, ready to run tests.');
const testCmd = spawn(
/^win/.test(process.platform) ? 'npm.cmd' : 'npm',
['run', 'playwright'],
{
stdio: 'inherit',
},
);
testCmd.on('exit', (code) => {
console.log('服务已经退出,退出码:', code);
startServer.kill();
process.exit(code);
});
}
});

10
tests/setupTests.js

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

64
tests/setupTests.jsx

@ -0,0 +1,64 @@
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
Object.defineProperty(URL, 'createObjectURL', {
writable: true,
value: jest.fn(),
});
class Worker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}
postMessage(msg) {
this.onmessage(msg);
}
}
window.Worker = Worker;
/* eslint-disable global-require */
if (typeof window !== 'undefined') {
// ref: https://github.com/ant-design/ant-design/issues/18774
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn(() => ({
matches: false,
addListener: jest.fn(),
removeListener: jest.fn(),
})),
});
}
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn((query) => ({
matches: query.includes('max-width'),
addListener: jest.fn(),
removeListener: jest.fn(),
})),
});
}
}
const errorLog = console.error;
Object.defineProperty(global.window.console, 'error', {
writable: true,
configurable: true,
value: (...rest) => {
const logStr = rest.join('');
if (logStr.includes('Warning: An update to %s inside a test was not wrapped in act(...)')) {
return;
}
errorLog(...rest);
},
});

40
tsconfig.json

@ -1,41 +1,23 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "esnext",
"lib": ["esnext", "dom"],
"sourceMap": true,
"baseUrl": ".",
"jsx": "react-jsx",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"module": "esnext",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"allowJs": true,
"importHelpers": true,
"jsx": "preserve",
"esModuleInterop": true,
"sourceMap": true,
"baseUrl": "./",
"skipLibCheck": true,
"experimentalDecorators": true,
"strict": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": ["./src/*"],
"@@/*": ["./src/.umi/*"]
"@@/*": ["./src/.umi/*"],
"@@test/*": ["./src/.umi-test/*"]
}
},
"include": [
"mock/**/*",
"src/**/*",
"playwright.config.ts",
"tests/**/*",
"test/**/*",
"__test__/**/*",
"typings/**/*",
"config/**/*",
".eslintrc.js",
".prettierrc.js",
"jest.config.js",
"mock/*"
],
"exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"]
}

1
types/cache/cache.json

@ -0,0 +1 @@
{}

386
types/cache/login.cache.json

File diff suppressed because one or more lines are too long

324
types/cache/mock/login.mock.cache.js

@ -0,0 +1,324 @@
module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{ key: '0', label: '很有想法的' },
{ key: '1', label: '专注设计' },
{ key: '2', label: '辣~' },
{ key: '3', label: '大长腿' },
{ key: '4', label: '川妹子' },
{ key: '5', label: '海纳百川' },
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: { label: '浙江省', key: '330000' },
city: { label: '杭州市', key: '330100' },
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
},
'GET /api/rule': {
data: [
{
key: 99,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 503,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 98,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 164,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 97,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 174,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 96,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 914,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 7,
},
{
key: 95,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 698,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 94,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 488,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 14,
},
{
key: 93,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 580,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 77,
},
{
key: 92,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 244,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 58,
},
{
key: 91,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 959,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 66,
},
{
key: 90,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 958,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 72,
},
{
key: 89,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 301,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 2,
},
{
key: 88,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 277,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 87,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 810,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 86,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 780,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 22,
},
{
key: 85,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 705,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 84,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 203,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 79,
},
{
key: 83,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 491,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 59,
},
{
key: 82,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 73,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 100,
},
{
key: 81,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 406,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 61,
},
{
key: 80,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 112,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 20,
},
],
total: 100,
success: true,
pageSize: 20,
current: 1,
},
'POST /api/login/outLogin': { data: {}, success: true },
'POST /api/login/account': {
status: 'ok',
type: 'account',
currentAuthority: 'admin',
},
};

0
types/cache/mock/mock.cache.js

120
types/index.d.ts

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save