Young 4 miesięcy temu
commit
cae9fdc2e8
90 zmienionych plików z 30845 dodań i 0 usunięć
  1. 16 0
      .editorconfig
  2. 8 0
      .eslintignore
  3. 7 0
      .eslintrc.js
  4. 43 0
      .gitignore
  5. 7 0
      .husky/commit-msg
  6. 4 0
      .husky/pre-commit
  7. 22 0
      .prettierignore
  8. 21 0
      .prettierrc.js
  9. 57 0
      README.md
  10. 171 0
      config/config.ts
  11. 28 0
      config/defaultSettings.ts
  12. 593 0
      config/oneapi.json
  13. 37 0
      config/proxy.ts
  14. 93 0
      config/routes.ts
  15. 23 0
      jest.config.ts
  16. 11 0
      jsconfig.json
  17. 176 0
      mock/listTableList.ts
  18. 31 0
      mock/login.ts
  19. 115 0
      mock/notices.ts
  20. 324 0
      mock/requestRecord.mock.js
  21. 5 0
      mock/route.ts
  22. 203 0
      mock/user.ts
  23. 109 0
      package.json
  24. 21929 0
      pnpm-lock.yaml
  25. 1 0
      public/CNAME
  26. BIN
      public/favicon.ico
  27. BIN
      public/icons/icon-128x128.png
  28. BIN
      public/icons/icon-192x192.png
  29. BIN
      public/icons/icon-512x512.png
  30. 1 0
      public/logo.svg
  31. 5 0
      public/pro_icon.svg
  32. 202 0
      public/scripts/loading.js
  33. 9 0
      src/access.ts
  34. 217 0
      src/app.tsx
  35. 35 0
      src/components/Footer/index.tsx
  36. 27 0
      src/components/HeaderDropdown/index.tsx
  37. 137 0
      src/components/RightContent/AvatarDropdown.tsx
  38. 31 0
      src/components/RightContent/index.tsx
  39. 12 0
      src/components/index.ts
  40. 21 0
      src/core/hooks/useEdit.ts
  41. 22 0
      src/core/network.ts
  42. 5 0
      src/core/ui/UIImage.less
  43. 204 0
      src/core/ui/UIImage.tsx
  44. 55 0
      src/global.less
  45. 91 0
      src/global.tsx
  46. 24 0
      src/locales/zh-CN.ts
  47. 5 0
      src/locales/zh-CN/component.ts
  48. 17 0
      src/locales/zh-CN/globalHeader.ts
  49. 59 0
      src/locales/zh-CN/menu.ts
  50. 67 0
      src/locales/zh-CN/pages.ts
  51. 6 0
      src/locales/zh-CN/pwa.ts
  52. 31 0
      src/locales/zh-CN/settingDrawer.ts
  53. 55 0
      src/locales/zh-CN/settings.ts
  54. 22 0
      src/manifest.json
  55. 17 0
      src/models/oss.ts
  56. 18 0
      src/pages/404.tsx
  57. 141 0
      src/pages/Content/HeadLines.tsx
  58. 15 0
      src/pages/Settings/Idols.tsx
  59. 338 0
      src/pages/Settings/Screens.tsx
  60. 559 0
      src/pages/Settings/Settings.tsx
  61. 236 0
      src/pages/Settings/Staff.tsx
  62. 120 0
      src/pages/Settings/Users.tsx
  63. 209 0
      src/pages/TableList/components/UpdateForm.tsx
  64. 397 0
      src/pages/TableList/index.tsx
  65. 1108 0
      src/pages/User/Login/__snapshots__/login.test.tsx.snap
  66. 219 0
      src/pages/User/Login/index.tsx
  67. 96 0
      src/pages/User/Login/login.test.tsx
  68. 27 0
      src/pages/User/Login/service.ts
  69. 164 0
      src/pages/Welcome.tsx
  70. 119 0
      src/requestErrorConfig.ts
  71. 65 0
      src/service-worker.js
  72. 94 0
      src/services/ant-design-pro/api.ts
  73. 10 0
      src/services/ant-design-pro/index.ts
  74. 21 0
      src/services/ant-design-pro/login.ts
  75. 101 0
      src/services/ant-design-pro/typings.d.ts
  76. 12 0
      src/services/swagger/index.ts
  77. 153 0
      src/services/swagger/pet.ts
  78. 48 0
      src/services/swagger/store.ts
  79. 112 0
      src/services/swagger/typings.d.ts
  80. 100 0
      src/services/swagger/user.ts
  81. 3 0
      src/tailwind.css
  82. 20 0
      src/typings.d.ts
  83. 11 0
      tailwind.config.js
  84. 64 0
      tests/setupTests.jsx
  85. 23 0
      tsconfig.json
  86. 1 0
      types/cache/cache.json
  87. 386 0
      types/cache/login.cache.json
  88. 324 0
      types/cache/mock/login.mock.cache.js
  89. 0 0
      types/cache/mock/mock.cache.js
  90. 120 0
      types/index.d.ts

+ 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

+ 8 - 0
.eslintignore

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

+ 7 - 0
.eslintrc.js

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

+ 43 - 0
.gitignore

@@ -0,0 +1,43 @@
+# 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
+*bak
+.vscode
+
+
+# visual studio code
+.history
+*.log
+functions/*
+.temp/**
+
+# umi
+.umi
+.umi-production
+.umi-test
+
+# screenshot
+screenshot
+.firebase
+.eslintcache
+
+.yarn
+
+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

+ 21 - 0
.prettierrc.js

@@ -0,0 +1,21 @@
+module.exports = {
+  singleQuote: true,
+  trailingComma: 'all',
+  printWidth: 100,
+  proseWrap: 'never',
+  endOfLine: 'lf',
+  overrides: [
+    {
+      files: '.prettierrc',
+      options: {
+        parser: 'json',
+      },
+    },
+    {
+      files: 'document.ejs',
+      options: {
+        parser: 'html',
+      },
+    },
+  ],
+};

+ 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).

+ 171 - 0
config/config.ts

@@ -0,0 +1,171 @@
+// 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 = 'dev' } = process.env;
+
+export default defineConfig({
+  mock: false,
+
+  extraPostCSSPlugins: [
+    require('postcss-import'),
+    require('tailwindcss'),
+    require('postcss-nested'), // or require('postcss-nesting')
+    require('autoprefixer'),
+  ],
+
+  /**
+   * @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 as keyof typeof proxy],
+  /**
+   * @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
+   */
+  title: '模饭星管理后台',
+  layout: {
+    locale: true,
+    ...defaultSettings,
+  },
+  /**
+   * @name moment2dayjs 插件
+   * @description 将项目中的 moment 替换为 dayjs
+   * @doc https://umijs.org/docs/max/moment2dayjs
+   */
+  moment2dayjs: {
+    preset: 'antd',
+    plugins: ['duration'],
+  },
+  /**
+   * @name 国际化插件
+   * @doc https://umijs.org/docs/max/i18n
+   */
+  locale: {
+    // default zh-CN
+    default: 'zh-CN',
+    antd: true,
+    // default true, when it is true, will use `navigator.language` overwrite default
+    baseNavigator: true,
+  },
+  /**
+   * @name antd 插件
+   * @description 内置了 babel import 插件
+   * @doc https://umijs.org/docs/max/antd#antd
+   */
+  antd: {
+    theme: {
+      token: {
+        borderRadius: 0,
+      },
+    },
+  },
+  /**
+   * @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: {},
+  /**
+   * @name <head> 中额外的 script
+   * @description 配置 <head> 中额外的 script
+   */
+  headScripts: [
+    // 解决首次加载时白屏的问题
+    { src: '/scripts/loading.js', async: true },
+  ],
+  //================ pro 插件配置 =================
+  presets: ['umi-presets-pro'],
+  /**
+   * @name openAPI 插件的配置
+   * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码
+   * @doc https://pro.ant.design/zh-cn/docs/openapi/
+   */
+  openAPI: [
+    {
+      requestLibPath: "import { request } from '@umijs/max'",
+      // 或者使用在线的版本
+      // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
+      schemaPath: join(__dirname, 'oneapi.json'),
+      mock: false,
+    },
+    {
+      requestLibPath: "import { request } from '@umijs/max'",
+      schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
+      projectName: 'swagger',
+    },
+  ],
+  mfsu: {
+    strategy: 'eager',
+  },
+  esbuildMinifyIIFE: true,
+  requestRecord: {},
+});

+ 28 - 0
config/defaultSettings.ts

@@ -0,0 +1,28 @@
+import { ProLayoutProps } from '@ant-design/pro-components';
+
+/**
+ * @name
+ */
+const Settings: ProLayoutProps & {
+  pwa?: boolean;
+  logo?: string;
+} = {
+  navTheme: 'light',
+  // 拂晓蓝
+  colorPrimary: '#1890ff',
+  layout: 'mix',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  fixSiderbar: true,
+  colorWeak: false,
+  title: '魔饭星',
+  pwa: true,
+  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
+  iconfontUrl: '',
+  token: {
+    // 参见ts声明,demo 见文档,通过token 修改样式
+    //https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F
+  },
+};
+
+export default Settings;

+ 593 - 0
config/oneapi.json

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

+ 37 - 0
config/proxy.ts

@@ -0,0 +1,37 @@
+/**
+ * 代理的配置
+ * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
+ * https://pro.ant.design/docs/deploy
+ *
+ * @doc https://umijs.org/docs/guides/proxy
+ */
+export default {
+  // 如果需要自定义本地开发服务器  请取消注释按需调整
+  dev: {
+    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
+    '/apis/': {
+      target: 'http://127.0.0.1:8010',
+      changeOrigin: true,
+    },
+  },
+
+  /**
+   * 详细的代理配置
+   * @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: { '^': '' },
+    },
+  },
+};

+ 93 - 0
config/routes.ts

@@ -0,0 +1,93 @@
+/**
+ * @name umi 的路由配置
+ * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
+ * @param path  path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
+ * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
+ * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
+ * @param redirect 配置路由跳转
+ * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
+ * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题
+ * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
+ * @doc https://umijs.org/docs/guides/routes
+ */
+export default [
+  {
+    path: '/user',
+    layout: false,
+    routes: [
+      {
+        name: 'login',
+        path: '/user/login',
+        component: './User/Login',
+      },
+    ],
+  },
+  {
+    path: '/welcome',
+    name: 'welcome',
+    icon: 'smile',
+    component: './Welcome',
+  },
+  {
+    path: '/content',
+    name: 'content',
+    icon: 'table',
+    routes: [
+      {
+        path: '/content',
+        redirect: '/content/headlines',
+      },
+      {
+        path: '/content/headlines',
+        name: 'headlines',
+        component: './Content/HeadLines',
+      },
+    ],
+  },
+  {
+    path: '/settings',
+    name: 'settings',
+    icon: 'crown',
+    //  access: 'canAdmin',
+    routes: [
+      {
+        path: '/settings',
+        redirect: '/settings/members',
+      },
+      {
+        path: '/settings/members',
+        name: 'members',
+        component: './Settings/Staff',
+      },
+      {
+        path: '/settings/users',
+        name: 'users',
+        component: './Settings/Users',
+      },
+      {
+        path: '/settings/screens',
+        name: 'screen',
+        component: './Settings/Screens',
+      },
+      {
+        path: '/settings/idols',
+        name: 'idols',
+        component: './Settings/Idols',
+      },
+      {
+        path: '/settings/settings',
+        name: 'settings',
+        component: './Settings/Settings',
+      },
+    ],
+  },
+  {
+    path: '/',
+    redirect: '/welcome',
+  },
+  {
+    path: '*',
+    layout: false,
+    component: './404',
+  },
+];

+ 23 - 0
jest.config.ts

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

+ 11 - 0
jsconfig.json

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

+ 176 - 0
mock/listTableList.ts

@@ -0,0 +1,176 @@
+import { Request, Response } from 'express';
+import moment from 'moment';
+import { parse } from 'url';
+
+// mock tableListDataSource
+const genList = (current: number, pageSize: number) => {
+  const tableListDataSource: API.RuleListItem[] = [];
+
+  for (let i = 0; i < pageSize; i += 1) {
+    const index = (current - 1) * 10 + i;
+    tableListDataSource.push({
+      key: index,
+      disabled: i % 6 === 0,
+      href: 'https://ant.design',
+      avatar: [
+        'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+        'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+      ][i % 2],
+      name: `TradeCode ${index}`,
+      owner: '曲丽丽',
+      desc: '这是一段描述',
+      callNo: Math.floor(Math.random() * 1000),
+      status: Math.floor(Math.random() * 10) % 4,
+      updatedAt: moment().format('YYYY-MM-DD'),
+      createdAt: moment().format('YYYY-MM-DD'),
+      progress: Math.ceil(Math.random() * 100),
+    });
+  }
+  tableListDataSource.reverse();
+  return tableListDataSource;
+};
+
+let tableListDataSource = genList(1, 100);
+
+function getRule(req: Request, res: Response, u: string) {
+  let realUrl = u;
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
+    realUrl = req.url;
+  }
+  const { current = 1, pageSize = 10 } = req.query;
+  const params = parse(realUrl, true).query as unknown as API.PageParams &
+    API.RuleListItem & {
+      sorter: any;
+      filter: any;
+    };
+
+  let dataSource = [...tableListDataSource].slice(
+    ((current as number) - 1) * (pageSize as number),
+    (current as number) * (pageSize as number),
+  );
+  if (params.sorter) {
+    const sorter = JSON.parse(params.sorter);
+    dataSource = dataSource.sort((prev, next) => {
+      let sortNumber = 0;
+      (Object.keys(sorter) as Array<keyof API.RuleListItem>).forEach((key) => {
+        let nextSort = next?.[key] as number;
+        let preSort = prev?.[key] as number;
+        if (sorter[key] === 'descend') {
+          if (preSort - nextSort > 0) {
+            sortNumber += -1;
+          } else {
+            sortNumber += 1;
+          }
+          return;
+        }
+        if (preSort - nextSort > 0) {
+          sortNumber += 1;
+        } else {
+          sortNumber += -1;
+        }
+      });
+      return sortNumber;
+    });
+  }
+  if (params.filter) {
+    const filter = JSON.parse(params.filter as any) as {
+      [key: string]: string[];
+    };
+    if (Object.keys(filter).length > 0) {
+      dataSource = dataSource.filter((item) => {
+        return (Object.keys(filter) as Array<keyof API.RuleListItem>).some((key) => {
+          if (!filter[key]) {
+            return true;
+          }
+          if (filter[key].includes(`${item[key]}`)) {
+            return true;
+          }
+          return false;
+        });
+      });
+    }
+  }
+
+  if (params.name) {
+    dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
+  }
+  const result = {
+    data: dataSource,
+    total: tableListDataSource.length,
+    success: true,
+    pageSize,
+    current: parseInt(`${params.current}`, 10) || 1,
+  };
+
+  return res.json(result);
+}
+
+function postRule(req: Request, res: Response, u: string, b: Request) {
+  let realUrl = u;
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
+    realUrl = req.url;
+  }
+
+  const body = (b && b.body) || req.body;
+  const { method, name, desc, key } = body;
+
+  switch (method) {
+    /* eslint no-case-declarations:0 */
+    case 'delete':
+      tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
+      break;
+    case 'post':
+      (() => {
+        const i = Math.ceil(Math.random() * 10000);
+        const newRule: API.RuleListItem = {
+          key: tableListDataSource.length,
+          href: 'https://ant.design',
+          avatar: [
+            'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+            'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+          ][i % 2],
+          name,
+          owner: '曲丽丽',
+          desc,
+          callNo: Math.floor(Math.random() * 1000),
+          status: Math.floor(Math.random() * 10) % 2,
+          updatedAt: moment().format('YYYY-MM-DD'),
+          createdAt: moment().format('YYYY-MM-DD'),
+          progress: Math.ceil(Math.random() * 100),
+        };
+        tableListDataSource.unshift(newRule);
+        return res.json(newRule);
+      })();
+      return;
+
+    case 'update':
+      (() => {
+        let newRule = {};
+        tableListDataSource = tableListDataSource.map((item) => {
+          if (item.key === key) {
+            newRule = { ...item, desc, name };
+            return { ...item, desc, name };
+          }
+          return item;
+        });
+        return res.json(newRule);
+      })();
+      return;
+    default:
+      break;
+  }
+
+  const result = {
+    list: tableListDataSource,
+    pagination: {
+      total: tableListDataSource.length,
+    },
+  };
+
+  res.json(result);
+}
+
+export default {
+  'GET /api/rule': getRule,
+  'POST /api/rule': postRule,
+};

+ 31 - 0
mock/login.ts

@@ -0,0 +1,31 @@
+import { defineMock } from 'umi';
+
+export default defineMock({
+  'POST /apis/admin/staff/login': (req, res) => {
+    res.status(200).send({
+      code: 0,
+      msg: 'OK',
+      data: {
+        token: '',
+      },
+    });
+
+    return;
+  },
+  'GET /apis/admin/image/token': (req, res) => {
+    res.status(200).send({
+      code: 0,
+      msg: 'OK',
+      data: {
+        accessId: 'LTAI5tM57HQzYUmZEBTTgvua',
+        policy:
+          'eyJleHBpcmF0aW9uIjoiMjAyNC0wNi0wM1QwMjowNToyNy42OTBaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIiXV19',
+        signature: '8LDgLM5ZkWW9IlcQn69W40E2XIU=',
+        dir: '',
+        host: 'https://oss-cn-shenzhen.aliyuncs.com',
+        expire: 1717380327,
+        callbackUrl: 'https://dev.century-info.com/apis/admin/oss/callback',
+      },
+    });
+  },
+});

+ 115 - 0
mock/notices.ts

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

+ 324 - 0
mock/requestRecord.mock.js

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

+ 5 - 0
mock/route.ts

@@ -0,0 +1,5 @@
+export default {
+  '/api/auth_routes': {
+    '/form/advanced-form': { authority: ['admin', 'user'] },
+  },
+};

+ 203 - 0
mock/user.ts

@@ -0,0 +1,203 @@
+import { Request, Response } from 'express';
+
+const waitTime = (time: number = 100) => {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(true);
+    }, time);
+  });
+};
+
+async function getFakeCaptcha(req: Request, res: Response) {
+  await waitTime(2000);
+  return res.json('captcha-xxx');
+}
+
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
+
+/**
+ * 当前用户的权限,如果为空代表没登录
+ * current user access, if is '', user need login
+ * 如果是 pro 的预览,默认是有权限的
+ */
+let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
+
+const getAccess = () => {
+  return access;
+};
+
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+export default {
+  // 支持值为 Object 和 Array
+  'GET /api/currentUser': (req: Request, res: Response) => {
+    if (!getAccess()) {
+      res.status(401).send({
+        data: {
+          isLogin: false,
+        },
+        errorCode: '401',
+        errorMessage: '请先登录!',
+        success: true,
+      });
+      return;
+    }
+    res.send({
+      success: true,
+      data: {
+        name: 'Serati Ma',
+        avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+        userid: '00000001',
+        email: 'antdesign@alipay.com',
+        signature: '海纳百川,有容乃大',
+        title: '交互专家',
+        group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+        tags: [
+          {
+            key: '0',
+            label: '很有想法的',
+          },
+          {
+            key: '1',
+            label: '专注设计',
+          },
+          {
+            key: '2',
+            label: '辣~',
+          },
+          {
+            key: '3',
+            label: '大长腿',
+          },
+          {
+            key: '4',
+            label: '川妹子',
+          },
+          {
+            key: '5',
+            label: '海纳百川',
+          },
+        ],
+        notifyCount: 12,
+        unreadCount: 11,
+        country: 'China',
+        access: getAccess(),
+        geographic: {
+          province: {
+            label: '浙江省',
+            key: '330000',
+          },
+          city: {
+            label: '杭州市',
+            key: '330100',
+          },
+        },
+        address: '西湖区工专路 77 号',
+        phone: '0752-268888888',
+      },
+    });
+  },
+  // GET POST 可省略
+  'GET /api/users': [
+    {
+      key: '1',
+      name: 'John Brown',
+      age: 32,
+      address: 'New York No. 1 Lake Park',
+    },
+    {
+      key: '2',
+      name: 'Jim Green',
+      age: 42,
+      address: 'London No. 1 Lake Park',
+    },
+    {
+      key: '3',
+      name: 'Joe Black',
+      age: 32,
+      address: 'Sidney No. 1 Lake Park',
+    },
+  ],
+  'POST /api/login/account': async (req: Request, res: Response) => {
+    const { password, username, type } = req.body;
+    await waitTime(2000);
+    if (password === 'ant.design' && username === 'admin') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      access = 'admin';
+      return;
+    }
+    if (password === 'ant.design' && username === 'user') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'user',
+      });
+      access = 'user';
+      return;
+    }
+    if (type === 'mobile') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      access = 'admin';
+      return;
+    }
+
+    res.send({
+      status: 'error',
+      type,
+      currentAuthority: 'guest',
+    });
+    access = 'guest';
+  },
+  'POST /api/login/outLogin': (req: Request, res: Response) => {
+    access = '';
+    res.send({ data: {}, success: true });
+  },
+  'POST /api/register': (req: Request, res: Response) => {
+    res.send({ status: 'ok', currentAuthority: 'user', success: true });
+  },
+  'GET /api/500': (req: Request, res: Response) => {
+    res.status(500).send({
+      timestamp: 1513932555104,
+      status: 500,
+      error: 'error',
+      message: 'error',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/404': (req: Request, res: Response) => {
+    res.status(404).send({
+      timestamp: 1513932643431,
+      status: 404,
+      error: 'Not Found',
+      message: 'No message available',
+      path: '/base/category/list/2121212',
+    });
+  },
+  'GET /api/403': (req: Request, res: Response) => {
+    res.status(403).send({
+      timestamp: 1513932555104,
+      status: 403,
+      error: 'Forbidden',
+      message: 'Forbidden',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/401': (req: Request, res: Response) => {
+    res.status(401).send({
+      timestamp: 1513932555104,
+      status: 401,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+
+  'GET  /api/login/captcha': getFakeCaptcha,
+};

+ 109 - 0
package.json

@@ -0,0 +1,109 @@
+{
+  "name": "ant-design-pro",
+  "version": "6.0.0",
+  "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",
+    "jest": "jest",
+    "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
+    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
+    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
+    "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
+    "openapi": "max openapi",
+    "prepare": "husky install",
+    "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
+    "preview": "npm run build && max preview --port 8000",
+    "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
+    "serve": "umi-serve",
+    "start": "cross-env UMI_ENV=dev max dev",
+    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
+    "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
+    "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
+    "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
+    "test": "jest",
+    "test:coverage": "npm run jest -- --coverage",
+    "test:update": "npm run jest -- -u",
+    "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.8.1",
+    "@ant-design/pro-components": "^2.6.48",
+    "@types/ali-oss": "^6.16.11",
+    "@umijs/route-utils": "^2.2.2",
+    "ali-oss": "^6.20.0",
+    "antd": "^5.13.2",
+    "antd-style": "^3.6.1",
+    "axios": "^1.7.2",
+    "classnames": "^2.5.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.30.1",
+    "omit.js": "^2.0.2",
+    "querystring": "^0.2.1",
+    "rc-menu": "^9.12.4",
+    "rc-util": "^5.38.1",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-helmet-async": "^1.3.0",
+    "react-json-view": "^1.21.3",
+    "uuid": "^9.0.1"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^3.3.0",
+    "@testing-library/react": "^13.4.0",
+    "@types/classnames": "^2.3.1",
+    "@types/express": "^4.17.21",
+    "@types/history": "^4.7.11",
+    "@types/jest": "^29.5.11",
+    "@types/lodash": "^4.14.202",
+    "@types/react": "^18.2.48",
+    "@types/react-dom": "^18.2.18",
+    "@types/react-helmet": "^6.1.11",
+    "@types/uuid": "^9.0.8",
+    "@umijs/fabric": "^2.14.1",
+    "@umijs/lint": "^4.1.1",
+    "@umijs/max": "^4.1.1",
+    "autoprefixer": "^10.4.19",
+    "cross-env": "^7.0.3",
+    "eslint": "^8.56.0",
+    "express": "^4.18.2",
+    "gh-pages": "^3.2.3",
+    "husky": "^7.0.4",
+    "jest": "^29.7.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "lint-staged": "^10.5.4",
+    "mockjs": "^1.1.0",
+    "postcss-import": "^16.1.0",
+    "postcss-nested": "^6.0.1",
+    "prettier": "^3.2.5",
+    "react-dev-inspector": "^1.9.0",
+    "swagger-ui-dist": "^4.19.1",
+    "tailwindcss": "^3.4.3",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.3.3",
+    "umi-presets-pro": "^2.0.3",
+    "umi-serve": "^1.9.11"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  }
+}

Plik diff jest za duży
+ 21929 - 0
pnpm-lock.yaml


+ 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


Plik diff jest za duży
+ 1 - 0
public/logo.svg


Plik diff jest za duży
+ 5 - 0
public/pro_icon.svg


+ 202 - 0
public/scripts/loading.js

@@ -0,0 +1,202 @@
+/**
+ * loading 占位
+ * 解决首次加载时白屏的问题
+ */
+ (function () {
+  const _root = document.querySelector('#root');
+  if (_root && _root.innerHTML === '') {
+    _root.innerHTML = `
+      <style>
+        html,
+        body,
+        #root {
+          height: 100%;
+          margin: 0;
+          padding: 0;
+        }
+        #root {
+          background-repeat: no-repeat;
+          background-size: 100% auto;
+        }
+
+        .loading-title {
+          font-size: 1.1rem;
+        }
+
+        .loading-sub-title {
+          margin-top: 20px;
+          font-size: 1rem;
+          color: #888;
+        }
+
+        .page-loading-warp {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding: 26px;
+        }
+        .ant-spin {
+          position: absolute;
+          display: none;
+          -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+          margin: 0;
+          padding: 0;
+          color: rgba(0, 0, 0, 0.65);
+          color: #1890ff;
+          font-size: 14px;
+          font-variant: tabular-nums;
+          line-height: 1.5;
+          text-align: center;
+          list-style: none;
+          opacity: 0;
+          -webkit-transition: -webkit-transform 0.3s
+            cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: -webkit-transform 0.3s
+            cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
+            -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          -webkit-font-feature-settings: "tnum";
+          font-feature-settings: "tnum";
+        }
+
+        .ant-spin-spinning {
+          position: static;
+          display: inline-block;
+          opacity: 1;
+        }
+
+        .ant-spin-dot {
+          position: relative;
+          display: inline-block;
+          width: 20px;
+          height: 20px;
+          font-size: 20px;
+        }
+
+        .ant-spin-dot-item {
+          position: absolute;
+          display: block;
+          width: 9px;
+          height: 9px;
+          background-color: #1890ff;
+          border-radius: 100%;
+          -webkit-transform: scale(0.75);
+          -ms-transform: scale(0.75);
+          transform: scale(0.75);
+          -webkit-transform-origin: 50% 50%;
+          -ms-transform-origin: 50% 50%;
+          transform-origin: 50% 50%;
+          opacity: 0.3;
+          -webkit-animation: antspinmove 1s infinite linear alternate;
+          animation: antSpinMove 1s infinite linear alternate;
+        }
+
+        .ant-spin-dot-item:nth-child(1) {
+          top: 0;
+          left: 0;
+        }
+
+        .ant-spin-dot-item:nth-child(2) {
+          top: 0;
+          right: 0;
+          -webkit-animation-delay: 0.4s;
+          animation-delay: 0.4s;
+        }
+
+        .ant-spin-dot-item:nth-child(3) {
+          right: 0;
+          bottom: 0;
+          -webkit-animation-delay: 0.8s;
+          animation-delay: 0.8s;
+        }
+
+        .ant-spin-dot-item:nth-child(4) {
+          bottom: 0;
+          left: 0;
+          -webkit-animation-delay: 1.2s;
+          animation-delay: 1.2s;
+        }
+
+        .ant-spin-dot-spin {
+          -webkit-transform: rotate(45deg);
+          -ms-transform: rotate(45deg);
+          transform: rotate(45deg);
+          -webkit-animation: antrotate 1.2s infinite linear;
+          animation: antRotate 1.2s infinite linear;
+        }
+
+        .ant-spin-lg .ant-spin-dot {
+          width: 32px;
+          height: 32px;
+          font-size: 32px;
+        }
+
+        .ant-spin-lg .ant-spin-dot i {
+          width: 14px;
+          height: 14px;
+        }
+
+        @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
+          .ant-spin-blur {
+            background: #fff;
+            opacity: 0.5;
+          }
+        }
+
+        @-webkit-keyframes antSpinMove {
+          to {
+            opacity: 1;
+          }
+        }
+
+        @keyframes antSpinMove {
+          to {
+            opacity: 1;
+          }
+        }
+
+        @-webkit-keyframes antRotate {
+          to {
+            -webkit-transform: rotate(405deg);
+            transform: rotate(405deg);
+          }
+        }
+
+        @keyframes antRotate {
+          to {
+            -webkit-transform: rotate(405deg);
+            transform: rotate(405deg);
+          }
+        }
+      </style>
+
+      <div style="
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        min-height: 362px;
+      ">
+        <div class="page-loading-warp">
+          <div class="ant-spin ant-spin-lg ant-spin-spinning">
+            <span class="ant-spin-dot ant-spin-dot-spin">
+              <i class="ant-spin-dot-item"></i>
+              <i class="ant-spin-dot-item"></i>
+              <i class="ant-spin-dot-item"></i>
+              <i class="ant-spin-dot-item"></i>
+            </span>
+          </div>
+        </div>
+        <div class="loading-title">
+          正在加载资源
+        </div>
+        <div class="loading-sub-title">
+          初次加载资源可能需要较多时间 请耐心等待
+        </div>
+      </div>
+    `;
+  }
+})();

+ 9 - 0
src/access.ts

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

+ 217 - 0
src/app.tsx

@@ -0,0 +1,217 @@
+import { AvatarDropdown, AvatarName, Question, SelectLang } from '@/components';
+import { LinkOutlined } from '@ant-design/icons';
+import type { Settings as LayoutSettings } from '@ant-design/pro-components';
+import { SettingDrawer } from '@ant-design/pro-components';
+import { history, Link, RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
+import defaultSettings from '../config/defaultSettings';
+import React from 'react';
+import { AxiosResponse } from 'axios';
+import { Service } from '@/pages/User/Login/service';
+import { message } from 'antd';
+import { Response } from '@/core/network';
+
+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 Service.me();
+
+      if (!msg.success) {
+        message.warning(msg.errorMessage);
+        history.push(loginPath);
+        return undefined;
+      }
+
+      /**
+       * name: 'Serati Ma',
+       *         avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+       *         userid: '00000001',
+       *         email: 'antdesign@alipay.com',
+       *         signature: '海纳百川,有容乃大',
+       *         title: '交互专家',
+       */
+      return {
+        name: msg.data.nickname,
+        avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+        userid: msg.data.id,
+      };
+    } catch (error) {
+      history.push(loginPath);
+    }
+    return undefined;
+  };
+  // 如果不是登录页面,执行
+  const { location } = history;
+  if (location.pathname !== loginPath) {
+    const currentUser = await fetchUserInfo();
+    return {
+      fetchUserInfo,
+      currentUser,
+      settings: defaultSettings as Partial<LayoutSettings>,
+    };
+  }
+  return {
+    fetchUserInfo,
+    settings: defaultSettings as Partial<LayoutSettings>,
+  };
+}
+
+// ProLayout 支持的api https://procomponents.ant.design/components/layout
+export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
+  return {
+    actionsRender: () => [<Question key="doc" />, <SelectLang key="SelectLang" />],
+    avatarProps: {
+      src: initialState?.currentUser?.avatar,
+      title: <AvatarName />,
+      render: (_, avatarChildren) => {
+        return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
+      },
+    },
+    // waterMarkProps: {
+    //   content: initialState?.currentUser?.name,
+    // },
+    //footerRender: () => <Footer />,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      if (!initialState?.currentUser && location.pathname !== loginPath) {
+        history.push(loginPath);
+      }
+    },
+    bgLayoutImgList: [
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
+        left: 85,
+        bottom: 100,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
+        bottom: -68,
+        right: -45,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
+        bottom: 0,
+        left: 0,
+        width: '331px',
+      },
+    ],
+    links: isDev
+      ? [
+          <Link key="openapi" to="/umi/plugin/openapi" target="_blank">
+            <LinkOutlined />
+            <span>OpenAPI 文档</span>
+          </Link>,
+        ]
+      : [],
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    // 增加一个 loading 的状态
+    childrenRender: (children) => {
+      // if (initialState?.loading) return <PageLoading />;
+      return (
+        <>
+          {children}
+          {isDev && (
+            <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: RequestConfig = {
+  errorConfig: {},
+  requestInterceptors: [
+    (url, options) => {
+      const token = localStorage.getItem('token');
+
+      if (token) {
+        options.headers = {
+          ...options.headers,
+          Authorization: token,
+        };
+      }
+      return {
+        url,
+        options,
+      };
+    },
+  ],
+  responseInterceptors: [
+    <T,>(response: AxiosResponse<Response<T>>) => {
+      console.log('response', response);
+
+      console.log('res', response.status);
+      if (response.status === 200) {
+        const data = response.data as any;
+
+        if (data.code !== undefined && data.msg !== undefined) {
+          if (data.code === 0) {
+            response.data = {
+              success: true,
+              data: data.data,
+              errorCode: data.code,
+              errorMessage: data.msg,
+            };
+          } else {
+            response.data = {
+              success: false,
+              data: data,
+              errorCode: data.code,
+              errorMessage: data.msg,
+            };
+          }
+        } else {
+          response.data = {
+            success: false,
+            errorCode: -1,
+            errorMessage: '返回结构错误!',
+            data: undefined as T,
+          };
+        }
+      } else {
+        response.data = {
+          success: false,
+          errorCode: response.status,
+          errorMessage: response.statusText,
+          data: undefined as T,
+        };
+      }
+
+      return response;
+    },
+  ],
+};

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

@@ -0,0 +1,35 @@
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-components';
+import React from 'react';
+
+const Footer: React.FC = () => {
+  return (
+    <DefaultFooter
+      style={{
+        background: 'none',
+      }}
+      links={[
+        {
+          key: 'Ant Design Pro',
+          title: 'Ant Design Pro',
+          href: 'https://pro.ant.design',
+          blankTarget: true,
+        },
+        {
+          key: 'github',
+          title: <GithubOutlined />,
+          href: 'https://github.com/ant-design/ant-design-pro',
+          blankTarget: true,
+        },
+        {
+          key: 'Ant Design',
+          title: 'Ant Design',
+          href: 'https://ant.design',
+          blankTarget: true,
+        },
+      ]}
+    />
+  );
+};
+
+export default Footer;

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

@@ -0,0 +1,27 @@
+import { Dropdown } from 'antd';
+import type { DropDownProps } from 'antd/es/dropdown';
+import React from 'react';
+import { createStyles } from 'antd-style';
+import classNames from 'classnames';
+
+const useStyles = createStyles(({ token }) => {
+  return {
+    dropdown: {
+      [`@media screen and (max-width: ${token.screenXS}px)`]: {
+        width: '100%',
+      },
+    },
+  };
+});
+
+export type HeaderDropdownProps = {
+  overlayClassName?: string;
+  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+} & Omit<DropDownProps, 'overlay'>;
+
+const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => {
+  const { styles } = useStyles();
+  return <Dropdown overlayClassName={classNames(styles.dropdown, cls)} {...restProps} />;
+};
+
+export default HeaderDropdown;

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

@@ -0,0 +1,137 @@
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { history, useModel } from '@umijs/max';
+import { Spin } from 'antd';
+import { createStyles } from 'antd-style';
+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';
+
+export type GlobalHeaderRightProps = {
+  menu?: boolean;
+  children?: React.ReactNode;
+};
+
+export const AvatarName = () => {
+  const { initialState } = useModel('@@initialState');
+  const { currentUser } = initialState || {};
+  return <span className="anticon">{currentUser?.name}</span>;
+};
+
+const useStyles = createStyles(({ token }) => {
+  return {
+    action: {
+      display: 'flex',
+      height: '48px',
+      marginLeft: 'auto',
+      overflow: 'hidden',
+      alignItems: 'center',
+      padding: '0 8px',
+      cursor: 'pointer',
+      borderRadius: token.borderRadius,
+      '&:hover': {
+        backgroundColor: token.colorBgTextHover,
+      },
+    },
+  };
+});
+
+export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
+  /**
+   * 退出登录,并且将当前的 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 { styles } = useStyles();
+
+  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}>
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    </span>
+  );
+
+  if (!initialState) {
+    return loading;
+  }
+
+  const { currentUser } = initialState;
+
+  if (!currentUser || !currentUser.name) {
+    return loading;
+  }
+
+  const menuItems = [
+    ...(menu
+      ? [
+          {
+            key: 'center',
+            icon: <UserOutlined />,
+            label: '个人中心',
+          },
+          {
+            key: 'settings',
+            icon: <SettingOutlined />,
+            label: '个人设置',
+          },
+          {
+            type: 'divider' as const,
+          },
+        ]
+      : []),
+    {
+      key: 'logout',
+      icon: <LogoutOutlined />,
+      label: '退出登录',
+    },
+  ];
+
+  return (
+    <HeaderDropdown
+      menu={{
+        selectedKeys: [],
+        onClick: onMenuClick,
+        items: menuItems,
+      }}
+    >
+      {children}
+    </HeaderDropdown>
+  );
+};

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

@@ -0,0 +1,31 @@
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { SelectLang as UmiSelectLang } from '@umijs/max';
+import React from 'react';
+
+export type SiderTheme = 'light' | 'dark';
+
+export const SelectLang = () => {
+  return (
+    <UmiSelectLang
+      style={{
+        padding: 4,
+      }}
+    />
+  );
+};
+
+export const Question = () => {
+  return (
+    <div
+      style={{
+        display: 'flex',
+        height: 26,
+      }}
+      onClick={() => {
+        window.open('https://pro.ant.design/docs/getting-started');
+      }}
+    >
+      <QuestionCircleOutlined />
+    </div>
+  );
+};

+ 12 - 0
src/components/index.ts

@@ -0,0 +1,12 @@
+/**
+ * 这个文件作为组件的目录
+ * 目的是统一管理对外输出的组件,方便分类
+ */
+/**
+ * 布局组件
+ */
+import Footer from './Footer';
+import { Question, SelectLang } from './RightContent';
+import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
+
+export { Footer, Question, SelectLang, AvatarDropdown, AvatarName };

+ 21 - 0
src/core/hooks/useEdit.ts

@@ -0,0 +1,21 @@
+import { useState } from 'react';
+
+export type EditBundle<T = number> = {
+  open: boolean;
+  id?: T;
+  create: () => void;
+  update: (id: T) => void;
+  close: () => void;
+};
+
+export function useEdit<T = number>(): EditBundle<T> {
+  const [edit, setEdit] = useState<{ open: boolean; id?: T }>({ open: false, id: undefined });
+
+  return {
+    open: edit.open,
+    id: edit.id,
+    create: () => setEdit({ open: true, id: undefined }),
+    update: (id: T) => setEdit({ open: true, id: id }),
+    close: () => setEdit(() => ({ open: false, id: undefined })),
+  };
+}

+ 22 - 0
src/core/network.ts

@@ -0,0 +1,22 @@
+import { request } from '@umijs/max';
+import { message } from 'antd';
+
+export type Response<T> = {
+  success: boolean;
+  data: T;
+  errorCode?: number;
+  errorMessage?: string;
+  showType?: number;
+  traceId?: string;
+  host?: string;
+};
+
+export async function invoke<T>(url: string, opts: any): Promise<Response<T>> {
+  const res = (await request(url, opts)) as unknown as Response<T>;
+
+  if (!res.success) {
+    message.warning(`[${res.errorCode}]${res.errorMessage}`);
+  }
+
+  return res;
+}

+ 5 - 0
src/core/ui/UIImage.less

@@ -0,0 +1,5 @@
+.ui-image {
+  .ant-upload {
+    height: auto !important;
+  }
+}

Plik diff jest za duży
+ 204 - 0
src/core/ui/UIImage.tsx


+ 55 - 0
src/global.less

@@ -0,0 +1,55 @@
+@import "tailwind.css";
+
+html,
+body,
+#root {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+    'Noto Color Emoji';
+}
+
+.colorWeak {
+  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: 768px) {
+  .ant-table {
+    width: 100%;
+    overflow-x: auto;
+    &-thead > tr,
+    &-tbody > tr {
+      > th,
+      > td {
+        white-space: pre;
+        > span {
+          display: block;
+        }
+      }
+    }
+  }
+}

+ 91 - 0
src/global.tsx

@@ -0,0 +1,91 @@
+import { useIntl } from '@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(useIntl().formatMessage({ id: 'app.pwa.offline' }));
+  });
+
+  // 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.destroy(key);
+          reloadSW();
+        }}
+      >
+        {useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
+      </Button>
+    );
+    notification.open({
+      message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
+      description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
+      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();
+}

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

@@ -0,0 +1,24 @@
+import component from './zh-CN/component';
+import globalHeader from './zh-CN/globalHeader';
+import menu from './zh-CN/menu';
+import pages from './zh-CN/pages';
+import pwa from './zh-CN/pwa';
+import settingDrawer from './zh-CN/settingDrawer';
+import settings from './zh-CN/settings';
+
+export default {
+  'navBar.lang': '语言',
+  'layout.user.link.help': '帮助',
+  'layout.user.link.privacy': '隐私',
+  'layout.user.link.terms': '条款',
+  'app.preview.down.block': '下载此页面到本地项目',
+  'app.welcome.link.fetch-blocks': '获取全部区块',
+  'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
+  ...pages,
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

+ 5 - 0
src/locales/zh-CN/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': '展开',
+  'component.tagSelect.collapse': '收起',
+  'component.tagSelect.all': '全部',
+};

+ 17 - 0
src/locales/zh-CN/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': '站内搜索',
+  'component.globalHeader.search.example1': '搜索提示一',
+  'component.globalHeader.search.example2': '搜索提示二',
+  'component.globalHeader.search.example3': '搜索提示三',
+  'component.globalHeader.help': '使用文档',
+  'component.globalHeader.notification': '通知',
+  'component.globalHeader.notification.empty': '你已查看所有通知',
+  'component.globalHeader.message': '消息',
+  'component.globalHeader.message.empty': '您已读完所有消息',
+  'component.globalHeader.event': '待办',
+  'component.globalHeader.event.empty': '你已完成所有待办',
+  'component.noticeIcon.clear': '清空',
+  'component.noticeIcon.cleared': '清空了',
+  'component.noticeIcon.empty': '暂无数据',
+  'component.noticeIcon.view-more': '查看更多',
+};

+ 59 - 0
src/locales/zh-CN/menu.ts

@@ -0,0 +1,59 @@
+export default {
+  'menu.welcome': '欢迎',
+  'menu.more-blocks': '更多区块',
+  'menu.home': '首页',
+  'menu.content': '内容管理',
+  'menu.content.headlines': '头条管理',
+  'menu.settings': '系统设置',
+  'menu.settings.users': '用户管理',
+  'menu.settings.settings': '字典设置',
+  'menu.settings.members': '管理员管理',
+  'menu.settings.screen': '开机屏管理',
+  'menu.settings.idols': '明星管理',
+  'menu.admin.sub-page': '二级管理页',
+  'menu.login': '登录',
+  'menu.register': '注册',
+  'menu.register-result': '注册结果',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': '分析页',
+  'menu.dashboard.monitor': '监控页',
+  'menu.dashboard.workplace': '工作台',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': '表单页',
+  'menu.form.basic-form': '基础表单',
+  'menu.form.step-form': '分步表单',
+  'menu.form.step-form.info': '分步表单(填写转账信息)',
+  'menu.form.step-form.confirm': '分步表单(确认转账信息)',
+  'menu.form.step-form.result': '分步表单(完成)',
+  'menu.form.advanced-form': '高级表单',
+  'menu.list': '列表页',
+  'menu.list.table-list': '查询表格',
+  'menu.list.basic-list': '标准列表',
+  'menu.list.card-list': '卡片列表',
+  'menu.list.search-list': '搜索列表',
+  'menu.list.search-list.articles': '搜索列表(文章)',
+  'menu.list.search-list.projects': '搜索列表(项目)',
+  'menu.list.search-list.applications': '搜索列表(应用)',
+  'menu.profile': '详情页',
+  'menu.profile.basic': '基础详情页',
+  'menu.profile.advanced': '高级详情页',
+  'menu.result': '结果页',
+  'menu.result.success': '成功页',
+  'menu.result.fail': '失败页',
+  'menu.exception': '异常页',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': '触发错误',
+  'menu.account': '个人页',
+  'menu.account.center': '个人中心',
+  'menu.account.settings': '个人设置',
+  'menu.account.trigger': '触发报错',
+  'menu.account.logout': '退出登录',
+  'menu.editor': '图形编辑器',
+  'menu.editor.flow': '流程编辑器',
+  'menu.editor.mind': '脑图编辑器',
+  'menu.editor.koni': '拓扑编辑器',
+};

+ 67 - 0
src/locales/zh-CN/pages.ts

@@ -0,0 +1,67 @@
+export default {
+  'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
+  'pages.login.accountLogin.tab': '账户密码登录',
+  'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)',
+  'pages.login.failure': '登录失败,请重试!',
+  'pages.login.success': '登录成功!',
+  'pages.login.username.placeholder': '用户名: admin or user',
+  'pages.login.username.required': '用户名是必填项!',
+  'pages.login.password.placeholder': '密码: ant.design',
+  'pages.login.password.required': '密码是必填项!',
+  'pages.login.phoneLogin.tab': '手机号登录',
+  'pages.login.phoneLogin.errorMessage': '验证码错误',
+  'pages.login.phoneNumber.placeholder': '请输入手机号!',
+  'pages.login.phoneNumber.required': '手机号是必填项!',
+  'pages.login.phoneNumber.invalid': '不合法的手机号!',
+  'pages.login.captcha.placeholder': '请输入验证码!',
+  'pages.login.captcha.required': '验证码是必填项!',
+  'pages.login.phoneLogin.getVerificationCode': '获取验证码',
+  'pages.getCaptchaSecondText': '秒后重新获取',
+  'pages.login.rememberMe': '自动登录',
+  'pages.login.forgotPassword': '忘记密码 ?',
+  'pages.login.submit': '登录',
+  'pages.login.loginWith': '其他登录方式 :',
+  'pages.login.registerAccount': '注册账户',
+  'pages.welcome.link': '欢迎使用',
+  'pages.welcome.alertMessage': '更快更强的重型组件,已经发布。',
+  'pages.404.subTitle': '抱歉,您访问的页面不存在。',
+  'pages.404.buttonText': '返回首页',
+  'pages.admin.subPage.title': ' 这个页面只有 admin 权限才能查看',
+  'pages.admin.subPage.alertMessage': 'umi ui 现已发布,欢迎使用 npm run ui 启动体验。',
+  'pages.searchTable.createForm.newRule': '新建规则',
+  'pages.searchTable.updateForm.ruleConfig': '规则配置',
+  'pages.searchTable.updateForm.basicConfig': '基本信息',
+  'pages.searchTable.updateForm.ruleName.nameLabel': '规则名称',
+  'pages.searchTable.updateForm.ruleName.nameRules': '请输入规则名称!',
+  'pages.searchTable.updateForm.ruleDesc.descLabel': '规则描述',
+  'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '请输入至少五个字符',
+  'pages.searchTable.updateForm.ruleDesc.descRules': '请输入至少五个字符的规则描述!',
+  'pages.searchTable.updateForm.ruleProps.title': '配置规则属性',
+  'pages.searchTable.updateForm.object': '监控对象',
+  'pages.searchTable.updateForm.ruleProps.templateLabel': '规则模板',
+  'pages.searchTable.updateForm.ruleProps.typeLabel': '规则类型',
+  'pages.searchTable.updateForm.schedulingPeriod.title': '设定调度周期',
+  'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '开始时间',
+  'pages.searchTable.updateForm.schedulingPeriod.timeRules': '请选择开始时间!',
+  'pages.searchTable.titleDesc': '描述',
+  'pages.searchTable.ruleName': '规则名称为必填项',
+  'pages.searchTable.titleCallNo': '服务调用次数',
+  'pages.searchTable.titleStatus': '状态',
+  'pages.searchTable.nameStatus.default': '关闭',
+  'pages.searchTable.nameStatus.running': '运行中',
+  'pages.searchTable.nameStatus.online': '已上线',
+  'pages.searchTable.nameStatus.abnormal': '异常',
+  'pages.searchTable.titleUpdatedAt': '上次调度时间',
+  'pages.searchTable.exception': '请输入异常原因!',
+  'pages.searchTable.titleOption': '操作',
+  'pages.searchTable.config': '配置',
+  'pages.searchTable.subscribeAlert': '订阅警报',
+  'pages.searchTable.title': '查询表格',
+  'pages.searchTable.new': '新建',
+  'pages.searchTable.chosen': '已选择',
+  'pages.searchTable.item': '项',
+  'pages.searchTable.totalServiceCalls': '服务调用次数总计',
+  'pages.searchTable.tenThousand': '万',
+  'pages.searchTable.batchDeletion': '批量删除',
+  'pages.searchTable.batchApproval': '批量审批',
+};

+ 6 - 0
src/locales/zh-CN/pwa.ts

@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': '当前处于离线状态',
+  'app.pwa.serviceworker.updated': '有新内容',
+  'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
+  'app.pwa.serviceworker.updated.ok': '刷新',
+};

+ 31 - 0
src/locales/zh-CN/settingDrawer.ts

@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': '整体风格设置',
+  'app.setting.pagestyle.dark': '暗色菜单风格',
+  'app.setting.pagestyle.light': '亮色菜单风格',
+  'app.setting.content-width': '内容区域宽度',
+  'app.setting.content-width.fixed': '定宽',
+  'app.setting.content-width.fluid': '流式',
+  'app.setting.themecolor': '主题色',
+  'app.setting.themecolor.dust': '薄暮',
+  'app.setting.themecolor.volcano': '火山',
+  'app.setting.themecolor.sunset': '日暮',
+  'app.setting.themecolor.cyan': '明青',
+  'app.setting.themecolor.green': '极光绿',
+  'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
+  'app.setting.themecolor.geekblue': '极客蓝',
+  'app.setting.themecolor.purple': '酱紫',
+  'app.setting.navigationmode': '导航模式',
+  'app.setting.sidemenu': '侧边菜单布局',
+  'app.setting.topmenu': '顶部菜单布局',
+  'app.setting.fixedheader': '固定 Header',
+  'app.setting.fixedsidebar': '固定侧边菜单',
+  'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
+  'app.setting.hideheader': '下滑时隐藏 Header',
+  'app.setting.hideheader.hint': '固定 Header 时可配置',
+  'app.setting.othersettings': '其他设置',
+  'app.setting.weakmode': '色弱模式',
+  'app.setting.copy': '拷贝设置',
+  'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置',
+  'app.setting.production.hint':
+    '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
+};

+ 55 - 0
src/locales/zh-CN/settings.ts

@@ -0,0 +1,55 @@
+export default {
+  'app.settings.menuMap.basic': '基本设置',
+  'app.settings.menuMap.security': '安全设置',
+  'app.settings.menuMap.binding': '账号绑定',
+  'app.settings.menuMap.notification': '新消息通知',
+  'app.settings.basic.avatar': '头像',
+  'app.settings.basic.change-avatar': '更换头像',
+  'app.settings.basic.email': '邮箱',
+  'app.settings.basic.email-message': '请输入您的邮箱!',
+  'app.settings.basic.nickname': '昵称',
+  'app.settings.basic.nickname-message': '请输入您的昵称!',
+  'app.settings.basic.profile': '个人简介',
+  'app.settings.basic.profile-message': '请输入个人简介!',
+  'app.settings.basic.profile-placeholder': '个人简介',
+  'app.settings.basic.country': '国家/地区',
+  'app.settings.basic.country-message': '请输入您的国家或地区!',
+  'app.settings.basic.geographic': '所在省市',
+  'app.settings.basic.geographic-message': '请输入您的所在省市!',
+  'app.settings.basic.address': '街道地址',
+  'app.settings.basic.address-message': '请输入您的街道地址!',
+  'app.settings.basic.phone': '联系电话',
+  'app.settings.basic.phone-message': '请输入您的联系电话!',
+  'app.settings.basic.update': '更新基本信息',
+  'app.settings.security.strong': '强',
+  'app.settings.security.medium': '中',
+  'app.settings.security.weak': '弱',
+  'app.settings.security.password': '账户密码',
+  'app.settings.security.password-description': '当前密码强度',
+  'app.settings.security.phone': '密保手机',
+  'app.settings.security.phone-description': '已绑定手机',
+  'app.settings.security.question': '密保问题',
+  'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
+  'app.settings.security.email': '备用邮箱',
+  'app.settings.security.email-description': '已绑定邮箱',
+  'app.settings.security.mfa': 'MFA 设备',
+  'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
+  'app.settings.security.modify': '修改',
+  'app.settings.security.set': '设置',
+  'app.settings.security.bind': '绑定',
+  'app.settings.binding.taobao': '绑定淘宝',
+  'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
+  'app.settings.binding.alipay': '绑定支付宝',
+  'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
+  'app.settings.binding.dingding': '绑定钉钉',
+  'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
+  'app.settings.binding.bind': '绑定',
+  'app.settings.notification.password': '账户密码',
+  'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
+  'app.settings.notification.messages': '系统消息',
+  'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
+  'app.settings.notification.todo': '待办任务',
+  'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
+  'app.settings.open': '开',
+  'app.settings.close': '关',
+};

+ 22 - 0
src/manifest.json

@@ -0,0 +1,22 @@
+{
+  "name": "mfx",
+  "short_name": "mfx",
+  "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"
+    }
+  ]
+}

+ 17 - 0
src/models/oss.ts

@@ -0,0 +1,17 @@
+import OSS from 'ali-oss';
+
+export default function () {
+  const opts = {
+    AccessKeyId: 'LTAI5tP4xamXgWHCas1mqBwg',
+    AccessKeySecret: 'makHzw6FDwmSd2nJ7X0NTHSIeoVGof',
+    Bucket: 'mfx-app',
+    Endpoint: 'oss-cn-shenzhen',
+  };
+
+  return new OSS({
+    region: opts.Endpoint,
+    accessKeyId: opts.AccessKeyId,
+    accessKeySecret: opts.AccessKeySecret,
+    bucket: opts.Bucket,
+  });
+}

+ 18 - 0
src/pages/404.tsx

@@ -0,0 +1,18 @@
+import { history, useIntl } from '@umijs/max';
+import { Button, Result } from 'antd';
+import React from 'react';
+
+const NoFoundPage: React.FC = () => (
+  <Result
+    status="404"
+    title="404"
+    subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })}
+    extra={
+      <Button type="primary" onClick={() => history.push('/')}>
+        {useIntl().formatMessage({ id: 'pages.404.buttonText' })}
+      </Button>
+    }
+  />
+);
+
+export default NoFoundPage;

+ 141 - 0
src/pages/Content/HeadLines.tsx

@@ -0,0 +1,141 @@
+import React from 'react';
+import {
+  PageContainer,
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Card, Modal, Popover } from 'antd';
+import { EditBundle, useEdit } from '@/core/hooks/useEdit';
+import { PlusOutlined } from '@ant-design/icons';
+
+export type HeadLine = {
+  id: number;
+  title: string;
+  content: string;
+  createTime: string;
+};
+
+const Edit: React.FC<{ bundle: EditBundle }> = (props) => {
+  const { bundle } = props;
+  const [form] = ProForm.useForm<HeadLine>();
+
+  return (
+    <Modal
+      open={bundle.open}
+      title={bundle.id ? '编辑' : '新增'}
+      onCancel={() => {
+        bundle.close();
+      }}
+      onClose={() => {
+        bundle.close();
+      }}
+      onOk={() => {}}
+    >
+      <ProForm
+        className={'pt-5 pb-1'}
+        form={form}
+        submitter={false}
+        layout={'horizontal'}
+        labelCol={{ span: 4 }}
+        validateTrigger={false}
+      >
+        <ProFormText
+          name="title"
+          label="标题"
+          rules={[
+            {
+              required: true,
+              message: '请输入标题',
+            },
+            {
+              min: 3,
+              max: 20,
+              message: '标题长度在3-20之间',
+            },
+          ]}
+        />
+        <ProFormTextArea
+          name="content"
+          label="内容"
+          fieldProps={{
+            style: { height: 200 },
+            count: {
+              max: 1024,
+              show: true,
+            },
+          }}
+          rules={[
+            {
+              required: true,
+              message: '请输入内容',
+            },
+            {
+              min: 10,
+              max: 1024,
+              message: '内容长度在10-100之间',
+            },
+          ]}
+        />
+      </ProForm>
+    </Modal>
+  );
+};
+
+const HeadLines: React.FC<{}> = (props) => {
+  const edit = useEdit();
+
+  return (
+    <PageContainer>
+      <Card>
+        {edit.open && <Edit bundle={edit} />}
+        <ProTable<HeadLine>
+          rowKey="id"
+          search={false}
+          toolbar={{
+            settings: [],
+            actions: [
+              <Button
+                icon={<PlusOutlined />}
+                type={'primary'}
+                key={'add'}
+                onClick={() => edit.create()}
+              >
+                新建
+              </Button>,
+            ],
+          }}
+          columns={[
+            {
+              title: 'ID',
+              dataIndex: 'id',
+            },
+            {
+              title: '标题',
+              dataIndex: 'title',
+            },
+            {
+              title: '内容',
+              dataIndex: 'content',
+              render: (_, record) => {
+                return (
+                  <Popover content={record.content}>
+                    <a href="#">查看</a>
+                  </Popover>
+                );
+              },
+            },
+            {
+              title: '创建时间',
+              valueType: 'date',
+              dataIndex: 'createTime',
+            },
+          ]}
+        ></ProTable>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default HeadLines;

+ 15 - 0
src/pages/Settings/Idols.tsx

@@ -0,0 +1,15 @@
+import { PageContainer, ProTable } from '@ant-design/pro-components';
+import React from 'react';
+import { Card } from 'antd';
+
+const Idols: React.FC<{}> = (props) => {
+  return (
+    <PageContainer>
+      <Card>
+        <ProTable></ProTable>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Idols;

+ 338 - 0
src/pages/Settings/Screens.tsx

@@ -0,0 +1,338 @@
+import React, { useRef, useState } from 'react';
+import {
+  ActionType,
+  PageContainer,
+  ProForm,
+  ProFormDateRangePicker,
+  ProFormText,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Calendar, Card, Image, Modal, Popover, Tabs, Tag } from 'antd';
+import { AddOutline } from 'antd-mobile-icons';
+import { EditBundle, useEdit } from '@/core/hooks/useEdit';
+import UIImage from '@/core/ui/UIImage';
+import { v4 } from 'uuid';
+import { invoke } from '@/core/network';
+import dayjs from 'dayjs';
+
+export interface LaunchImage {
+  id: number;
+  title: string;
+  uri: string;
+  effectiveTime: string;
+  takeDownTime: string;
+}
+
+export interface LaunchImageList {
+  records: LaunchImage[];
+  total: number;
+  size: number;
+  current: number;
+}
+
+class Service {
+  public static async list(current: number, size: number) {
+    return invoke<LaunchImageList>('/apis/admin/launch_img/page', {
+      method: 'POST',
+      data: {
+        current,
+        size,
+      },
+    });
+  }
+
+  public static async get(id: number) {
+    return invoke<LaunchImage>('/apis/admin/launch_img/details', {
+      method: 'GET',
+      params: {
+        id,
+      },
+    });
+  }
+
+  public static async remove(id: number) {
+    return invoke<void>(`/apis/admin/launch_img/remove?id=${id}`, {
+      method: 'DELETE',
+    });
+  }
+
+  public static async save(image: Partial<LaunchImage>) {
+    return invoke<LaunchImage>('/apis/admin/launch_img/save', {
+      method: 'POST',
+      data: image,
+    });
+  }
+}
+
+type Screen = {
+  id: number;
+  title: string;
+  image: string;
+  date: string[];
+};
+
+const Edit: React.FC<{ bundle: EditBundle; onSuccess?: () => void }> = (props) => {
+  const { bundle } = props;
+  const [form] = ProForm.useForm();
+  const [loading, setLoading] = useState<boolean>(false);
+
+  return (
+    <Modal
+      confirmLoading={loading}
+      open={bundle.open}
+      title={bundle.id ? '编辑开机屏' : '添加开机屏'}
+      onCancel={() => {
+        bundle.close();
+      }}
+      onClose={() => {
+        bundle.close();
+      }}
+      onOk={async () => {
+        await form.validateFields();
+
+        try {
+          setLoading(true);
+          const values = form.getFieldsValue();
+
+          const res = await Service.save({
+            id: bundle.id,
+            title: values.title,
+            uri: values.image.name,
+            effectiveTime: values.date[0].format('YYYY-MM-DD HH:mm:ss'),
+            takeDownTime: values.date[1].format('YYYY-MM-DD HH:mm:ss'),
+          });
+
+          if (!res.success) {
+            return;
+          }
+
+          props.onSuccess?.();
+          bundle.close();
+        } finally {
+          setLoading(false);
+        }
+      }}
+    >
+      <ProForm
+        className={'mt-5'}
+        form={form}
+        layout={'horizontal'}
+        labelCol={{ span: 4 }}
+        submitter={false}
+        request={async () => {
+          if (bundle.id) {
+            const data = await Service.get(bundle.id);
+
+            return {
+              title: data.data.title,
+              image: data.data.uri,
+              date: [data.data.effectiveTime, data.data.takeDownTime],
+            };
+          }
+
+          return Promise.resolve({
+            image: {
+              name: `screen/${v4()}`,
+            },
+          });
+        }}
+      >
+        <ProFormText name={'title'} label={'标题'} required={true} />
+        <ProForm.Item name={'image'} label={'图片'} required={true}>
+          <UIImage className={'h-[180px]'}></UIImage>
+        </ProForm.Item>
+        <ProFormDateRangePicker
+          name={'date'}
+          label={'生效时间'}
+          rules={[
+            {
+              required: true,
+              message: '请选择生效时间区间',
+            },
+          ]}
+        ></ProFormDateRangePicker>
+      </ProForm>
+    </Modal>
+  );
+};
+
+const Screens: React.FC<{}> = (props) => {
+  const edit = useEdit();
+  const ref = useRef<ActionType>();
+  const [records, setRecords] = useState<LaunchImage[]>([]);
+
+  const modeTable = () => {
+    return (
+      <ProTable<Screen>
+        rowKey={'id'}
+        request={async (params) => {
+          const res = await Service.list(params.current || 1, params.pageSize || 10);
+
+          setRecords(res.data.records);
+
+          console.log(res.data.records, 'records');
+
+          return {
+            success: res.success,
+            total: res.data.total,
+            data: res.data.records.map((record) => {
+              return {
+                id: record.id,
+                title: record.title,
+                image: record.uri,
+                date: [record.effectiveTime, record.takeDownTime],
+              };
+            }),
+          };
+        }}
+        columns={[
+          {
+            title: 'ID',
+            dataIndex: 'id',
+          },
+          {
+            title: '标题',
+            dataIndex: 'title',
+          },
+          {
+            title: '图片',
+            dataIndex: 'image',
+            render: (node, record) => {
+              return <img src={record.image} alt={record.title} className={'w-[64px]'} />;
+            },
+          },
+          {
+            title: '生效时间',
+            dataIndex: 'date',
+            valueType: 'dateRange',
+          },
+          {
+            title: '操作',
+            valueType: 'option',
+            render: (_, record) => [
+              <a key={'edit'} onClick={() => edit.update(record.id)}>
+                编辑
+              </a>,
+              <a
+                key={'delete'}
+                onClick={async () => {
+                  await Service.remove(record.id);
+                  ref.current?.reload();
+                }}
+              >
+                删除
+              </a>,
+            ],
+          },
+        ]}
+        actionRef={ref}
+        search={false}
+        toolbar={{
+          actions: [
+            <Button key="add" type="primary" icon={<AddOutline />} onClick={() => edit.create()}>
+              添加
+            </Button>,
+          ],
+          settings: [],
+        }}
+      ></ProTable>
+    );
+  };
+
+  const stringToColor = (str: string): string => {
+    // 计算字符串的哈希值
+    let hash = 0;
+    for (let i = 0; i < str.length; i++) {
+      const char = str.charCodeAt(i);
+      hash = (hash << 5) - hash + char;
+      hash |= 0; // 转换为32位整数
+    }
+
+    // 使用哈希值的低位来生成 RGB 颜色
+    let r = (hash & 0xff0000) >> 16;
+    let g = (hash & 0x00ff00) >> 8;
+    let b = hash & 0x0000ff;
+
+    // 混合颜色使其变浅
+    const mix = (color: number, base: number): number => Math.round((color + base) / 2.4);
+    r = mix(r, 255);
+    g = mix(g, 255);
+    b = mix(b, 255);
+
+    // 将 RGB 值转换为十六进制字符串,并确保两位数
+    const toHex = (n: number): string => n.toString(16).padStart(2, '0');
+
+    // 返回颜色的十六进制表示
+    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+  };
+
+  const modeCalendar = () => {
+    return (
+      <Calendar
+        cellRender={(date) => {
+          const items = records.filter((i) => {
+            const effect = dayjs(i.effectiveTime);
+            const take = dayjs(i.takeDownTime);
+
+            //date between [effect,take]
+            return date.isAfter(effect) && date.isBefore(take);
+          });
+
+          return (
+            <div className={'flex flex-col gap-2'}>
+              {items.map((i, index) => {
+                return (
+                  <Popover
+                    key={`${index}`}
+                    title={i.title}
+                    content={
+                      <div>
+                        <Image src={i.uri} width={120}></Image>
+                      </div>
+                    }
+                  >
+                    <Tag
+                      key={`${index}`}
+                      style={{ borderRadius: 5 }}
+                      color={stringToColor(i.title)}
+                      onClick={() => {
+                        console.log('click');
+                      }}
+                    >
+                      {i.title}
+                    </Tag>
+                  </Popover>
+                );
+              })}
+            </div>
+          );
+        }}
+      ></Calendar>
+    );
+  };
+
+  return (
+    <PageContainer>
+      {edit.open && <Edit bundle={edit} onSuccess={() => ref.current?.reload()}></Edit>}
+      <Card>
+        <Tabs
+          defaultActiveKey={'1'}
+          items={[
+            {
+              label: '表单模式',
+              key: '1',
+              children: modeTable(),
+            },
+            {
+              label: '日历模式',
+              key: '2',
+              children: modeCalendar(),
+            },
+          ]}
+        ></Tabs>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Screens;

+ 559 - 0
src/pages/Settings/Settings.tsx

@@ -0,0 +1,559 @@
+import React, { useRef } from 'react';
+import {
+  ActionType,
+  PageContainer,
+  ProForm,
+  ProFormDatePicker,
+  ProFormDateTimePicker,
+  ProFormDependency,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Card, Modal } from 'antd';
+import { invoke } from '@/core/network';
+import { AddOutline } from 'antd-mobile-icons';
+import { EditBundle, useEdit } from '@/core/hooks/useEdit';
+import dayjs from 'dayjs';
+import ReactJson from 'react-json-view';
+
+enum ConfigPermission {
+  /**
+   * 普通配置(游客/授权访问)
+   */
+  NORMAL = 10,
+  /**
+   * 业务配置(授权访问)
+   */
+  BUSINESS = 30,
+  /**
+   * 系统配置(不可访问,不可修改)
+   */
+  SYSTEM = 60,
+}
+
+type ConfigType = 'int' | 'long' | 'str' | 'bool' | 'date' | 'dateTime' | 'json' | 'array';
+
+export interface Config {
+  id: number;
+  type: ConfigType;
+  code: string;
+  remark: string;
+  content: string;
+  permission: ConfigPermission;
+}
+
+export interface ConfigList {
+  records: Config[];
+  total: number;
+  size: number;
+  current: number;
+}
+
+class Service {
+  public static async page(current: number, size: number) {
+    return invoke<ConfigList>('/apis/admin/config/page', {
+      method: 'POST',
+      data: {
+        current,
+        size,
+      },
+    });
+  }
+
+  //save
+  public static async save(config: Partial<Config>) {
+    return invoke<Config>('/apis/admin/config/save', {
+      method: 'POST',
+      data: config,
+    });
+  }
+
+  //get
+  public static async get(code: string) {
+    return invoke<Config>(`/apis/admin/config/get`, {
+      method: 'GET',
+      params: {
+        code,
+      },
+    });
+  }
+}
+
+const Edit: React.FC<{ bundle: EditBundle<string>; onSuccess?: () => void }> = (props) => {
+  const { bundle } = props;
+  const [form] = ProForm.useForm();
+
+  return (
+    <Modal
+      open={bundle.open}
+      title={bundle.id ? '编辑配置' : '添加配置'}
+      onOk={async () => {
+        const values = await form.validateFields();
+
+        console.log(values);
+
+        const res = await Service.save({
+          ...values,
+        });
+
+        if (!res.success) {
+          return;
+        }
+
+        props.onSuccess?.();
+        bundle.close();
+      }}
+      onCancel={() => {
+        bundle.close();
+      }}
+      onClose={() => {
+        bundle.close();
+      }}
+    >
+      <ProForm<Partial<Config>>
+        className={'mt-5'}
+        validateTrigger={false}
+        form={form}
+        submitter={false}
+        layout={'horizontal'}
+        labelCol={{ span: 4 }}
+        request={async (): Promise<Partial<Config>> => {
+          if (bundle.id) {
+            const res = await Service.get(bundle.id);
+
+            return {
+              code: res.data.code,
+              type: res.data.type,
+              remark: res.data.remark,
+              content: res.data.content,
+            };
+          } else {
+            return {
+              type: 'str',
+            };
+          }
+        }}
+      >
+        <ProFormText
+          disabled={bundle.id !== undefined}
+          name={'code'}
+          label={'编码'}
+          rules={[
+            {
+              required: true,
+              message: '请输入配置编码',
+            },
+          ]}
+        />
+        <ProFormSelect
+          fieldProps={{ allowClear: false }}
+          rules={[
+            {
+              required: true,
+              message: '请选择权限',
+            },
+          ]}
+          name={'permission'}
+          label={'权限'}
+          options={[
+            {
+              label: '普通配置(游客/授权访问)',
+              value: ConfigPermission.NORMAL,
+            },
+            {
+              label: '业务配置(授权访问)',
+              value: ConfigPermission.BUSINESS,
+            },
+          ]}
+          initialValue={ConfigPermission.NORMAL}
+        />
+        <ProFormText name={'remark'} label={'说明'} />
+        <ProFormSelect
+          fieldProps={{
+            allowClear: false,
+          }}
+          name={'type'}
+          label={'类型'}
+          rules={[
+            {
+              required: true,
+              message: '请选择配置类型',
+            },
+          ]}
+          options={[
+            {
+              label: '整数',
+              value: 'int',
+            },
+            {
+              label: '长整数',
+              value: 'long',
+            },
+            {
+              label: '字符串',
+              value: 'str',
+            },
+            {
+              label: '布尔',
+              value: 'bool',
+            },
+            {
+              label: '日期',
+              value: 'date',
+            },
+            {
+              label: '日期时间',
+              value: 'dateTime',
+            },
+            {
+              label: 'JSON',
+              value: 'json',
+            },
+            {
+              label: '数组',
+              value: 'array',
+            },
+          ]}
+        ></ProFormSelect>
+        <ProFormDependency name={['type']}>
+          {({ type }) => {
+            const which = type as ConfigType;
+
+            switch (which) {
+              case 'long':
+              case 'int':
+                return (
+                  <ProFormText
+                    name={'content'}
+                    label={'内容'}
+                    placeholder={'请输入整数'}
+                    fieldProps={{
+                      type: 'number',
+                    }}
+                    initialValue={0}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入整数',
+                      },
+                    ]}
+                  />
+                );
+              case 'str':
+                return (
+                  <ProFormText
+                    name={'content'}
+                    label={'内容'}
+                    placeholder={'请输入字符串'}
+                    initialValue={''}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入字符串',
+                      },
+                      {
+                        min: 1,
+                        message: '字符串长度不能小于1',
+                      },
+                    ]}
+                  />
+                );
+              case 'bool':
+                return (
+                  <ProFormSelect
+                    fieldProps={{
+                      allowClear: false,
+                    }}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请选择布尔值',
+                      },
+                    ]}
+                    initialValue={true}
+                    name={'content'}
+                    label={'内容'}
+                    options={[
+                      {
+                        label: '是',
+                        value: 'true',
+                      },
+                      {
+                        label: '否',
+                        value: 'false',
+                      },
+                    ]}
+                  ></ProFormSelect>
+                );
+              case 'date':
+                form.setFieldValue('content', new Date());
+                return (
+                  <ProFormDatePicker
+                    name={'content'}
+                    label={'内容'}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请选择日期',
+                      },
+                    ]}
+                  />
+                );
+              case 'dateTime':
+                form.setFieldValue('content', new Date());
+                return (
+                  <ProFormDateTimePicker
+                    name={'content'}
+                    label={'内容'}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请选择日期时间',
+                      },
+                    ]}
+                  />
+                );
+              case 'json':
+                form.setFieldValue('content', '{}');
+                return (
+                  <ProFormTextArea
+                    name={'content'}
+                    label={'内容'}
+                    placeholder={'请输入JSON'}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入JSON',
+                      },
+                      () => ({
+                        validator(_, value) {
+                          try {
+                            const v = JSON.parse(value);
+
+                            if (Array.isArray(v)) {
+                              return Promise.reject('JSON不能是数组');
+                            }
+
+                            if (typeof v !== 'object') {
+                              return Promise.reject('JSON必须是对象');
+                            }
+
+                            return Promise.resolve();
+                          } catch (e) {
+                            return Promise.reject('请输入正确的JSON');
+                          }
+                        },
+                      }),
+                    ]}
+                  />
+                );
+              case 'array':
+                form.setFieldValue('content', '[]');
+                return (
+                  <ProFormTextArea
+                    name={'content'}
+                    label={'内容'}
+                    placeholder={'请输入数组'}
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入数组',
+                      },
+                      () => ({
+                        validator(_, value) {
+                          try {
+                            const v = JSON.parse(value);
+
+                            if (!Array.isArray(v)) {
+                              return Promise.reject('内容必须是数组');
+                            }
+
+                            return Promise.resolve();
+                          } catch (e) {
+                            return Promise.reject('请输入正确的数组');
+                          }
+                        },
+                      }),
+                    ]}
+                  />
+                );
+              default:
+                return <></>;
+            }
+          }}
+        </ProFormDependency>
+      </ProForm>
+    </Modal>
+  );
+};
+
+const JSONView: React.FC<{ value: string }> = (props) => {
+  const [open, setOpen] = React.useState(false);
+
+  return (
+    <>
+      {open && (
+        <Modal
+          title={'JSON'}
+          open={open}
+          onClose={() => setOpen(false)}
+          footer={false}
+          onCancel={() => setOpen(false)}
+        >
+          <ReactJson src={JSON.parse(props.value)}></ReactJson>
+        </Modal>
+      )}
+      <a
+        onClick={() => {
+          setOpen(true);
+        }}
+      >
+        查看
+      </a>
+    </>
+  );
+};
+
+const Settings: React.FC = () => {
+  const edit = useEdit<string>();
+  const ref = useRef<ActionType>();
+
+  return (
+    <PageContainer>
+      {edit.open && (
+        <Edit
+          bundle={edit}
+          onSuccess={() => {
+            ref.current?.reload();
+          }}
+        />
+      )}
+      <Card>
+        <ProTable<Config>
+          actionRef={ref}
+          search={false}
+          columns={[
+            {
+              title: '编码',
+              dataIndex: 'code',
+            },
+            {
+              title: '类型',
+              dataIndex: 'type',
+              render: (_, record) => {
+                switch (record.type) {
+                  case 'int':
+                    return '整数';
+                  case 'long':
+                    return '长整数';
+                  case 'str':
+                    return '字符串';
+                  case 'bool':
+                    return '布尔';
+                  case 'date':
+                    return '日期';
+                  case 'dateTime':
+                    return '日期时间';
+                  case 'json':
+                    return 'JSON';
+                  case 'array':
+                    return '数组';
+                  default:
+                    return '未知';
+                }
+              },
+            },
+
+            {
+              title: '说明',
+              dataIndex: 'remark',
+            },
+            {
+              title: '内容',
+              dataIndex: 'content',
+              render: (_, record) => {
+                switch (record.type) {
+                  case 'json':
+                  case 'array':
+                    return <JSONView value={record.content}></JSONView>;
+                  case 'date':
+                    return dayjs(record.content).format('YYYY-MM-DD');
+                  case 'dateTime':
+                    return dayjs(record.content).format('YYYY-MM-DD HH:mm:ss');
+                  default:
+                    return record.content;
+                }
+              },
+            },
+            {
+              title: '权限',
+              dataIndex: 'permission',
+              render: (_, record) => {
+                switch (record.permission) {
+                  case ConfigPermission.NORMAL:
+                    return '普通配置';
+                  case ConfigPermission.BUSINESS:
+                    return '业务配置';
+                  case ConfigPermission.SYSTEM:
+                    return '系统配置';
+                  default:
+                    return '未知';
+                }
+              },
+            },
+            {
+              title: '操作',
+              valueType: 'option',
+              render: (_, record) => {
+                const opts = [];
+
+                if (record.permission !== ConfigPermission.SYSTEM) {
+                  opts.push(
+                    <a
+                      key={'edit'}
+                      onClick={() => {
+                        edit.update(record.code);
+                      }}
+                    >
+                      编辑
+                    </a>,
+                  );
+                }
+
+                return opts;
+              },
+            },
+          ]}
+          toolbar={{
+            actions: [
+              <Button
+                type={'primary'}
+                onClick={() => {
+                  edit.create();
+                }}
+                icon={<AddOutline />}
+                key={'add'}
+              >
+                添加
+              </Button>,
+            ],
+            settings: [],
+          }}
+          request={async (params, sort, filter) => {
+            const res = await Service.page(params.current || 1, params.pageSize || 20);
+
+            return {
+              data: res.data.records,
+              total: res.data.current,
+              success: true,
+            };
+          }}
+        ></ProTable>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Settings;

+ 236 - 0
src/pages/Settings/Staff.tsx

@@ -0,0 +1,236 @@
+import {
+  ActionType,
+  PageContainer,
+  ProForm,
+  ProFormText,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Card, Modal } from 'antd';
+import React, { useRef } from 'react';
+import { AddOutline } from 'antd-mobile-icons';
+import { EditBundle, useEdit } from '@/core/hooks/useEdit';
+import { invoke } from '@/core/network';
+
+type Member = {
+  id?: number;
+  username: string;
+  password: string;
+  nickname: string;
+  thumbAvatar?: string;
+  level: number;
+};
+
+type MemberList = {
+  records: Member[];
+  total: number;
+  size: number;
+  current: number;
+};
+
+class Service {
+  public static async list(current: number, size: number) {
+    return invoke<MemberList>('/apis/admin/staff/page', {
+      method: 'POST',
+      data: {
+        current,
+        size,
+      },
+    });
+  }
+
+  public static async remove(id: number) {
+    return invoke<void>(`/apis/admin/staff/remove?id=${id}`, {
+      method: 'DELETE',
+    });
+  }
+
+  public static async save(member: Partial<Member>) {
+    return invoke<Member>('/apis/admin/staff/save', {
+      method: 'POST',
+      data: member,
+    });
+  }
+
+  public static async get(id: number) {
+    return invoke<Member>(`/apis/admin/staff/details?id=${id}`, {
+      method: 'GET',
+    });
+  }
+}
+
+const Edit: React.FC<{ bundle: EditBundle; onSuccess?: () => void }> = (props) => {
+  const { bundle } = props;
+  const [form] = ProForm.useForm<Member>();
+  const [loading, setLoading] = React.useState(false);
+
+  return (
+    <Modal
+      confirmLoading={loading}
+      title={bundle.id ? '编辑管理员' : '添加管理员'}
+      open={bundle.open}
+      onOk={async () => {
+        await form.validateFields();
+
+        try {
+          setLoading(true);
+
+          await Service.save({
+            id: bundle.id,
+            ...form.getFieldsValue(),
+            thumbAvatar: '',
+            level: 1,
+          });
+        } finally {
+          setLoading(false);
+        }
+
+        props.onSuccess?.();
+        bundle.close();
+      }}
+      onClose={() => {
+        form.resetFields();
+        bundle.close();
+      }}
+      onCancel={() => {
+        form.resetFields();
+        bundle.close();
+      }}
+    >
+      <ProForm
+        className={'mt-5'}
+        form={form}
+        labelCol={{ span: 4 }}
+        layout={'horizontal'}
+        submitter={false}
+        request={async () => {
+          if (bundle.id !== undefined) {
+            console.log(bundle.id, 'bundle');
+            const res = await Service.get(bundle.id);
+
+            return res.data;
+          }
+
+          return Promise.resolve({});
+        }}
+      >
+        <ProFormText
+          name={'username'}
+          label={'账号'}
+          rules={[
+            {
+              required: true,
+              message: '请输入账号',
+            },
+            {
+              min: 3,
+              max: 20,
+              message: '账号长度必须在3-20之间',
+            },
+          ]}
+          placeholder={'请输入账号'}
+        ></ProFormText>
+        <ProFormText
+          name="nickname"
+          label="姓名"
+          rules={[
+            {
+              required: true,
+              message: '请输入姓名',
+            },
+            {
+              min: 2,
+              max: 20,
+              message: '姓名长度必须在2-20之间',
+            },
+          ]}
+          placeholder={'请输入用户姓名'}
+        />
+        <ProFormText.Password
+          name="password"
+          label="密码"
+          placeholder={'请输入密码'}
+          rules={[
+            {
+              required: true,
+              message: '请输入密码',
+            },
+            {
+              min: 6,
+              max: 50,
+              message: '密码长度必须在6-50之间',
+            },
+          ]}
+        />
+      </ProForm>
+    </Modal>
+  );
+};
+
+const Staff: React.FC = () => {
+  const bundle = useEdit();
+  const ref = useRef<ActionType>();
+
+  return (
+    <PageContainer content={''}>
+      {bundle.open && <Edit bundle={bundle} onSuccess={() => ref.current?.reload()}></Edit>}
+      <Card>
+        <ProTable<Member>
+          rowKey={'id'}
+          request={async (params) => {
+            const res = await Service.list(params.current || 1, params.pageSize || 10);
+
+            return {
+              success: res.success,
+              total: res.data.total,
+              data: res.data.records,
+            };
+          }}
+          columns={[
+            {
+              title: 'ID',
+              dataIndex: 'id',
+            },
+            {
+              title: '账号',
+              dataIndex: 'username',
+            },
+            {
+              title: '姓名',
+              dataIndex: 'nickname',
+            },
+            {
+              title: '操作',
+              valueType: 'option',
+              render: (_, record) => [
+                <a key={'edit'} onClick={() => bundle.update(record.id as number)}>
+                  编辑
+                </a>,
+                <a
+                  key={'delete'}
+                  onClick={async () => {
+                    await Service.remove(record.id as number);
+                    ref.current?.reload();
+                  }}
+                >
+                  删除
+                </a>,
+              ],
+            },
+          ]}
+          actionRef={ref}
+          toolbar={{
+            actions: [
+              <Button icon={<AddOutline />} key={'add'} type={'primary'} onClick={bundle.create}>
+                添加
+              </Button>,
+            ],
+            settings: [],
+          }}
+          search={false}
+        ></ProTable>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Staff;

+ 120 - 0
src/pages/Settings/Users.tsx

@@ -0,0 +1,120 @@
+import React, { useRef } from 'react';
+import { ActionType, PageContainer, ProTable } from '@ant-design/pro-components';
+import { Card, Image, Popconfirm } from 'antd';
+import { invoke } from '@/core/network';
+
+type User = {
+  id: number;
+  mobile: string;
+  nickname: string;
+  avatarThumbnail: string;
+  avatarOriginal: string;
+};
+
+type UserList = {
+  records: User[];
+  total: number;
+  size: number;
+  current: number;
+};
+
+class Service {
+  public static async page(current: number, size: number) {
+    return invoke<UserList>('/apis/admin/user/page', {
+      method: 'POST',
+      data: {
+        current,
+        size,
+      },
+    });
+  }
+
+  public static async remove(id: number) {
+    return invoke<void>(`/apis/admin/user/remove?id=${id}`, {
+      method: 'DELETE',
+    });
+  }
+
+  public static async get(id: number) {
+    return invoke<User>(`/apis/admin/user/details?id=${id}`, {
+      method: 'GET',
+    });
+  }
+}
+
+const Users: React.FC<{}> = (props) => {
+  const ref = useRef<ActionType>();
+
+  return (
+    <PageContainer>
+      <Card>
+        <ProTable<User>
+          actionRef={ref}
+          rowKey="id"
+          search={false}
+          toolbar={{
+            actions: [],
+            settings: [],
+          }}
+          request={async (params) => {
+            const { current, pageSize } = params;
+            const result = await Service.page(current || 1, pageSize || 20);
+            return {
+              data: result.data.records,
+              total: result.data.total,
+              success: result.success,
+            };
+          }}
+          columns={[
+            {
+              title: 'ID',
+              dataIndex: 'id',
+            },
+            {
+              title: '头像',
+              dataIndex: 'avatarThumbnail',
+              render: (text, record) => {
+                return <Image src={record.avatarThumbnail} width={50}></Image>;
+              },
+            },
+            {
+              title: '手机号码',
+              dataIndex: 'mobile',
+            },
+            {
+              title: '昵称',
+              dataIndex: 'nickname',
+            },
+            {
+              title: '操作',
+              valueType: 'option',
+              render: (_, record) => [
+                <a
+                  key={'edit'}
+                  onClick={async () => {
+                    const user = await Service.get(record.id);
+                    console.log(user);
+                  }}
+                >
+                  编辑
+                </a>,
+                <Popconfirm
+                  key={'delete'}
+                  title={'确认删除吗?'}
+                  onConfirm={async () => {
+                    await Service.remove(record.id);
+                    ref.current?.reload();
+                  }}
+                >
+                  <a key={'delete'}>删除</a>
+                </Popconfirm>,
+              ],
+            },
+          ]}
+        ></ProTable>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Users;

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

@@ -0,0 +1,209 @@
+import {
+  ProFormDateTimePicker,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  StepsForm,
+} from '@ant-design/pro-components';
+import { FormattedMessage, useIntl } from '@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>;
+  updateModalOpen: boolean;
+  values: Partial<API.RuleListItem>;
+};
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+  const intl = useIntl();
+  return (
+    <StepsForm
+      stepsProps={{
+        size: 'small',
+      }}
+      stepsFormRender={(dom, submitter) => {
+        return (
+          <Modal
+            width={640}
+            bodyStyle={{ padding: '32px 40px 48px' }}
+            destroyOnClose
+            title={intl.formatMessage({
+              id: 'pages.searchTable.updateForm.ruleConfig',
+              defaultMessage: '规则配置',
+            })}
+            open={props.updateModalOpen}
+            footer={submitter}
+            onCancel={() => {
+              props.onCancel();
+            }}
+          >
+            {dom}
+          </Modal>
+        );
+      }}
+      onFinish={props.onSubmit}
+    >
+      <StepsForm.StepForm
+        initialValues={{
+          name: props.values.name,
+          desc: props.values.desc,
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.basicConfig',
+          defaultMessage: '基本信息',
+        })}
+      >
+        <ProFormText
+          name="name"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleName.nameLabel',
+            defaultMessage: '规则名称',
+          })}
+          width="md"
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.ruleName.nameRules"
+                  defaultMessage="请输入规则名称!"
+                />
+              ),
+            },
+          ]}
+        />
+        <ProFormTextArea
+          name="desc"
+          width="md"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleDesc.descLabel',
+            defaultMessage: '规则描述',
+          })}
+          placeholder={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleDesc.descPlaceholder',
+            defaultMessage: '请输入至少五个字符',
+          })}
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.ruleDesc.descRules"
+                  defaultMessage="请输入至少五个字符的规则描述!"
+                />
+              ),
+              min: 5,
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          target: '0',
+          template: '0',
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.ruleProps.title',
+          defaultMessage: '配置规则属性',
+        })}
+      >
+        <ProFormSelect
+          name="target"
+          width="md"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.object',
+            defaultMessage: '监控对象',
+          })}
+          valueEnum={{
+            0: '表一',
+            1: '表二',
+          }}
+        />
+        <ProFormSelect
+          name="template"
+          width="md"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleProps.templateLabel',
+            defaultMessage: '规则模板',
+          })}
+          valueEnum={{
+            0: '规则模板一',
+            1: '规则模板二',
+          }}
+        />
+        <ProFormRadio.Group
+          name="type"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleProps.typeLabel',
+            defaultMessage: '规则类型',
+          })}
+          options={[
+            {
+              value: '0',
+              label: '强',
+            },
+            {
+              value: '1',
+              label: '弱',
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          type: '1',
+          frequency: 'month',
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.schedulingPeriod.title',
+          defaultMessage: '设定调度周期',
+        })}
+      >
+        <ProFormDateTimePicker
+          name="time"
+          width="md"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.schedulingPeriod.timeLabel',
+            defaultMessage: '开始时间',
+          })}
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.schedulingPeriod.timeRules"
+                  defaultMessage="请选择开始时间!"
+                />
+              ),
+            },
+          ]}
+        />
+        <ProFormSelect
+          name="frequency"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.object',
+            defaultMessage: '监控对象',
+          })}
+          width="md"
+          valueEnum={{
+            month: '月',
+            week: '周',
+          }}
+        />
+      </StepsForm.StepForm>
+    </StepsForm>
+  );
+};
+
+export default UpdateForm;

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

@@ -0,0 +1,397 @@
+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 { FormattedMessage, useIntl } from '@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 [createModalOpen, handleModalOpen] = useState<boolean>(false);
+  /**
+   * @en-US The pop-up window of the distribution update window
+   * @zh-CN 分布更新窗口的弹窗
+   * */
+  const [updateModalOpen, handleUpdateModalOpen] = 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 intl = useIntl();
+
+  const columns: ProColumns<API.RuleListItem>[] = [
+    {
+      title: (
+        <FormattedMessage
+          id="pages.searchTable.updateForm.ruleName.nameLabel"
+          defaultMessage="Rule name"
+        />
+      ),
+      dataIndex: 'name',
+      tip: 'The rule name is the unique key',
+      render: (dom, entity) => {
+        return (
+          <a
+            onClick={() => {
+              setCurrentRow(entity);
+              setShowDetail(true);
+            }}
+          >
+            {dom}
+          </a>
+        );
+      },
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleDesc" defaultMessage="Description" />,
+      dataIndex: 'desc',
+      valueType: 'textarea',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="pages.searchTable.titleCallNo"
+          defaultMessage="Number of service calls"
+        />
+      ),
+      dataIndex: 'callNo',
+      sorter: true,
+      hideInForm: true,
+      renderText: (val: string) =>
+        `${val}${intl.formatMessage({
+          id: 'pages.searchTable.tenThousand',
+          defaultMessage: ' 万 ',
+        })}`,
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleStatus" defaultMessage="Status" />,
+      dataIndex: 'status',
+      hideInForm: true,
+      valueEnum: {
+        0: {
+          text: (
+            <FormattedMessage
+              id="pages.searchTable.nameStatus.default"
+              defaultMessage="Shut down"
+            />
+          ),
+          status: 'Default',
+        },
+        1: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.running" defaultMessage="Running" />
+          ),
+          status: 'Processing',
+        },
+        2: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.online" defaultMessage="Online" />
+          ),
+          status: 'Success',
+        },
+        3: {
+          text: (
+            <FormattedMessage
+              id="pages.searchTable.nameStatus.abnormal"
+              defaultMessage="Abnormal"
+            />
+          ),
+          status: 'Error',
+        },
+      },
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="pages.searchTable.titleUpdatedAt"
+          defaultMessage="Last scheduled time"
+        />
+      ),
+      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={intl.formatMessage({
+                id: 'pages.searchTable.exception',
+                defaultMessage: 'Please enter the reason for the exception!',
+              })}
+            />
+          );
+        }
+        return defaultRender(item);
+      },
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="Operating" />,
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => [
+        <a
+          key="config"
+          onClick={() => {
+            handleUpdateModalOpen(true);
+            setCurrentRow(record);
+          }}
+        >
+          <FormattedMessage id="pages.searchTable.config" defaultMessage="Configuration" />
+        </a>,
+        <a key="subscribeAlert" href="https://procomponents.ant.design/">
+          <FormattedMessage
+            id="pages.searchTable.subscribeAlert"
+            defaultMessage="Subscribe to alerts"
+          />
+        </a>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <ProTable<API.RuleListItem, API.PageParams>
+        headerTitle={intl.formatMessage({
+          id: 'pages.searchTable.title',
+          defaultMessage: 'Enquiry form',
+        })}
+        actionRef={actionRef}
+        rowKey="key"
+        search={{
+          labelWidth: 120,
+        }}
+        toolBarRender={() => [
+          <Button
+            type="primary"
+            key="primary"
+            onClick={() => {
+              handleModalOpen(true);
+            }}
+          >
+            <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
+          </Button>,
+        ]}
+        request={rule}
+        columns={columns}
+        rowSelection={{
+          onChange: (_, selectedRows) => {
+            setSelectedRows(selectedRows);
+          },
+        }}
+      />
+      {selectedRowsState?.length > 0 && (
+        <FooterToolbar
+          extra={
+            <div>
+              <FormattedMessage id="pages.searchTable.chosen" defaultMessage="Chosen" />{' '}
+              <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
+              <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+              &nbsp;&nbsp;
+              <span>
+                <FormattedMessage
+                  id="pages.searchTable.totalServiceCalls"
+                  defaultMessage="Total number of service calls"
+                />{' '}
+                {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}{' '}
+                <FormattedMessage id="pages.searchTable.tenThousand" defaultMessage="万" />
+              </span>
+            </div>
+          }
+        >
+          <Button
+            onClick={async () => {
+              await handleRemove(selectedRowsState);
+              setSelectedRows([]);
+              actionRef.current?.reloadAndRest?.();
+            }}
+          >
+            <FormattedMessage
+              id="pages.searchTable.batchDeletion"
+              defaultMessage="Batch deletion"
+            />
+          </Button>
+          <Button type="primary">
+            <FormattedMessage
+              id="pages.searchTable.batchApproval"
+              defaultMessage="Batch approval"
+            />
+          </Button>
+        </FooterToolbar>
+      )}
+      <ModalForm
+        title={intl.formatMessage({
+          id: 'pages.searchTable.createForm.newRule',
+          defaultMessage: 'New rule',
+        })}
+        width="400px"
+        open={createModalOpen}
+        onOpenChange={handleModalOpen}
+        onFinish={async (value) => {
+          const success = await handleAdd(value as API.RuleListItem);
+          if (success) {
+            handleModalOpen(false);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+      >
+        <ProFormText
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.ruleName"
+                  defaultMessage="Rule name is required"
+                />
+              ),
+            },
+          ]}
+          width="md"
+          name="name"
+        />
+        <ProFormTextArea width="md" name="desc" />
+      </ModalForm>
+      <UpdateForm
+        onSubmit={async (value) => {
+          const success = await handleUpdate(value);
+          if (success) {
+            handleUpdateModalOpen(false);
+            setCurrentRow(undefined);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+        onCancel={() => {
+          handleUpdateModalOpen(false);
+          if (!showDetail) {
+            setCurrentRow(undefined);
+          }
+        }}
+        updateModalOpen={updateModalOpen}
+        values={currentRow || {}}
+      />
+
+      <Drawer
+        width={600}
+        open={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;

Plik diff jest za duży
+ 1108 - 0
src/pages/User/Login/__snapshots__/login.test.tsx.snap


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

@@ -0,0 +1,219 @@
+import { LockOutlined, UserOutlined } from '@ant-design/icons';
+import { LoginForm, ProFormText } from '@ant-design/pro-components';
+import { FormattedMessage, Helmet, history, useIntl, useModel } from '@umijs/max';
+import { Alert, message, Tabs } from 'antd';
+import Settings from '../../../../config/defaultSettings';
+import React, { useState } from 'react';
+import { flushSync } from 'react-dom';
+import { createStyles } from 'antd-style';
+import { LoginParams, Service } from '@/pages/User/Login/service';
+
+const useStyles = createStyles(({ token }) => {
+  return {
+    action: {
+      marginLeft: '8px',
+      color: 'rgba(0, 0, 0, 0.2)',
+      fontSize: '24px',
+      verticalAlign: 'middle',
+      cursor: 'pointer',
+      transition: 'color 0.3s',
+      '&:hover': {
+        color: token.colorPrimaryActive,
+      },
+    },
+    lang: {
+      width: 42,
+      height: 42,
+      lineHeight: '42px',
+      position: 'fixed',
+      right: 16,
+      borderRadius: token.borderRadius,
+      ':hover': {
+        backgroundColor: token.colorBgTextHover,
+      },
+    },
+    container: {
+      display: 'flex',
+      flexDirection: 'column',
+      height: '100vh',
+      overflow: 'auto',
+      backgroundImage:
+        "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
+      backgroundSize: '100% 100%',
+    },
+  };
+});
+
+const 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 { styles } = useStyles();
+  const intl = useIntl();
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      flushSync(() => {
+        setInitialState((s) => ({
+          ...s,
+          currentUser: userInfo,
+        }));
+      });
+    }
+  };
+
+  const handleSubmit = async (values: LoginParams) => {
+    try {
+      // 登录
+      const msg = await Service.login({ ...values });
+
+      console.log('msg', msg);
+
+      if (msg.success) {
+        localStorage.setItem('token', msg.data);
+
+        message.success('登陆成功!');
+        await fetchUserInfo();
+        const urlParams = new URL(window.location.href).searchParams;
+        history.push(urlParams.get('redirect') || '/');
+        return;
+      }
+      console.log(msg);
+      // 如果失败去设置用户错误信息
+      setUserLoginState({
+        type: 'account',
+        status: 'error',
+      });
+    } catch (error) {
+      const defaultLoginFailureMessage = intl.formatMessage({
+        id: 'pages.login.failure',
+        defaultMessage: '登录失败,请重试!',
+      });
+      console.log(error);
+      message.error(defaultLoginFailureMessage);
+    }
+  };
+  const { status, type: loginType } = userLoginState;
+
+  return (
+    <div className={styles.container}>
+      <Helmet>
+        <title>
+          {intl.formatMessage({
+            id: 'menu.login',
+            defaultMessage: '登录页',
+          })}
+          - {Settings.title}
+        </title>
+      </Helmet>
+      <div
+        style={{
+          flex: '1',
+          padding: '32px 0',
+        }}
+      >
+        <LoginForm
+          contentStyle={{
+            minWidth: 280,
+            maxWidth: '75vw',
+          }}
+          logo={<img alt="logo" src="/logo.svg" />}
+          title="魔饭星"
+          subTitle={'欢迎登录魔饭星管理后台'}
+          initialValues={{
+            autoLogin: true,
+          }}
+          actions={[]}
+          onFinish={async (values) => {
+            await handleSubmit(values as LoginParams);
+          }}
+        >
+          <Tabs
+            activeKey={type}
+            onChange={setType}
+            centered
+            items={[
+              {
+                key: 'account',
+                label: intl.formatMessage({
+                  id: 'pages.login.accountLogin.tab',
+                  defaultMessage: '账户密码登录',
+                }),
+              },
+            ]}
+          />
+
+          {status === 'error' && loginType === 'account' && (
+            <LoginMessage content={'账户或密码错误'} />
+          )}
+          {type === 'account' && (
+            <>
+              <ProFormText
+                name="username"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <UserOutlined />,
+                }}
+                placeholder={'请输入用户名'}
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.username.required"
+                        defaultMessage="请输入用户名!"
+                      />
+                    ),
+                  },
+                ]}
+              />
+              <ProFormText.Password
+                name="password"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <LockOutlined />,
+                }}
+                placeholder={'请输入密码'}
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.password.required"
+                        defaultMessage="请输入密码!"
+                      />
+                    ),
+                  },
+                ]}
+              />
+            </>
+          )}
+          <div
+            style={{
+              marginBottom: 24,
+            }}
+          ></div>
+        </LoginForm>
+      </div>
+      {/*<Footer />*/}
+    </div>
+  );
+};
+
+export default Login;

+ 96 - 0
src/pages/User/Login/login.test.tsx

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

+ 27 - 0
src/pages/User/Login/service.ts

@@ -0,0 +1,27 @@
+import { invoke } from '@/core/network';
+
+export interface LoginParams {
+  username: string;
+  password: string;
+}
+
+export interface Me {
+  id: number;
+  name: string;
+  nickname: string;
+}
+
+export class Service {
+  public static async login(params: LoginParams) {
+    return invoke<string>('/apis/admin/staff/login', {
+      method: 'POST',
+      data: params,
+    });
+  }
+
+  public static async me() {
+    return invoke<Me>('/apis/admin/staff/me', {
+      skipErrorHandler: true,
+    });
+  }
+}

+ 164 - 0
src/pages/Welcome.tsx

@@ -0,0 +1,164 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { useModel } from '@umijs/max';
+import { Card, theme } 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 }) => {
+  const { useToken } = theme;
+
+  const { token } = useToken();
+
+  return (
+    <div
+      style={{
+        backgroundColor: token.colorBgContainer,
+        boxShadow: token.boxShadow,
+        borderRadius: '8px',
+        fontSize: '14px',
+        color: token.colorTextSecondary,
+        lineHeight: '22px',
+        padding: '16px 19px',
+        minWidth: '220px',
+        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: token.colorText,
+            paddingBottom: 8,
+          }}
+        >
+          {title}
+        </div>
+      </div>
+      <div
+        style={{
+          fontSize: '14px',
+          color: token.colorTextSecondary,
+          textAlign: 'justify',
+          lineHeight: '22px',
+          marginBottom: 8,
+        }}
+      >
+        {desc}
+      </div>
+      <a href={href} target="_blank" rel="noreferrer">
+        了解更多 {'>'}
+      </a>
+    </div>
+  );
+};
+
+const Welcome: React.FC = () => {
+  const { token } = theme.useToken();
+  const { initialState } = useModel('@@initialState');
+  return (
+    <PageContainer>
+      <Card
+        style={{
+          borderRadius: 8,
+        }}
+        bodyStyle={{
+          backgroundImage:
+            initialState?.settings?.navTheme === 'realDark'
+              ? 'background-image: linear-gradient(75deg, #1A1B1F 0%, #191C1F 100%)'
+              : 'background-image: linear-gradient(75deg, #FBFDFF 0%, #F5F7FF 100%)',
+        }}
+      >
+        <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: token.colorTextHeading,
+            }}
+          >
+            欢迎使用 Ant Design Pro
+          </div>
+          <p
+            style={{
+              fontSize: '14px',
+              color: token.colorTextSecondary,
+              lineHeight: '22px',
+              marginTop: 16,
+              marginBottom: 32,
+              width: '65%',
+            }}
+          >
+            Ant Design Pro 是一个整合了 umi,Ant Design 和 ProComponents
+            的脚手架方案。致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套设计资源,进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。
+          </p>
+          <div
+            style={{
+              display: 'flex',
+              flexWrap: 'wrap',
+              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;

+ 119 - 0
src/requestErrorConfig.ts

@@ -0,0 +1,119 @@
+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 {
+  success: boolean;
+  data: any;
+  errorCode?: number;
+  errorMessage?: string;
+  showType?: ErrorShowType;
+}
+
+/**
+ * @name 错误处理
+ * pro 自带的错误处理, 可以在这里做自己的改动
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+export const errorConfig: RequestConfig = {
+  // 错误处理: umi@3 的错误处理方案。
+  errorConfig: {
+    // 错误抛出
+    errorThrower: (res) => {
+      const { success, data, errorCode, errorMessage, showType } =
+        res as unknown as ResponseStructure;
+      if (!success) {
+        const error: any = new Error(errorMessage);
+        error.name = 'BizError';
+        error.info = { errorCode, errorMessage, showType, 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 { errorMessage, errorCode } = errorInfo;
+          switch (errorInfo.showType) {
+            case ErrorShowType.SILENT:
+              // do nothing
+              break;
+            case ErrorShowType.WARN_MESSAGE:
+              message.warning(errorMessage);
+              break;
+            case ErrorShowType.ERROR_MESSAGE:
+              message.error(errorMessage);
+              break;
+            case ErrorShowType.NOTIFICATION:
+              notification.open({
+                description: errorMessage,
+                message: errorCode,
+              });
+              break;
+            case ErrorShowType.REDIRECT:
+              // TODO: redirect
+              break;
+            default:
+              message.error(errorMessage);
+          }
+        }
+      } 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('None response! Please retry.');
+      } else {
+        // 发送请求时出了点问题
+        message.error('Request error, please retry.');
+      }
+    },
+  },
+
+  // 请求拦截器
+  requestInterceptors: [
+    (config: RequestOptions) => {
+      console.log('req', config);
+      const token = localStorage.getItem('token');
+
+      if (token !== null) {
+        config.headers = {
+          token,
+          ...config.headers,
+        };
+      }
+
+      return config;
+    },
+  ],
+
+  // 响应拦截器
+  responseInterceptors: [
+    (response) => {
+      console.log('resp', response);
+
+      // 拦截响应数据,进行个性化处理
+      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,
+          });
+        },
+      ),
+    );
+  }
+});

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

@@ -0,0 +1,94 @@
+// @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: 'POST',
+    data:{
+      method: 'update',
+      ...(options || {}),
+    }
+  });
+}
+
+/** 新建规则 POST /api/rule */
+export async function addRule(options?: { [key: string]: any }) {
+  return request<API.RuleListItem>('/api/rule', {
+    method: 'POST',
+    data:{
+      method: 'post',
+      ...(options || {}),
+    }
+  });
+}
+
+/** 删除规则 DELETE /api/rule */
+export async function removeRule(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/api/rule', {
+    method: 'POST',
+    data:{
+      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 || {}),
+  });
+}

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

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

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

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

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

@@ -0,0 +1,153 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from '@umijs/max';
+
+/** Update an existing pet PUT /pet */
+export async function updatePet(body: API.Pet, options?: { [key: string]: any }) {
+  return request<any>('/pet', {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Add a new pet to the store POST /pet */
+export async function addPet(body: API.Pet, options?: { [key: string]: any }) {
+  return request<any>('/pet', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Find pet by ID Returns a single pet GET /pet/${param0} */
+export async function getPetById(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.getPetByIdParams,
+  options?: { [key: string]: any },
+) {
+  const { petId: param0, ...queryParams } = params;
+  return request<API.Pet>(`/pet/${param0}`, {
+    method: 'GET',
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}
+
+/** Updates a pet in the store with form data POST /pet/${param0} */
+export async function updatePetWithForm(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.updatePetWithFormParams,
+  body: { name?: string; status?: string },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0, ...queryParams } = params;
+  const formData = new FormData();
+
+  Object.keys(body).forEach((ele) => {
+    const item = (body as any)[ele];
+
+    if (item !== undefined && item !== null) {
+      formData.append(
+        ele,
+        typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
+      );
+    }
+  });
+
+  return request<any>(`/pet/${param0}`, {
+    method: 'POST',
+    params: { ...queryParams },
+    data: formData,
+    ...(options || {}),
+  });
+}
+
+/** Deletes a pet DELETE /pet/${param0} */
+export async function deletePet(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.deletePetParams & {
+    // header
+    api_key?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { petId: param0, ...queryParams } = params;
+  return request<any>(`/pet/${param0}`, {
+    method: 'DELETE',
+    headers: {},
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}
+
+/** uploads an image POST /pet/${param0}/uploadImage */
+export async function uploadFile(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.uploadFileParams,
+  body: { additionalMetadata?: string; file?: string },
+  file?: File,
+  options?: { [key: string]: any },
+) {
+  const { petId: param0, ...queryParams } = params;
+  const formData = new FormData();
+
+  if (file) {
+    formData.append('file', file);
+  }
+
+  Object.keys(body).forEach((ele) => {
+    const item = (body as any)[ele];
+
+    if (item !== undefined && item !== null) {
+      formData.append(
+        ele,
+        typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
+      );
+    }
+  });
+
+  return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, {
+    method: 'POST',
+    params: { ...queryParams },
+    data: formData,
+    requestType: 'form',
+    ...(options || {}),
+  });
+}
+
+/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
+export async function findPetsByStatus(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.findPetsByStatusParams,
+  options?: { [key: string]: any },
+) {
+  return request<API.Pet[]>('/pet/findByStatus', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use         tag1, tag2, tag3 for testing. GET /pet/findByTags */
+export async function findPetsByTags(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.findPetsByTagsParams,
+  options?: { [key: string]: any },
+) {
+  return request<API.Pet[]>('/pet/findByTags', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}

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

@@ -0,0 +1,48 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from '@umijs/max';
+
+/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
+export async function getInventory(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/store/inventory', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
+/** Place an order for a pet POST /store/order */
+export async function placeOrder(body: API.Order, options?: { [key: string]: any }) {
+  return request<API.Order>('/store/order', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10.         Other values will generated exceptions GET /store/order/${param0} */
+export async function getOrderById(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.getOrderByIdParams,
+  options?: { [key: string]: any },
+) {
+  const { orderId: param0, ...queryParams } = params;
+  return request<API.Order>(`/store/order/${param0}`, {
+    method: 'GET',
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}
+
+/** Delete purchase order by ID For valid response try integer IDs with positive integer value.         Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
+export async function deleteOrder(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.deleteOrderParams,
+  options?: { [key: string]: any },
+) {
+  const { orderId: param0, ...queryParams } = params;
+  return request<any>(`/store/order/${param0}`, {
+    method: 'DELETE',
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}

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

@@ -0,0 +1,112 @@
+declare namespace API {
+  type ApiResponse = {
+    code?: number;
+    type?: string;
+    message?: string;
+  };
+
+  type Category = {
+    id?: number;
+    name?: string;
+  };
+
+  type deleteOrderParams = {
+    /** ID of the order that needs to be deleted */
+    orderId: number;
+  };
+
+  type deletePetParams = {
+    api_key?: string;
+    /** Pet id to delete */
+    petId: number;
+  };
+
+  type deleteUserParams = {
+    /** The name that needs to be deleted */
+    username: string;
+  };
+
+  type findPetsByStatusParams = {
+    /** Status values that need to be considered for filter */
+    status: ('available' | 'pending' | 'sold')[];
+  };
+
+  type findPetsByTagsParams = {
+    /** Tags to filter by */
+    tags: string[];
+  };
+
+  type getOrderByIdParams = {
+    /** ID of pet that needs to be fetched */
+    orderId: number;
+  };
+
+  type getPetByIdParams = {
+    /** ID of pet to return */
+    petId: number;
+  };
+
+  type getUserByNameParams = {
+    /** The name that needs to be fetched. Use user1 for testing.  */
+    username: string;
+  };
+
+  type loginUserParams = {
+    /** The user name for login */
+    username: string;
+    /** The password for login in clear text */
+    password: string;
+  };
+
+  type Order = {
+    id?: number;
+    petId?: number;
+    quantity?: number;
+    shipDate?: string;
+    /** Order Status */
+    status?: 'placed' | 'approved' | 'delivered';
+    complete?: boolean;
+  };
+
+  type Pet = {
+    id?: number;
+    category?: Category;
+    name: string;
+    photoUrls: string[];
+    tags?: Tag[];
+    /** pet status in the store */
+    status?: 'available' | 'pending' | 'sold';
+  };
+
+  type Tag = {
+    id?: number;
+    name?: string;
+  };
+
+  type updatePetWithFormParams = {
+    /** ID of pet that needs to be updated */
+    petId: number;
+  };
+
+  type updateUserParams = {
+    /** name that need to be updated */
+    username: string;
+  };
+
+  type uploadFileParams = {
+    /** ID of pet to update */
+    petId: number;
+  };
+
+  type User = {
+    id?: number;
+    username?: string;
+    firstName?: string;
+    lastName?: string;
+    email?: string;
+    password?: string;
+    phone?: string;
+    /** User Status */
+    userStatus?: number;
+  };
+}

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

@@ -0,0 +1,100 @@
+// @ts-ignore
+/* eslint-disable */
+import { request } from '@umijs/max';
+
+/** Create user This can only be done by the logged in user. POST /user */
+export async function createUser(body: API.User, options?: { [key: string]: any }) {
+  return request<any>('/user', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Get user by user name GET /user/${param0} */
+export async function getUserByName(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.getUserByNameParams,
+  options?: { [key: string]: any },
+) {
+  const { username: param0, ...queryParams } = params;
+  return request<API.User>(`/user/${param0}`, {
+    method: 'GET',
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}
+
+/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
+export async function updateUser(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.updateUserParams,
+  body: API.User,
+  options?: { [key: string]: any },
+) {
+  const { username: param0, ...queryParams } = params;
+  return request<any>(`/user/${param0}`, {
+    method: 'PUT',
+    params: { ...queryParams },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
+export async function deleteUser(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.deleteUserParams,
+  options?: { [key: string]: any },
+) {
+  const { username: param0, ...queryParams } = params;
+  return request<any>(`/user/${param0}`, {
+    method: 'DELETE',
+    params: { ...queryParams },
+    ...(options || {}),
+  });
+}
+
+/** Creates list of users with given input array POST /user/createWithArray */
+export async function createUsersWithArrayInput(
+  body: API.User[],
+  options?: { [key: string]: any },
+) {
+  return request<any>('/user/createWithArray', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Creates list of users with given input array POST /user/createWithList */
+export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) {
+  return request<any>('/user/createWithList', {
+    method: 'POST',
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** Logs user into the system GET /user/login */
+export async function loginUser(
+  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
+  params: API.loginUserParams,
+  options?: { [key: string]: any },
+) {
+  return request<string>('/user/login', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** Logs out current logged in user session GET /user/logout */
+export async function logoutUser(options?: { [key: string]: any }) {
+  return request<any>('/user/logout', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}

+ 3 - 0
src/tailwind.css

@@ -0,0 +1,3 @@
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';

+ 20 - 0
src/typings.d.ts

@@ -0,0 +1,20 @@
+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 'mockjs';
+declare module 'react-fittext';
+declare module 'bizcharts-plugin-slider';
+
+declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

+ 11 - 0
tailwind.config.js

@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ['./src/**/*.{html,js,tsx}'],
+  corePlugins: {
+    preflight: false,
+  },
+  theme: {
+    extend: {},
+  },
+  plugins: [],
+};

+ 64 - 0
tests/setupTests.jsx

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

+ 23 - 0
tsconfig.json

@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "importHelpers": true,
+    "jsx": "preserve",
+    "esModuleInterop": true,
+    "sourceMap": true,
+    "baseUrl": "./",
+    "skipLibCheck": true,
+    "experimentalDecorators": true,
+    "strict": true,
+    "resolveJsonModule": true,
+    "allowSyntheticDefaultImports": true,
+    "paths": {
+      "@/*": ["./src/*"],
+      "@@/*": ["./src/.umi/*"],
+      "@@test/*": ["./src/.umi-test/*"]
+    }
+  },
+  "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"]
+}

+ 1 - 0
types/cache/cache.json

@@ -0,0 +1 @@
+{}

Plik diff jest za duży
+ 386 - 0
types/cache/login.cache.json


+ 324 - 0
types/cache/mock/login.mock.cache.js

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

+ 0 - 0
types/cache/mock/mock.cache.js


Plik diff jest za duży
+ 120 - 0
types/index.d.ts