xbx 2 rokov pred
commit
abfb928344
60 zmenil súbory, kde vykonal 3486 pridanie a 0 odobranie
  1. 16 0
      .editorconfig
  2. 7 0
      .eslintignore
  3. 8 0
      .eslintrc.js
  4. 42 0
      .gitignore
  5. 7 0
      .husky/commit-msg
  6. 4 0
      .husky/pre-commit
  7. 22 0
      .prettierignore
  8. 5 0
      .prettierrc.js
  9. 57 0
      README.md
  10. 107 0
      config/config.ts
  11. 24 0
      config/defaultSettings.ts
  12. 39 0
      config/proxy.ts
  13. 21 0
      config/routes.ts
  14. 11 0
      jsconfig.json
  15. 86 0
      package.json
  16. 22 0
      playwright.config.ts
  17. 1 0
      public/CNAME
  18. BIN
      public/favicon.ico
  19. BIN
      public/icons/icon-128x128.png
  20. BIN
      public/icons/icon-192x192.png
  21. BIN
      public/icons/icon-512x512.png
  22. 1 0
      public/logo.svg
  23. 5 0
      public/pro_icon.svg
  24. 9 0
      src/access.ts
  25. 104 0
      src/app.tsx
  26. 16 0
      src/components/Footer/index.tsx
  27. 16 0
      src/components/HeaderDropdown/index.less
  28. 21 0
      src/components/HeaderDropdown/index.tsx
  29. 25 0
      src/components/HeaderSearch/index.less
  30. 101 0
      src/components/HeaderSearch/index.tsx
  31. 126 0
      src/components/NoticeIcon/NoticeIcon.tsx
  32. 103 0
      src/components/NoticeIcon/NoticeList.less
  33. 112 0
      src/components/NoticeIcon/NoticeList.tsx
  34. 35 0
      src/components/NoticeIcon/index.less
  35. 152 0
      src/components/NoticeIcon/index.tsx
  36. 115 0
      src/components/RightContent/AvatarDropdown.tsx
  37. 82 0
      src/components/RightContent/index.less
  38. 59 0
      src/components/RightContent/index.tsx
  39. 267 0
      src/components/index.md
  40. 44 0
      src/e2e/baseLayout.e2e.spec.ts
  41. 50 0
      src/global.less
  42. 92 0
      src/global.tsx
  43. 22 0
      src/manifest.json
  44. 18 0
      src/pages/404.tsx
  45. 44 0
      src/pages/Admin.tsx
  46. 155 0
      src/pages/TableList/components/UpdateForm.tsx
  47. 329 0
      src/pages/TableList/index.tsx
  48. 48 0
      src/pages/User/Login/index.less
  49. 226 0
      src/pages/User/Login/index.tsx
  50. 154 0
      src/pages/Welcome.tsx
  51. 94 0
      src/requestErrorConfig.ts
  52. 65 0
      src/service-worker.js
  53. 85 0
      src/services/ant-design-pro/api.ts
  54. 10 0
      src/services/ant-design-pro/index.ts
  55. 21 0
      src/services/ant-design-pro/login.ts
  56. 82 0
      src/services/ant-design-pro/typings.d.ts
  57. 23 0
      src/typings.d.ts
  58. 47 0
      tests/run-tests.js
  59. 10 0
      tests/setupTests.js
  60. 39 0
      tsconfig.json

+ 16 - 0
.editorconfig

@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab

+ 7 - 0
.eslintignore

@@ -0,0 +1,7 @@
+/lambda/
+/scripts
+/config
+.history
+public
+dist
+.umi

+ 8 - 0
.eslintrc.js

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

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+**/node_modules
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+_roadhog-api-doc
+
+# production
+/dist
+
+# misc
+.DS_Store
+npm-debug.log*
+yarn-error.log
+
+/coverage
+.idea
+yarn.lock
+package-lock.json
+pnpm-lock.yaml
+*bak
+.vscode
+
+
+# visual studio code
+.history
+*.log
+functions/*
+.temp/**
+
+# umi
+.umi
+.umi-production
+.umi-test
+
+# screenshot
+screenshot
+.firebase
+.eslintcache
+
+build

+ 7 - 0
.husky/commit-msg

@@ -0,0 +1,7 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+# Export Git hook params
+export GIT_PARAMS=$*
+
+npx --no-install fabric verify-commit

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install lint-staged

+ 22 - 0
.prettierignore

@@ -0,0 +1,22 @@
+**/*.svg
+.umi
+.umi-production
+/dist
+.dockerignore
+.DS_Store
+.eslintignore
+*.png
+*.toml
+docker
+.editorconfig
+Dockerfile*
+.gitignore
+.prettierignore
+LICENSE
+.eslintcache
+*.lock
+yarn-error.log
+.history
+CNAME
+/build
+/public

+ 5 - 0
.prettierrc.js

@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+  ...fabric.prettier,
+};

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# Ant Design Pro
+
+This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
+
+## Environment Prepare
+
+Install `node_modules`:
+
+```bash
+npm install
+```
+
+or
+
+```bash
+yarn
+```
+
+## Provided Scripts
+
+Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
+
+Scripts provided in `package.json`. It's safe to modify or add additional script:
+
+### Start project
+
+```bash
+npm start
+```
+
+### Build project
+
+```bash
+npm run build
+```
+
+### Check code style
+
+```bash
+npm run lint
+```
+
+You can also use script to auto fix some lint error:
+
+```bash
+npm run lint:fix
+```
+
+### Test code
+
+```bash
+npm test
+```
+
+## More
+
+You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

+ 107 - 0
config/config.ts

@@ -0,0 +1,107 @@
+// https://umijs.org/config/
+import { defineConfig } from '@umijs/max';
+import { join } from 'path';
+import defaultSettings from './defaultSettings';
+import proxy from './proxy';
+import routes from './routes';
+const { REACT_APP_ENV } = process.env;
+export default defineConfig({
+  /**
+   * @name 开启 hash 模式
+   * @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。
+   * @doc https://umijs.org/docs/api/config#hash
+   */
+  hash: true,
+  /**
+   * @name 兼容性设置
+   * @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖
+   * @doc https://umijs.org/docs/api/config#targets
+   */
+  // targets: {
+  //   ie: 11,
+  // },
+  /**
+   * @name 路由的配置,不在路由中引入的文件不会编译
+   * @description 只支持 path,component,routes,redirect,wrappers,title 的配置
+   * @doc https://umijs.org/docs/guides/routes
+   */
+  // umi routes: https://umijs.org/docs/routing
+  routes,
+  /**
+   * @name 主题的配置
+   * @description 虽然叫主题,但是其实只是 less 的变量设置
+   * @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn
+   * @doc umi 的theme 配置 https://umijs.org/docs/api/config#theme
+   */
+  theme: {
+    // 如果不想要 configProvide 动态设置主题需要把这个设置为 default
+    // 只有设置为 variable, 才能使用 configProvide 动态设置主色调
+    'root-entry-name': 'variable',
+  },
+  /**
+   * @name moment 的国际化配置
+   * @description 如果对国际化没有要求,打开之后能减少js的包大小
+   * @doc https://umijs.org/docs/api/config#ignoremomentlocale
+   */
+  ignoreMomentLocale: true,
+  /**
+   * @name 代理配置
+   * @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了
+   * @see 要注意以下 代理只能在本地开发时使用,build 之后就无法使用了。
+   * @doc 代理介绍 https://umijs.org/docs/guides/proxy
+   * @doc 代理配置 https://umijs.org/docs/api/config#proxy
+   */
+  proxy: proxy[REACT_APP_ENV || 'dev'],
+  /**
+   * @name 快速热更新配置
+   * @description 一个不错的热更新组件,更新时可以保留 state
+   */
+  fastRefresh: true,
+  //============== 以下都是max的插件配置 ===============
+  /**
+   * @name 数据流插件
+   * @@doc https://umijs.org/docs/max/data-flow
+   */
+  model: {},
+  /**
+   * 一个全局的初始数据流,可以用它在插件之间共享数据
+   * @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。
+   * @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
+   */
+  initialState: {},
+  /**
+   * @name layout 插件
+   * @doc https://umijs.org/docs/max/layout-menu
+   */
+  layout: {
+    locale: false,
+    ...defaultSettings,
+  },
+  /**
+   * @name antd 插件
+   * @description 内置了 babel import 插件
+   * @doc https://umijs.org/docs/max/antd#antd
+   */
+  antd: {
+    import: true,
+    style: 'less',
+  },
+  /**
+   * @name 网络请求配置
+   * @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
+   * @doc https://umijs.org/docs/max/request
+   */
+  request: {},
+  /**
+   * @name 权限插件
+   * @description 基于 initialState 的权限插件,必须先打开 initialState
+   * @doc https://umijs.org/docs/max/access
+   */
+  access: {},
+  //================ pro 插件配置 =================
+  presets: ['umi-presets-pro'],
+  mfsu: {
+    exclude: ['@playwright/test'],
+  },
+  
+});

+ 24 - 0
config/defaultSettings.ts

@@ -0,0 +1,24 @@
+import { Settings as LayoutSettings } from '@ant-design/pro-components';
+
+/**
+ * @name
+ */
+const Settings: LayoutSettings & {
+  pwa?: boolean;
+  logo?: string;
+} = {
+  navTheme: 'light',
+  // 拂晓蓝
+  colorPrimary: '#1890ff',
+  layout: 'mix',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  fixSiderbar: true,
+  colorWeak: false,
+  title: '快应用管理后台',
+  pwa: false,
+  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
+  iconfontUrl: '',
+};
+
+export default Settings;

+ 39 - 0
config/proxy.ts

@@ -0,0 +1,39 @@
+/**
+ * @name 代理的配置
+ * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
+ * -------------------------------
+ * The agent cannot take effect in the production environment
+ * so there is no configuration of the production environment
+ * For details, please see
+ * https://pro.ant.design/docs/deploy
+ *
+ * @doc https://umijs.org/docs/guides/proxy
+ */
+export default {
+  dev: {
+    '/api/': {
+      target: 'https://localhost:8080',
+      changeOrigin: true,
+      pathRewrite: { '^': '' },
+    },
+  },
+  /**
+   * @name 详细的代理配置
+   * @doc https://github.com/chimurai/http-proxy-middleware
+   */
+  test: {
+    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
+    '/api/': {
+      target: 'https://proapi.azurewebsites.net',
+      changeOrigin: true,
+      pathRewrite: { '^': '' },
+    },
+  },
+  pre: {
+    '/api/': {
+      target: 'your pre url',
+      changeOrigin: true,
+      pathRewrite: { '^': '' },
+    },
+  },
+};

+ 21 - 0
config/routes.ts

@@ -0,0 +1,21 @@
+export default [
+  {
+    path: '/user',
+    layout: false,
+    routes: [{ name: '登录', path: '/user/login', component: './User/Login' }],
+  },
+  { path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome' },
+  {
+    path: '/admin',
+    name: '管理页',
+    icon: 'crown',
+    access: 'canAdmin',
+    routes: [
+      { path: '/admin', redirect: '/admin/sub-page' },
+      { path: '/admin/sub-page', name: '二级管理页', component: './Admin' },
+    ],
+  },
+  { name: '查询表格', icon: 'table', path: '/list', component: './TableList' },
+  { path: '/', redirect: '/welcome' },
+  { path: '*', layout: false, component: './404' },
+];

+ 11 - 0
jsconfig.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 86 - 0
package.json

@@ -0,0 +1,86 @@
+{
+  "name": "ant-design-pro",
+  "version": "6.0.0-beta.1",
+  "private": true,
+  "description": "An out-of-box UI solution for enterprise applications",
+  "scripts": {
+    "analyze": "cross-env ANALYZE=1 max build",
+    "build": "max build",
+    "deploy": "npm run build && npm run gh-pages",
+    "dev": "npm run start:dev",
+    "gh-pages": "gh-pages -d dist",
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+    "postinstall": "max setup",
+    "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",
+    "playwright": "playwright install && playwright test",
+    "prepare": "husky install",
+    "prettier": "prettier -c --write \"src/**/*\"",
+    "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: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"
+  },
+  "lint-staged": {
+    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write"
+    ]
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ],
+  "dependencies": {
+    "@ant-design/icons": "^4.7.0",
+    "@ant-design/pro-components": "^2.3.13",
+    "@umijs/route-utils": "^2.1.3",
+    "antd": "^4.23.3",
+    "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-dev-inspector": "^1.8.1",
+    "react-dom": "^17.0.0",
+    "react-helmet-async": "^1.3.0"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^2.1.0",
+    "@playwright/test": "^1.26.1",
+    "@types/classnames": "^2.3.1",
+    "@types/express": "^4.17.14",
+    "@types/history": "^4.7.11",
+    "@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",
+    "cross-env": "^7.0.3",
+    "cross-port-killer": "^1.4.0",
+    "detect-installer": "^1.0.2",
+    "eslint": "^7.32.0",
+    "gh-pages": "^3.2.0",
+    "husky": "^7.0.4",
+    "lint-staged": "^10.0.0",
+    "prettier": "^2.7.1",
+    "swagger-ui-dist": "^4.14.2",
+    "typescript": "^4.8.4",
+    "umi-presets-pro": "^1.0.5",
+    "umi-serve": "^1.9.11"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  }
+}

+ 22 - 0
playwright.config.ts

@@ -0,0 +1,22 @@
+// 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;

+ 1 - 0
public/CNAME

@@ -0,0 +1 @@
+preview.pro.ant.design

BIN
public/favicon.ico


BIN
public/icons/icon-128x128.png


BIN
public/icons/icon-192x192.png


BIN
public/icons/icon-512x512.png


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
public/logo.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5 - 0
public/pro_icon.svg


+ 9 - 0
src/access.ts

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

+ 104 - 0
src/app.tsx

@@ -0,0 +1,104 @@
+import Footer from '@/components/Footer';
+import RightContent from '@/components/RightContent';
+import { LinkOutlined } from '@ant-design/icons';
+import type { Settings as LayoutSettings } from '@ant-design/pro-components';
+import { WaterMark } from '@ant-design/pro-components';
+import type { RunTimeLayoutConfig } from '@umijs/max';
+import { history } from '@umijs/max';
+import defaultSettings from '../config/defaultSettings';
+import { errorConfig } from './requestErrorConfig';
+//import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
+
+//const isDev = process.env.NODE_ENV === 'development';
+const loginPath = '/user/login';
+
+/**
+ * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
+ * */
+export async function getInitialState(): Promise<{
+  settings?: Partial<LayoutSettings>;
+  // currentUser?: API.CurrentUser;
+  loading?: boolean;
+  //fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
+}> {
+  /*  const fetchUserInfo = async () => {
+    try {
+      const msg = await queryCurrentUser({
+        skipErrorHandler: true,
+      });
+      return msg.data;
+    } catch (error) {
+      history.push(loginPath);
+    }
+    return undefined;
+  }; */
+  // 如果不是登录页面,执行
+  /* if (window.location.pathname !== loginPath) {
+    //const currentUser = await fetchUserInfo();
+    const currentUser = {
+
+    }
+    return {
+      fetchUserInfo,
+      currentUser,
+      settings: defaultSettings,
+    };
+  } */
+  return {
+    settings: defaultSettings,
+  };
+}
+
+export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
+  return {
+    rightContentRender: () => <RightContent />,
+    waterMarkProps: {
+      //content: initialState?.currentUser?.name,
+    },
+    footerRender: () => <Footer />,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      /*  if (!initialState?.currentUser && location.pathname !== loginPath) {
+        history.push(loginPath);
+      } */
+    },
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    // 增加一个 loading 的状态
+    childrenRender: (children, props) => {
+      // if (initialState?.loading) return <PageLoading />;
+      console.log(props)
+      return (
+        <>
+          <WaterMark content="盛钟旋">{children}</WaterMark>
+
+          {/* {!props.location?.pathname?.includes('/login') && (
+            <SettingDrawer
+              disableUrlParams
+              enableDarkTheme
+              settings={initialState?.settings}
+              onSettingChange={(settings) => {
+                setInitialState((preInitialState) => ({
+                  ...preInitialState,
+                  settings,
+                }));
+              }}
+            />
+          )} */}
+        </>
+      );
+    },
+    ...initialState?.settings,
+  };
+};
+
+/**
+ * @name request 配置,可以配置错误处理
+ * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+export const request = {
+  ...errorConfig,
+};

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

@@ -0,0 +1,16 @@
+
+import { DefaultFooter } from '@ant-design/pro-components';
+import '@umijs/max';
+const Footer: React.FC = () => {
+  const defaultMessage = '快应用管理后台';
+  const currentYear = new Date().getFullYear();
+  return (
+    <DefaultFooter
+      style={{
+        background: 'none',
+      }}
+      copyright={`${currentYear} ${defaultMessage}`}
+    />
+  );
+};
+export default Footer;

+ 16 - 0
src/components/HeaderDropdown/index.less

@@ -0,0 +1,16 @@
+@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;
+  }
+}

+ 21 - 0
src/components/HeaderDropdown/index.tsx

@@ -0,0 +1,21 @@
+import { Dropdown } from 'antd';
+import type { DropDownProps } from 'antd/es/dropdown';
+import classNames from 'classnames';
+import React from 'react';
+import styles from './index.less';
+
+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 }) => (
+  <Dropdown
+    overlayClassName={classNames(styles.container, cls)}
+    getPopupContainer={(target) => target.parentElement || document.body}
+    {...restProps}
+  />
+);
+
+export default HeaderDropdown;

+ 25 - 0
src/components/HeaderSearch/index.less

@@ -0,0 +1,25 @@
+@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 - 0
src/components/HeaderSearch/index.tsx

@@ -0,0 +1,101 @@
+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 - 0
src/components/NoticeIcon/NoticeIcon.tsx

@@ -0,0 +1,126 @@
+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 - 0
src/components/NoticeIcon/NoticeList.less

@@ -0,0 +1,103 @@
+@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 - 0
src/components/NoticeIcon/NoticeList.tsx

@@ -0,0 +1,112 @@
+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 - 0
src/components/NoticeIcon/index.less

@@ -0,0 +1,35 @@
+@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 - 0
src/components/NoticeIcon/index.tsx

@@ -0,0 +1,152 @@
+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;

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

@@ -0,0 +1,115 @@
+import { outLogin } from '@/services/ant-design-pro/api';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { history, useModel } from '@umijs/max';
+import { Avatar, Menu, Spin } from 'antd';
+import type { ItemType } from 'antd/es/menu/hooks/useItems';
+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 AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+  /**
+   * 退出登录,并且将当前的 url 保存
+   */
+  const loginOut = async () => {
+    await outLogin();
+    const { search, pathname } = window.location;
+    const urlParams = new URL(window.location.href).searchParams;
+    /** 此方法会跳转到 redirect 参数所在的位置 */
+    const redirect = urlParams.get('redirect');
+    // Note: There may be security issues, please note
+    if (window.location.pathname !== '/user/login' && !redirect) {
+      history.replace({
+        pathname: '/user/login',
+        search: stringify({
+          redirect: pathname + search,
+        }),
+      });
+    }
+  };
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const onMenuClick = useCallback(
+    (event: MenuInfo) => {
+      const { key } = event;
+      if (key === 'logout') {
+        flushSync(() => {
+          setInitialState((s) => ({ ...s, currentUser: undefined }));
+        });
+        loginOut();
+        return;
+      }
+      history.push(`/account/${key}`);
+    },
+    [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 menuItems: ItemType[] = [
+    ...(menu
+      ? [
+          {
+            key: 'center',
+            icon: <UserOutlined />,
+            label: '个人中心',
+          },
+          {
+            key: 'settings',
+            icon: <SettingOutlined />,
+            label: '个人设置',
+          },
+          {
+            type: 'divider' as const,
+          },
+        ]
+      : []),
+    {
+      key: 'logout',
+      icon: <LogoutOutlined />,
+      label: '退出登录',
+    },
+  ];
+
+  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>
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default AvatarDropdown;

+ 82 - 0
src/components/RightContent/index.less

@@ -0,0 +1,82 @@
+@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;
+    }
+  }
+}

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

@@ -0,0 +1,59 @@
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { 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 { 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);
+        // }}
+      />
+      <span
+        className={styles.action}
+        onClick={() => {
+          window.open('https://pro.ant.design/docs/getting-started');
+        }}
+      >
+        <QuestionCircleOutlined />
+      </span>
+      <Avatar />
+    </Space>
+  );
+};
+export default GlobalHeaderRight;

+ 267 - 0
src/components/index.md

@@ -0,0 +1,267 @@
+---
+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 - 0
src/e2e/baseLayout.e2e.spec.ts

@@ -0,0 +1,44 @@
+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);
+  });
+});

+ 50 - 0
src/global.less

@@ -0,0 +1,50 @@
+@import '~antd/es/style/variable.less';
+
+html,
+body,
+#root {
+  height: 100%;
+}
+
+.colorWeak {
+  filter: invert(80%);
+}
+
+.ant-layout {
+  min-height: 100vh;
+}
+.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
+  left: unset;
+}
+
+canvas {
+  display: block;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+  list-style: none;
+}
+
+@media (max-width: @screen-xs) {
+  .ant-table {
+    width: 100%;
+    overflow-x: auto;
+    &-thead > tr,
+    &-tbody > tr {
+      > th,
+      > td {
+        white-space: pre;
+        > span {
+          display: block;
+        }
+      }
+    }
+  }
+}

+ 92 - 0
src/global.tsx

@@ -0,0 +1,92 @@
+import '@umijs/max';
+import { Button, message, notification } from 'antd';
+import defaultSettings from '../config/defaultSettings';
+const { pwa } = defaultSettings;
+const isHttps = document.location.protocol === 'https:';
+const clearCache = () => {
+  // remove all caches
+  if (window.caches) {
+    caches
+      .keys()
+      .then((keys) => {
+        keys.forEach((key) => {
+          caches.delete(key);
+        });
+      })
+      .catch((e) => console.log(e));
+  }
+};
+
+// if pwa is true
+if (pwa) {
+  // Notify user if offline now
+  window.addEventListener('sw.offline', () => {
+    message.warning('当前处于离线状态');
+  });
+
+  // Pop up a prompt on the page asking the user if they want to use the latest version
+  window.addEventListener('sw.updated', (event: Event) => {
+    const e = event as CustomEvent;
+    const reloadSW = async () => {
+      // Check if there is sw whose state is waiting in ServiceWorkerRegistration
+      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
+      const worker = e.detail && e.detail.waiting;
+      if (!worker) {
+        return true;
+      }
+      // Send skip-waiting event to waiting SW with MessageChannel
+      await new Promise((resolve, reject) => {
+        const channel = new MessageChannel();
+        channel.port1.onmessage = (msgEvent) => {
+          if (msgEvent.data.error) {
+            reject(msgEvent.data.error);
+          } else {
+            resolve(msgEvent.data);
+          }
+        };
+        worker.postMessage(
+          {
+            type: 'skip-waiting',
+          },
+          [channel.port2],
+        );
+      });
+      clearCache();
+      window.location.reload();
+      return true;
+    };
+    const key = `open${Date.now()}`;
+    const btn = (
+      <Button
+        type="primary"
+        onClick={() => {
+          notification.close(key);
+          reloadSW();
+        }}
+      >
+        {'刷新'}
+      </Button>
+    );
+    notification.open({
+      message: '有新内容',
+      description: '请点击“刷新”按钮或者手动刷新页面',
+      btn,
+      key,
+      onClose: async () => null,
+    });
+  });
+} else if ('serviceWorker' in navigator && isHttps) {
+  // unregister service worker
+  const { serviceWorker } = navigator;
+  if (serviceWorker.getRegistrations) {
+    serviceWorker.getRegistrations().then((sws) => {
+      sws.forEach((sw) => {
+        sw.unregister();
+      });
+    });
+  }
+  serviceWorker.getRegistration().then((sw) => {
+    if (sw) sw.unregister();
+  });
+  clearCache();
+}

+ 22 - 0
src/manifest.json

@@ -0,0 +1,22 @@
+{
+  "name": "Ant Design Pro",
+  "short_name": "Ant Design Pro",
+  "display": "standalone",
+  "start_url": "./?utm_source=homescreen",
+  "theme_color": "#002140",
+  "background_color": "#001529",
+  "icons": [
+    {
+      "src": "icons/icon-192x192.png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "icons/icon-128x128.png",
+      "sizes": "128x128"
+    },
+    {
+      "src": "icons/icon-512x512.png",
+      "sizes": "512x512"
+    }
+  ]
+}

+ 18 - 0
src/pages/404.tsx

@@ -0,0 +1,18 @@
+import { history } from '@umijs/max';
+import { Button, Result } from 'antd';
+import React from 'react';
+
+const NoFoundPage: React.FC = () => (
+  <Result
+    status="404"
+    title="404"
+    subTitle="Sorry, the page you visited does not exist."
+    extra={
+      <Button type="primary" onClick={() => history.push('/')}>
+        Back Home
+      </Button>
+    }
+  />
+);
+
+export default NoFoundPage;

+ 44 - 0
src/pages/Admin.tsx

@@ -0,0 +1,44 @@
+import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-components';
+import '@umijs/max';
+import { Alert, Card, Typography } from 'antd';
+import React from 'react';
+const Admin: React.FC = () => {
+  return (
+    <PageContainer content={' 这个页面只有 admin 权限才能查看'}>
+      <Card>
+        <Alert
+          message={'更快更强的重型组件,已经发布。'}
+          type="success"
+          showIcon
+          banner
+          style={{
+            margin: -12,
+            marginBottom: 48,
+          }}
+        />
+        <Typography.Title
+          level={2}
+          style={{
+            textAlign: 'center',
+          }}
+        >
+          <SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
+        </Typography.Title>
+      </Card>
+      <p
+        style={{
+          textAlign: 'center',
+          marginTop: 24,
+        }}
+      >
+        Want to add more pages? Please refer to{' '}
+        <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
+          use block
+        </a>
+        。
+      </p>
+    </PageContainer>
+  );
+};
+export default Admin;

+ 155 - 0
src/pages/TableList/components/UpdateForm.tsx

@@ -0,0 +1,155 @@
+import {
+  ProFormDateTimePicker,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  StepsForm,
+} from '@ant-design/pro-components';
+import '@umijs/max';
+import { Modal } from 'antd';
+import React from 'react';
+export type FormValueType = {
+  target?: string;
+  template?: string;
+  type?: string;
+  time?: string;
+  frequency?: string;
+} & Partial<API.RuleListItem>;
+export type UpdateFormProps = {
+  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+  onSubmit: (values: FormValueType) => Promise<void>;
+  updateModalVisible: boolean;
+  values: Partial<API.RuleListItem>;
+};
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+  return (
+    <StepsForm
+      stepsProps={{
+        size: 'small',
+      }}
+      stepsFormRender={(dom, submitter) => {
+        return (
+          <Modal
+            width={640}
+            bodyStyle={{
+              padding: '32px 40px 48px',
+            }}
+            destroyOnClose
+            title={'规则配置'}
+            visible={props.updateModalVisible}
+            footer={submitter}
+            onCancel={() => {
+              props.onCancel();
+            }}
+          >
+            {dom}
+          </Modal>
+        );
+      }}
+      onFinish={props.onSubmit}
+    >
+      <StepsForm.StepForm
+        initialValues={{
+          name: props.values.name,
+          desc: props.values.desc,
+        }}
+        title={'基本信息'}
+      >
+        <ProFormText
+          name="name"
+          label={'规则名称'}
+          width="md"
+          rules={[
+            {
+              required: true,
+              message: '请输入规则名称!',
+            },
+          ]}
+        />
+        <ProFormTextArea
+          name="desc"
+          width="md"
+          label={'规则描述'}
+          placeholder={'请输入至少五个字符'}
+          rules={[
+            {
+              required: true,
+              message: '请输入至少五个字符的规则描述!',
+              min: 5,
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          target: '0',
+          template: '0',
+        }}
+        title={'配置规则属性'}
+      >
+        <ProFormSelect
+          name="target"
+          width="md"
+          label={'监控对象'}
+          valueEnum={{
+            0: '表一',
+            1: '表二',
+          }}
+        />
+        <ProFormSelect
+          name="template"
+          width="md"
+          label={'规则模板'}
+          valueEnum={{
+            0: '规则模板一',
+            1: '规则模板二',
+          }}
+        />
+        <ProFormRadio.Group
+          name="type"
+          label={'规则类型'}
+          options={[
+            {
+              value: '0',
+              label: '强',
+            },
+            {
+              value: '1',
+              label: '弱',
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          type: '1',
+          frequency: 'month',
+        }}
+        title={'设定调度周期'}
+      >
+        <ProFormDateTimePicker
+          name="time"
+          width="md"
+          label={'开始时间'}
+          rules={[
+            {
+              required: true,
+              message: '请选择开始时间!',
+            },
+          ]}
+        />
+        <ProFormSelect
+          name="frequency"
+          label={'监控对象'}
+          width="md"
+          valueEnum={{
+            month: '月',
+            week: '周',
+          }}
+        />
+      </StepsForm.StepForm>
+    </StepsForm>
+  );
+};
+export default UpdateForm;

+ 329 - 0
src/pages/TableList/index.tsx

@@ -0,0 +1,329 @@
+import { addRule, removeRule, rule, updateRule } from '@/services/ant-design-pro/api';
+import { PlusOutlined } from '@ant-design/icons';
+import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
+import {
+  FooterToolbar,
+  ModalForm,
+  PageContainer,
+  ProDescriptions,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from '@ant-design/pro-components';
+import '@umijs/max';
+import { Button, Drawer, Input, message } from 'antd';
+import React, { useRef, useState } from 'react';
+import type { FormValueType } from './components/UpdateForm';
+import UpdateForm from './components/UpdateForm';
+
+/**
+ * @en-US Add node
+ * @zh-CN 添加节点
+ * @param fields
+ */
+const handleAdd = async (fields: API.RuleListItem) => {
+  const hide = message.loading('正在添加');
+  try {
+    await addRule({
+      ...fields,
+    });
+    hide();
+    message.success('Added successfully');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('Adding failed, please try again!');
+    return false;
+  }
+};
+
+/**
+ * @en-US Update node
+ * @zh-CN 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: FormValueType) => {
+  const hide = message.loading('Configuring');
+  try {
+    await updateRule({
+      name: fields.name,
+      desc: fields.desc,
+      key: fields.key,
+    });
+    hide();
+    message.success('Configuration is successful');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('Configuration failed, please try again!');
+    return false;
+  }
+};
+
+/**
+ *  Delete node
+ * @zh-CN 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.RuleListItem[]) => {
+  const hide = message.loading('正在删除');
+  if (!selectedRows) return true;
+  try {
+    await removeRule({
+      key: selectedRows.map((row) => row.key),
+    });
+    hide();
+    message.success('Deleted successfully and will refresh soon');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('Delete failed, please try again');
+    return false;
+  }
+};
+const TableList: React.FC = () => {
+  /**
+   * @en-US Pop-up window of new window
+   * @zh-CN 新建窗口的弹窗
+   *  */
+  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
+  /**
+   * @en-US The pop-up window of the distribution update window
+   * @zh-CN 分布更新窗口的弹窗
+   * */
+  const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
+  const [showDetail, setShowDetail] = useState<boolean>(false);
+  const actionRef = useRef<ActionType>();
+  const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
+  const [selectedRowsState, setSelectedRows] = useState<API.RuleListItem[]>([]);
+
+  /**
+   * @en-US International configuration
+   * @zh-CN 国际化配置
+   * */
+
+  const columns: ProColumns<API.RuleListItem>[] = [
+    {
+      title: '规则名称',
+      dataIndex: 'name',
+      tip: 'The rule name is the unique key',
+      render: (dom, entity) => {
+        return (
+          <a
+            onClick={() => {
+              setCurrentRow(entity);
+              setShowDetail(true);
+            }}
+          >
+            {dom}
+          </a>
+        );
+      },
+    },
+    {
+      title: '描述',
+      dataIndex: 'desc',
+      valueType: 'textarea',
+    },
+    {
+      title: '服务调用次数',
+      dataIndex: 'callNo',
+      sorter: true,
+      hideInForm: true,
+      renderText: (val: string) => `${val}${'万'}`,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      hideInForm: true,
+      valueEnum: {
+        0: {
+          text: '关闭',
+          status: 'Default',
+        },
+        1: {
+          text: '运行中',
+          status: 'Processing',
+        },
+        2: {
+          text: '已上线',
+          status: 'Success',
+        },
+        3: {
+          text: '异常',
+          status: 'Error',
+        },
+      },
+    },
+    {
+      title: '上次调度时间',
+      sorter: true,
+      dataIndex: 'updatedAt',
+      valueType: 'dateTime',
+      renderFormItem: (item, { defaultRender, ...rest }, form) => {
+        const status = form.getFieldValue('status');
+        if (`${status}` === '0') {
+          return false;
+        }
+        if (`${status}` === '3') {
+          return <Input {...rest} placeholder={'请输入异常原因!'} />;
+        }
+        return defaultRender(item);
+      },
+    },
+    {
+      title: '操作',
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => [
+        <a
+          key="config"
+          onClick={() => {
+            handleUpdateModalVisible(true);
+            setCurrentRow(record);
+          }}
+        >
+          配置
+        </a>,
+        <a key="subscribeAlert" href="https://procomponents.ant.design/">
+          订阅警报
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <PageContainer>
+      <ProTable<API.RuleListItem, API.PageParams>
+        headerTitle={'查询表格'}
+        actionRef={actionRef}
+        rowKey="key"
+        search={{
+          labelWidth: 120,
+        }}
+        toolBarRender={() => [
+          <Button
+            type="primary"
+            key="primary"
+            onClick={() => {
+              handleModalVisible(true);
+            }}
+          >
+            <PlusOutlined /> 新建
+          </Button>,
+        ]}
+        request={rule}
+        columns={columns}
+        rowSelection={{
+          onChange: (_, selectedRows) => {
+            setSelectedRows(selectedRows);
+          },
+        }}
+      />
+      {selectedRowsState?.length > 0 && (
+        <FooterToolbar
+          extra={
+            <div>
+              已选择{' '}
+              <a
+                style={{
+                  fontWeight: 600,
+                }}
+              >
+                {selectedRowsState.length}
+              </a>{' '}
+              项 &nbsp;&nbsp;
+              <span>
+                服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)} 万
+              </span>
+            </div>
+          }
+        >
+          <Button
+            onClick={async () => {
+              await handleRemove(selectedRowsState);
+              setSelectedRows([]);
+              actionRef.current?.reloadAndRest?.();
+            }}
+          >
+            批量删除
+          </Button>
+          <Button type="primary">批量审批</Button>
+        </FooterToolbar>
+      )}
+      <ModalForm
+        title={'新建规则'}
+        width="400px"
+        visible={createModalVisible}
+        onVisibleChange={handleModalVisible}
+        onFinish={async (value) => {
+          const success = await handleAdd(value as API.RuleListItem);
+          if (success) {
+            handleModalVisible(false);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+      >
+        <ProFormText
+          rules={[
+            {
+              required: true,
+              message: '规则名称为必填项',
+            },
+          ]}
+          width="md"
+          name="name"
+        />
+        <ProFormTextArea width="md" name="desc" />
+      </ModalForm>
+      <UpdateForm
+        onSubmit={async (value) => {
+          const success = await handleUpdate(value);
+          if (success) {
+            handleUpdateModalVisible(false);
+            setCurrentRow(undefined);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+        onCancel={() => {
+          handleUpdateModalVisible(false);
+          if (!showDetail) {
+            setCurrentRow(undefined);
+          }
+        }}
+        updateModalVisible={updateModalVisible}
+        values={currentRow || {}}
+      />
+
+      <Drawer
+        width={600}
+        visible={showDetail}
+        onClose={() => {
+          setCurrentRow(undefined);
+          setShowDetail(false);
+        }}
+        closable={false}
+      >
+        {currentRow?.name && (
+          <ProDescriptions<API.RuleListItem>
+            column={2}
+            title={currentRow?.name}
+            request={async () => ({
+              data: currentRow || {},
+            })}
+            params={{
+              id: currentRow?.name,
+            }}
+            columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
+          />
+        )}
+      </Drawer>
+    </PageContainer>
+  );
+};
+export default TableList;

+ 48 - 0
src/pages/User/Login/index.less

@@ -0,0 +1,48 @@
+@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;
+  }
+}

+ 226 - 0
src/pages/User/Login/index.tsx

@@ -0,0 +1,226 @@
+import Footer from '@/components/Footer';
+import { login } from '@/services/ant-design-pro/api';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+import {
+  AlipayCircleOutlined,
+  LockOutlined,
+  MobileOutlined,
+  TaobaoCircleOutlined,
+  UserOutlined,
+  WeiboCircleOutlined,
+} from '@ant-design/icons';
+import {
+  LoginForm,
+  ProFormCaptcha,
+  ProFormCheckbox,
+  ProFormText,
+} from '@ant-design/pro-components';
+import { history, 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 LoginMessage: React.FC<{
+  content: string;
+}> = ({ content }) => {
+  return (
+    <Alert
+      style={{
+        marginBottom: 24,
+      }}
+      message={content}
+      type="error"
+      showIcon
+    />
+  );
+};
+const Login: React.FC = () => {
+  const [userLoginState, setUserLoginState] = useState<API.LoginResult>({});
+  const [type, setType] = useState<string>('account');
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      flushSync(() => {
+        setInitialState((s) => ({
+          ...s,
+          currentUser: userInfo,
+        }));
+      });
+    }
+  };
+  const handleSubmit = async (values: API.LoginParams) => {
+    try {
+      // 登录
+      const msg = await login({
+        ...values,
+        type,
+      });
+      if (msg.status === 'ok') {
+        const defaultLoginSuccessMessage = '登录成功!';
+        message.success(defaultLoginSuccessMessage);
+        await fetchUserInfo();
+        const urlParams = new URL(window.location.href).searchParams;
+        history.push(urlParams.get('redirect') || '/');
+        return;
+      }
+      console.log(msg);
+      // 如果失败去设置用户错误信息
+      setUserLoginState(msg);
+    } catch (error) {
+      const defaultLoginFailureMessage = '登录失败,请重试!';
+      console.log(error);
+      message.error(defaultLoginFailureMessage);
+    }
+  };
+  const { status, type: loginType } = userLoginState;
+  return (
+    <div className={styles.container}>
+      <div className={styles.content}>
+        <LoginForm
+          logo={<img alt="logo" src="/logo.svg" />}
+          title="Ant Design"
+          subTitle={'Ant Design 是西湖区最具影响力的 Web 设计规范'}
+          initialValues={{
+            autoLogin: true,
+          }}
+          actions={[
+            '其他登录方式 :',
+            <AlipayCircleOutlined key="AlipayCircleOutlined" className={styles.icon} />,
+            <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={styles.icon} />,
+            <WeiboCircleOutlined key="WeiboCircleOutlined" className={styles.icon} />,
+          ]}
+          onFinish={async (values) => {
+            await handleSubmit(values as API.LoginParams);
+          }}
+        >
+          <Tabs
+            activeKey={type}
+            onChange={setType}
+            centered
+            items={[
+              {
+                key: 'account',
+                label: '账户密码登录',
+              },
+              {
+                key: 'mobile',
+                label: '手机号登录',
+              },
+            ]}
+          />
+
+          {status === 'error' && loginType === 'account' && (
+            <LoginMessage content={'错误的用户名和密码(admin/ant.design)'} />
+          )}
+          {type === 'account' && (
+            <>
+              <ProFormText
+                name="username"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <UserOutlined className={styles.prefixIcon} />,
+                }}
+                placeholder={'用户名: admin or user'}
+                rules={[
+                  {
+                    required: true,
+                    message: '用户名是必填项!',
+                  },
+                ]}
+              />
+              <ProFormText.Password
+                name="password"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <LockOutlined className={styles.prefixIcon} />,
+                }}
+                placeholder={'密码: ant.design'}
+                rules={[
+                  {
+                    required: true,
+                    message: '密码是必填项!',
+                  },
+                ]}
+              />
+            </>
+          )}
+
+          {status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+          {type === 'mobile' && (
+            <>
+              <ProFormText
+                fieldProps={{
+                  size: 'large',
+                  prefix: <MobileOutlined className={styles.prefixIcon} />,
+                }}
+                name="mobile"
+                placeholder={'请输入手机号!'}
+                rules={[
+                  {
+                    required: true,
+                    message: '手机号是必填项!',
+                  },
+                  {
+                    pattern: /^1\d{10}$/,
+                    message: '不合法的手机号!',
+                  },
+                ]}
+              />
+              <ProFormCaptcha
+                fieldProps={{
+                  size: 'large',
+                  prefix: <LockOutlined className={styles.prefixIcon} />,
+                }}
+                captchaProps={{
+                  size: 'large',
+                }}
+                placeholder={'请输入验证码!'}
+                captchaTextRender={(timing, count) => {
+                  if (timing) {
+                    return `${count} ${'秒后重新获取'}`;
+                  }
+                  return '获取验证码';
+                }}
+                name="captcha"
+                rules={[
+                  {
+                    required: true,
+                    message: '验证码是必填项!',
+                  },
+                ]}
+                onGetCaptcha={async (phone) => {
+                  const result = await getFakeCaptcha({
+                    phone,
+                  });
+                  if (result === false) {
+                    return;
+                  }
+                  message.success('获取验证码成功!验证码为:1234');
+                }}
+              />
+            </>
+          )}
+          <div
+            style={{
+              marginBottom: 24,
+            }}
+          >
+            <ProFormCheckbox noStyle name="autoLogin">
+              自动登录
+            </ProFormCheckbox>
+            <a
+              style={{
+                float: 'right',
+              }}
+            >
+              忘记密码 ?
+            </a>
+          </div>
+        </LoginForm>
+      </div>
+      <Footer />
+    </div>
+  );
+};
+export default Login;

+ 154 - 0
src/pages/Welcome.tsx

@@ -0,0 +1,154 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Card } from 'antd';
+import React from 'react';
+
+/**
+ * 每个单独的卡片,为了复用样式抽成了组件
+ * @param param0
+ * @returns
+ */
+const InfoCard: React.FC<{
+  title: string;
+  index: number;
+  desc: string;
+  href: string;
+}> = ({ title, href, index, desc }) => {
+  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)',
+        borderRadius: '8px',
+        fontSize: '14px',
+        color: 'rgba(0,0,0,0.65)',
+        textAlign: 'justify',
+        lineHeight: ' 22px',
+        padding: '16px 19px',
+        flex: 1,
+      }}
+    >
+      <div
+        style={{
+          display: 'flex',
+          gap: '4px',
+          alignItems: 'center',
+        }}
+      >
+        <div
+          style={{
+            width: 48,
+            height: 48,
+            lineHeight: '22px',
+            backgroundSize: '100%',
+            textAlign: 'center',
+            padding: '8px 16px 16px 12px',
+            color: '#FFF',
+            fontWeight: 'bold',
+            backgroundImage:
+              "url('https://gw.alipayobjects.com/zos/bmw-prod/daaf8d50-8e6d-4251-905d-676a24ddfa12.svg')",
+          }}
+        >
+          {index}
+        </div>
+        <div
+          style={{
+            fontSize: '16px',
+            color: 'rgba(0, 0, 0, 0.85)',
+            paddingBottom: 8,
+          }}
+        >
+          {title}
+        </div>
+      </div>
+      <div
+        style={{
+          fontSize: '14px',
+          color: 'rgba(0,0,0,0.65)',
+          textAlign: 'justify',
+          lineHeight: '22px',
+          marginBottom: 8,
+        }}
+      >
+        {desc}
+      </div>
+      <a href={href} target="_blank" rel="noreferrer">
+        了解更多 {'>'}
+      </a>
+    </div>
+  );
+};
+
+const Welcome: React.FC = () => {
+  return (
+    <PageContainer>
+      <Card
+        style={{
+          borderRadius: 8,
+        }}
+        bodyStyle={{
+          backgroundImage:
+            'radial-gradient(circle at 97% 10%, #EBF2FF 0%, #F5F8FF 28%, #EBF1FF 124%)',
+        }}
+      >
+        <div
+          style={{
+            backgroundPosition: '100% -30%',
+            backgroundRepeat: 'no-repeat',
+            backgroundSize: '274px auto',
+            backgroundImage:
+              "url('https://gw.alipayobjects.com/mdn/rms_a9745b/afts/img/A*BuFmQqsB2iAAAAAAAAAAAAAAARQnAQ')",
+          }}
+        >
+          <div
+            style={{
+              fontSize: '20px',
+              color: '#1A1A1A',
+            }}
+          >
+            欢迎使用 Ant Design Pro
+          </div>
+          <p
+            style={{
+              fontSize: '14px',
+              color: 'rgba(0,0,0,0.65)',
+              lineHeight: '22px',
+              marginTop: 16,
+              marginBottom: 32,
+              width: '65%',
+            }}
+          >
+            Ant Design Pro 是一个整合了 umi,Ant Design 和 ProComponents
+            的脚手架方案。致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套设计资源,进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。
+          </p>
+          <div
+            style={{
+              display: 'flex',
+              gap: 16,
+            }}
+          >
+            <InfoCard
+              index={1}
+              href="https://umijs.org/docs/introduce/introduce"
+              title="了解 umi"
+              desc="umi 是一个可扩展的企业级前端应用框架,umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。"
+            />
+            <InfoCard
+              index={2}
+              title="了解 ant design"
+              href="https://ant.design"
+              desc="antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。"
+            />
+            <InfoCard
+              index={3}
+              title="了解 Pro Components"
+              href="https://procomponents.ant.design"
+              desc="ProComponents 是一个基于 Ant Design 做了更高抽象的模板组件,以 一个组件就是一个页面为开发理念,为中后台开发带来更好的体验。"
+            />
+          </div>
+        </div>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Welcome;

+ 94 - 0
src/requestErrorConfig.ts

@@ -0,0 +1,94 @@
+import type { RequestOptions } from '@@/plugin-request/request';
+import type { RequestConfig } from '@umijs/max';
+import { message, notification } from 'antd';
+
+// 错误处理方案: 错误类型
+enum ErrorShowType {
+  SILENT = 0,
+  WARN_MESSAGE = 1,
+  ERROR_MESSAGE = 2,
+  NOTIFICATION = 3,
+  REDIRECT = 9,
+}
+// 与后端约定的响应数据格式
+interface ResponseStructure {
+  data: any;
+  code?: number;
+  msg?: string;
+}
+
+/**
+ * @name 错误处理
+ * pro 自带的错误处理, 可以在这里做自己的改动
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+export const errorConfig: RequestConfig = {
+  // 错误处理: umi@3 的错误处理方案。
+  errorConfig: {
+    // 错误抛出
+    errorThrower: (res) => {
+      const { data, code, msg } = res as unknown as ResponseStructure;
+      //请求失败
+      if (code != 0) {
+        const error: any = new Error(msg);
+        error.name = 'BizError';
+        error.info = { code, msg, data };
+        throw error; // 抛出自制的错误
+      }
+    },
+    // 错误接收及处理
+    errorHandler: (error: any, opts: any) => {
+      if (opts?.skipErrorHandler) throw error;
+      // 我们的 errorThrower 抛出的错误。
+      if (error.name === 'BizError') {
+        const errorInfo: ResponseStructure | undefined = error.info;
+        if (errorInfo) {
+          const { msg, code } = errorInfo;
+          //在这里处理登陆或者其他的错误逻辑
+          switch (code) {
+            case ErrorShowType.SILENT:
+              // do nothing
+              break;
+            default:
+              message.error(msg);
+          }
+        }
+      } else if (error.response) {
+        // Axios 的错误
+        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
+        message.error(`Response status:${error.response.status}`);
+      } else if (error.request) {
+        // 请求已经成功发起,但没有收到响应
+        // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
+        // 而在node.js中是 http.ClientRequest 的实例
+        message.error('请求无响应');
+      } else {
+        // 发送请求时出了点问题
+        message.error('请求失败,请检查配置');
+      }
+    },
+  },
+
+  // 请求拦截器
+  requestInterceptors: [
+    (config: RequestOptions) => {
+      // 拦截请求配置,进行个性化处理。
+      const url = config?.url?.concat('?token = 123');
+      return { ...config, url };
+    },
+  ],
+
+  // 响应拦截器
+  responseInterceptors: [
+    (response) => {
+      alert('21312');
+      // 拦截响应数据,进行个性化处理
+      const { data } = response as unknown as ResponseStructure;
+
+      if (data?.success === false) {
+        message.error('请求失败!');
+      }
+      return response;
+    },
+  ],
+};

+ 65 - 0
src/service-worker.js

@@ -0,0 +1,65 @@
+/* eslint-disable no-restricted-globals */
+/* eslint-disable no-underscore-dangle */
+/* globals workbox */
+workbox.core.setCacheNameDetails({
+  prefix: 'antd-pro',
+  suffix: 'v5',
+});
+// Control all opened tabs ASAP
+workbox.clientsClaim();
+
+/**
+ * Use precaching list generated by workbox in build process.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
+ */
+workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
+
+/**
+ * Register a navigation route.
+ * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
+ */
+workbox.routing.registerNavigationRoute('/index.html');
+
+/**
+ * Use runtime cache:
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
+ *
+ * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
+ */
+
+/** Handle API requests */
+workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
+
+/** Handle third party requests */
+workbox.routing.registerRoute(
+  /^https:\/\/gw\.alipayobjects\.com\//,
+  workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(
+  /^https:\/\/cdnjs\.cloudflare\.com\//,
+  workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
+
+/** Response to client after skipping waiting with MessageChannel */
+addEventListener('message', (event) => {
+  const replyPort = event.ports[0];
+  const message = event.data;
+  if (replyPort && message && message.type === 'skip-waiting') {
+    event.waitUntil(
+      self.skipWaiting().then(
+        () => {
+          replyPort.postMessage({
+            error: null,
+          });
+        },
+        (error) => {
+          replyPort.postMessage({
+            error,
+          });
+        },
+      ),
+    );
+  }
+});

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

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

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

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

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

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

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

@@ -0,0 +1,82 @@
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace API {
+ 
+
+  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;
+  };
+}

+ 23 - 0
src/typings.d.ts

@@ -0,0 +1,23 @@
+declare module 'slash2';
+declare module '*.css';
+declare module '*.less';
+declare module '*.scss';
+declare module '*.sass';
+declare module '*.svg';
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.gif';
+declare module '*.bmp';
+declare module '*.tiff';
+declare module 'omit.js';
+declare module 'numeral';
+declare module '@antv/data-set';
+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 - 0
tests/run-tests.js

@@ -0,0 +1,47 @@
+/* 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 - 0
tests/setupTests.js

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

+ 39 - 0
tsconfig.json

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