From 8b8509b8d9a77fff9b1538ff01c0e6619e77ba2d Mon Sep 17 00:00:00 2001 From: Geminy <17730017620@163.com> Date: Fri, 23 Jul 2021 15:37:35 +0800 Subject: [PATCH 1/2] feat: init frontend app --- code/app/.editorconfig | 16 + code/app/.eslintignore | 8 + code/app/.eslintrc.js | 8 + code/app/.gitignore | 40 ++ code/app/.prettierignore | 23 + code/app/.prettierrc.js | 5 + code/app/.stylelintrc.js | 5 + code/app/README.md | 57 ++ code/app/config/config.dev.ts | 15 + code/app/config/config.ts | 71 +++ code/app/config/defaultSettings.ts | 21 + code/app/config/oneapi.json | 593 ++++++++++++++++++ code/app/config/proxy.ts | 31 + code/app/config/routes.ts | 52 ++ code/app/jest.config.js | 10 + code/app/jsconfig.json | 10 + code/app/mock/listTableList.ts | 175 ++++++ code/app/mock/notices.ts | 107 ++++ code/app/mock/route.ts | 5 + code/app/mock/user.ts | 203 ++++++ code/app/package.json | 110 ++++ code/app/public/CNAME | 1 + code/app/public/favicon.ico | Bin 0 -> 23045 bytes code/app/public/icons/icon-128x128.png | Bin 0 -> 1329 bytes code/app/public/icons/icon-192x192.png | Bin 0 -> 1856 bytes code/app/public/icons/icon-512x512.png | Bin 0 -> 5082 bytes code/app/public/logo.png | Bin 0 -> 5541 bytes code/app/public/logo.svg | 1 + code/app/public/pro_icon.svg | 5 + code/app/src/access.ts | 9 + code/app/src/app.tsx | 133 ++++ code/app/src/components/Footer/index.tsx | 24 + .../src/components/HeaderDropdown/index.less | 16 + .../src/components/HeaderDropdown/index.tsx | 17 + .../src/components/HeaderSearch/index.less | 25 + .../app/src/components/HeaderSearch/index.tsx | 101 +++ .../src/components/NoticeIcon/NoticeIcon.tsx | 126 ++++ .../src/components/NoticeIcon/NoticeList.less | 103 +++ .../src/components/NoticeIcon/NoticeList.tsx | 113 ++++ code/app/src/components/NoticeIcon/index.less | 35 ++ code/app/src/components/NoticeIcon/index.tsx | 153 +++++ .../RightContent/AvatarDropdown.tsx | 103 +++ .../src/components/RightContent/index.less | 84 +++ .../app/src/components/RightContent/index.tsx | 62 ++ code/app/src/components/index.md | 272 ++++++++ code/app/src/e2e/baseLayout.e2e.js | 61 ++ code/app/src/global.less | 57 ++ code/app/src/global.tsx | 83 +++ code/app/src/locales/en-US.ts | 25 + code/app/src/locales/en-US/component.ts | 5 + code/app/src/locales/en-US/globalHeader.ts | 17 + code/app/src/locales/en-US/menu.ts | 52 ++ code/app/src/locales/en-US/pages.ts | 70 +++ code/app/src/locales/en-US/pwa.ts | 6 + code/app/src/locales/en-US/settingDrawer.ts | 31 + code/app/src/locales/en-US/settings.ts | 60 ++ code/app/src/locales/zh-CN.ts | 25 + code/app/src/locales/zh-CN/component.ts | 5 + code/app/src/locales/zh-CN/globalHeader.ts | 17 + code/app/src/locales/zh-CN/menu.ts | 52 ++ code/app/src/locales/zh-CN/pages.ts | 67 ++ code/app/src/locales/zh-CN/pwa.ts | 6 + code/app/src/locales/zh-CN/settingDrawer.ts | 31 + code/app/src/locales/zh-CN/settings.ts | 55 ++ code/app/src/manifest.json | 22 + code/app/src/pages/404.tsx | 18 + code/app/src/pages/Admin.tsx | 43 ++ .../pages/TableList/components/UpdateForm.tsx | 209 ++++++ code/app/src/pages/TableList/index.tsx | 391 ++++++++++++ code/app/src/pages/Welcome.less | 8 + code/app/src/pages/Welcome.tsx | 63 ++ code/app/src/pages/document.ejs | 198 ++++++ code/app/src/pages/user/Login/index.less | 114 ++++ code/app/src/pages/user/Login/index.tsx | 292 +++++++++ code/app/src/service-worker.js | 66 ++ code/app/src/services/api/index.ts | 7 + code/app/src/services/api/login.ts | 41 ++ code/app/src/services/api/notice.ts | 11 + code/app/src/services/api/rule.ts | 47 ++ code/app/src/services/api/typings.d.ts | 103 +++ code/app/src/services/api/user.ts | 13 + code/app/src/services/swagger/index.ts | 12 + code/app/src/services/swagger/pet.ts | 166 +++++ code/app/src/services/swagger/store.ts | 54 ++ code/app/src/services/swagger/typings.d.ts | 52 ++ code/app/src/services/swagger/user.ts | 114 ++++ code/app/src/typings.d.ts | 24 + code/app/tests/PuppeteerEnvironment.js | 41 ++ code/app/tests/beforeTest.js | 39 ++ code/app/tests/getBrowser.js | 45 ++ code/app/tests/run-tests.js | 50 ++ code/app/tests/setupTests.js | 10 + code/app/tsconfig.json | 57 ++ 93 files changed, 5983 insertions(+) create mode 100644 code/app/.editorconfig create mode 100644 code/app/.eslintignore create mode 100644 code/app/.eslintrc.js create mode 100644 code/app/.gitignore create mode 100644 code/app/.prettierignore create mode 100644 code/app/.prettierrc.js create mode 100644 code/app/.stylelintrc.js create mode 100644 code/app/README.md create mode 100644 code/app/config/config.dev.ts create mode 100644 code/app/config/config.ts create mode 100644 code/app/config/defaultSettings.ts create mode 100644 code/app/config/oneapi.json create mode 100644 code/app/config/proxy.ts create mode 100644 code/app/config/routes.ts create mode 100644 code/app/jest.config.js create mode 100644 code/app/jsconfig.json create mode 100644 code/app/mock/listTableList.ts create mode 100644 code/app/mock/notices.ts create mode 100644 code/app/mock/route.ts create mode 100644 code/app/mock/user.ts create mode 100644 code/app/package.json create mode 100644 code/app/public/CNAME create mode 100644 code/app/public/favicon.ico create mode 100644 code/app/public/icons/icon-128x128.png create mode 100644 code/app/public/icons/icon-192x192.png create mode 100644 code/app/public/icons/icon-512x512.png create mode 100644 code/app/public/logo.png create mode 100644 code/app/public/logo.svg create mode 100644 code/app/public/pro_icon.svg create mode 100644 code/app/src/access.ts create mode 100644 code/app/src/app.tsx create mode 100644 code/app/src/components/Footer/index.tsx create mode 100644 code/app/src/components/HeaderDropdown/index.less create mode 100644 code/app/src/components/HeaderDropdown/index.tsx create mode 100644 code/app/src/components/HeaderSearch/index.less create mode 100644 code/app/src/components/HeaderSearch/index.tsx create mode 100644 code/app/src/components/NoticeIcon/NoticeIcon.tsx create mode 100644 code/app/src/components/NoticeIcon/NoticeList.less create mode 100644 code/app/src/components/NoticeIcon/NoticeList.tsx create mode 100644 code/app/src/components/NoticeIcon/index.less create mode 100644 code/app/src/components/NoticeIcon/index.tsx create mode 100644 code/app/src/components/RightContent/AvatarDropdown.tsx create mode 100644 code/app/src/components/RightContent/index.less create mode 100644 code/app/src/components/RightContent/index.tsx create mode 100644 code/app/src/components/index.md create mode 100644 code/app/src/e2e/baseLayout.e2e.js create mode 100644 code/app/src/global.less create mode 100644 code/app/src/global.tsx create mode 100644 code/app/src/locales/en-US.ts create mode 100644 code/app/src/locales/en-US/component.ts create mode 100644 code/app/src/locales/en-US/globalHeader.ts create mode 100644 code/app/src/locales/en-US/menu.ts create mode 100644 code/app/src/locales/en-US/pages.ts create mode 100644 code/app/src/locales/en-US/pwa.ts create mode 100644 code/app/src/locales/en-US/settingDrawer.ts create mode 100644 code/app/src/locales/en-US/settings.ts create mode 100644 code/app/src/locales/zh-CN.ts create mode 100644 code/app/src/locales/zh-CN/component.ts create mode 100644 code/app/src/locales/zh-CN/globalHeader.ts create mode 100644 code/app/src/locales/zh-CN/menu.ts create mode 100644 code/app/src/locales/zh-CN/pages.ts create mode 100644 code/app/src/locales/zh-CN/pwa.ts create mode 100644 code/app/src/locales/zh-CN/settingDrawer.ts create mode 100644 code/app/src/locales/zh-CN/settings.ts create mode 100644 code/app/src/manifest.json create mode 100644 code/app/src/pages/404.tsx create mode 100644 code/app/src/pages/Admin.tsx create mode 100644 code/app/src/pages/TableList/components/UpdateForm.tsx create mode 100644 code/app/src/pages/TableList/index.tsx create mode 100644 code/app/src/pages/Welcome.less create mode 100644 code/app/src/pages/Welcome.tsx create mode 100644 code/app/src/pages/document.ejs create mode 100644 code/app/src/pages/user/Login/index.less create mode 100644 code/app/src/pages/user/Login/index.tsx create mode 100644 code/app/src/service-worker.js create mode 100644 code/app/src/services/api/index.ts create mode 100644 code/app/src/services/api/login.ts create mode 100644 code/app/src/services/api/notice.ts create mode 100644 code/app/src/services/api/rule.ts create mode 100644 code/app/src/services/api/typings.d.ts create mode 100644 code/app/src/services/api/user.ts create mode 100644 code/app/src/services/swagger/index.ts create mode 100644 code/app/src/services/swagger/pet.ts create mode 100644 code/app/src/services/swagger/store.ts create mode 100644 code/app/src/services/swagger/typings.d.ts create mode 100644 code/app/src/services/swagger/user.ts create mode 100644 code/app/src/typings.d.ts create mode 100644 code/app/tests/PuppeteerEnvironment.js create mode 100644 code/app/tests/beforeTest.js create mode 100644 code/app/tests/getBrowser.js create mode 100644 code/app/tests/run-tests.js create mode 100644 code/app/tests/setupTests.js create mode 100644 code/app/tsconfig.json diff --git a/code/app/.editorconfig b/code/app/.editorconfig new file mode 100644 index 0000000..7e3649a --- /dev/null +++ b/code/app/.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 diff --git a/code/app/.eslintignore b/code/app/.eslintignore new file mode 100644 index 0000000..8336e93 --- /dev/null +++ b/code/app/.eslintignore @@ -0,0 +1,8 @@ +/lambda/ +/scripts +/config +.history +public +dist +.umi +mock \ No newline at end of file diff --git a/code/app/.eslintrc.js b/code/app/.eslintrc.js new file mode 100644 index 0000000..b882c20 --- /dev/null +++ b/code/app/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + extends: [require.resolve('@umijs/fabric/dist/eslint')], + globals: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, + page: true, + REACT_APP_ENV: true, + }, +}; diff --git a/code/app/.gitignore b/code/app/.gitignore new file mode 100644 index 0000000..7fd9f58 --- /dev/null +++ b/code/app/.gitignore @@ -0,0 +1,40 @@ +# 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 +/.vscode + +# 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 + +# screenshot +screenshot +.firebase +.eslintcache + +build diff --git a/code/app/.prettierignore b/code/app/.prettierignore new file mode 100644 index 0000000..d17efb4 --- /dev/null +++ b/code/app/.prettierignore @@ -0,0 +1,23 @@ +**/*.svg +package.json +.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 \ No newline at end of file diff --git a/code/app/.prettierrc.js b/code/app/.prettierrc.js new file mode 100644 index 0000000..7b597d7 --- /dev/null +++ b/code/app/.prettierrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.prettier, +}; diff --git a/code/app/.stylelintrc.js b/code/app/.stylelintrc.js new file mode 100644 index 0000000..c203078 --- /dev/null +++ b/code/app/.stylelintrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.stylelint, +}; diff --git a/code/app/README.md b/code/app/README.md new file mode 100644 index 0000000..4c89a72 --- /dev/null +++ b/code/app/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). diff --git a/code/app/config/config.dev.ts b/code/app/config/config.dev.ts new file mode 100644 index 0000000..ab0e590 --- /dev/null +++ b/code/app/config/config.dev.ts @@ -0,0 +1,15 @@ +// https://umijs.org/config/ +import { defineConfig } from 'umi'; + +export default defineConfig({ + plugins: [ + // https://github.com/zthxxx/react-dev-inspector + 'react-dev-inspector/plugins/umi/react-inspector', + ], + // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props + inspectorConfig: { + exclude: [], + babelPlugins: [], + babelOptions: {}, + }, +}); diff --git a/code/app/config/config.ts b/code/app/config/config.ts new file mode 100644 index 0000000..381be6d --- /dev/null +++ b/code/app/config/config.ts @@ -0,0 +1,71 @@ +// https://umijs.org/config/ +import { defineConfig } from 'umi'; +import { join } from 'path'; + +import defaultSettings from './defaultSettings'; +import proxy from './proxy'; +import routes from './routes'; + +const { REACT_APP_ENV } = process.env; + +export default defineConfig({ + hash: true, + antd: {}, + dva: { + hmr: true, + }, + layout: { + // https://umijs.org/zh-CN/plugins/plugin-layout + locale: true, + siderWidth: 208, + ...defaultSettings, + }, + // 国际化 https://umijs.org/zh-CN/plugins/plugin-locale + locale: { + default: 'en-US', + antd: true, + // default true, when it is true, will use `navigator.language` overwrite default + baseNavigator: true, + }, + dynamicImport: { + loading: '@ant-design/pro-layout/es/PageLoading', + }, + targets: { + ie: 11, + }, + // umi routes: https://umijs.org/docs/routing + routes, + // Theme for antd: https://ant.design/docs/react/customize-theme-cn + theme: { + 'primary-color': defaultSettings.primaryColor, + }, + // esbuild is father build tools + // https://umijs.org/plugins/plugin-esbuild + esbuild: {}, + title: false, + ignoreMomentLocale: true, + proxy: proxy[REACT_APP_ENV || 'dev'], + manifest: { + basePath: '/', + }, + // Fast Refresh 热更新 + fastRefresh: {}, + openAPI: [ + { + requestLibPath: "import { request } from 'umi'", + // 或者使用在线的版本 + // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json" + schemaPath: join(__dirname, 'oneapi.json'), + mock: false, + }, + { + requestLibPath: "import { request } from 'umi'", + schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json', + projectName: 'swagger', + }, + ], + nodeModulesTransform: { type: 'none' }, + mfsu: {}, + webpack5: {}, + exportStatic: {}, +}); diff --git a/code/app/config/defaultSettings.ts b/code/app/config/defaultSettings.ts new file mode 100644 index 0000000..69682b5 --- /dev/null +++ b/code/app/config/defaultSettings.ts @@ -0,0 +1,21 @@ +import { Settings as LayoutSettings } from '@ant-design/pro-layout'; + +const Settings: LayoutSettings & { + pwa?: boolean; + logo?: string; +} = { + navTheme: 'light', + // TeamLinker 主题 + primaryColor: '#0052CC', + layout: 'mix', + contentWidth: 'Fluid', + fixedHeader: false, + fixSiderbar: true, + colorWeak: false, + title: 'Teamlinker', + pwa: false, + logo: '/logo.png', + iconfontUrl: '', +}; + +export default Settings; diff --git a/code/app/config/oneapi.json b/code/app/config/oneapi.json new file mode 100644 index 0000000..996344b --- /dev/null +++ b/code/app/config/oneapi.json @@ -0,0 +1,593 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Teamlinker", + "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" + } + } + } + } + } +} diff --git a/code/app/config/proxy.ts b/code/app/config/proxy.ts new file mode 100644 index 0000000..d6c81ea --- /dev/null +++ b/code/app/config/proxy.ts @@ -0,0 +1,31 @@ +/** + * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 + * ------------------------------- + * The agent cannot take effect in the production environment + * so there is no configuration of the production environment + * For details, please see + * https://pro.ant.design/docs/deploy + */ +export default { + dev: { + '/api/': { + target: 'https://preview.pro.ant.design', + changeOrigin: true, + pathRewrite: { '^': '' }, + }, + }, + test: { + '/api/': { + target: 'https://preview.pro.ant.design', + changeOrigin: true, + pathRewrite: { '^': '' }, + }, + }, + pre: { + '/api/': { + target: 'your pre url', + changeOrigin: true, + pathRewrite: { '^': '' }, + }, + }, +}; diff --git a/code/app/config/routes.ts b/code/app/config/routes.ts new file mode 100644 index 0000000..a269819 --- /dev/null +++ b/code/app/config/routes.ts @@ -0,0 +1,52 @@ +export default [ + { + path: '/user', + layout: false, + routes: [ + { + path: '/user', + routes: [ + { + name: 'login', + path: '/user/login', + component: './user/Login', + }, + ], + }, + ], + }, + { + path: '/welcome', + name: 'welcome', + icon: 'smile', + component: './Welcome', + }, + { + path: '/admin', + name: 'admin', + icon: 'crown', + access: 'canAdmin', + component: './Admin', + routes: [ + { + path: '/admin/sub-page', + name: 'sub-page', + icon: 'smile', + component: './Welcome', + }, + ], + }, + { + name: 'list.table-list', + icon: 'table', + path: '/list', + component: './TableList', + }, + { + path: '/', + redirect: '/welcome', + }, + { + component: './404', + }, +]; diff --git a/code/app/jest.config.js b/code/app/jest.config.js new file mode 100644 index 0000000..09a2211 --- /dev/null +++ b/code/app/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + testURL: 'http://localhost:8000', + testEnvironment: './tests/PuppeteerEnvironment', + verbose: false, + extraSetupFiles: ['./tests/setupTests.js'], + globals: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, + localStorage: null, + }, +}; diff --git a/code/app/jsconfig.json b/code/app/jsconfig.json new file mode 100644 index 0000000..f87334d --- /dev/null +++ b/code/app/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/code/app/mock/listTableList.ts b/code/app/mock/listTableList.ts new file mode 100644 index 0000000..d58a1bd --- /dev/null +++ b/code/app/mock/listTableList.ts @@ -0,0 +1,175 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +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).forEach((key) => { + if (sorter[key] === 'descend') { + if (prev[key] - next[key] > 0) { + sortNumber += -1; + } else { + sortNumber += 1; + } + return; + } + if (prev[key] - next[key] > 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).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, +}; diff --git a/code/app/mock/notices.ts b/code/app/mock/notices.ts new file mode 100644 index 0000000..732dd58 --- /dev/null +++ b/code/app/mock/notices.ts @@ -0,0 +1,107 @@ +import { Request, Response } from 'express'; + +const getNotices = (req: Request, res: Response) => { + res.json({ + data: [ + { + id: '000000001', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '你收到了 14 份新周报', + datetime: '2017-08-09', + type: 'notification', + }, + { + id: '000000002', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', + title: '你推荐的 曲妮妮 已通过第三轮面试', + datetime: '2017-08-08', + type: 'notification', + }, + { + id: '000000003', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', + title: '这种模板可以区分多种通知类型', + datetime: '2017-08-07', + read: true, + type: 'notification', + }, + { + id: '000000004', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', + title: '左侧图标用于区分不同的类型', + datetime: '2017-08-07', + type: 'notification', + }, + { + id: '000000005', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '内容不要超过两行字,超出时自动截断', + datetime: '2017-08-07', + type: 'notification', + }, + { + id: '000000006', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '曲丽丽 评论了你', + description: '描述信息描述信息描述信息', + datetime: '2017-08-07', + type: 'message', + clickClose: true, + }, + { + id: '000000007', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '朱偏右 回复了你', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: 'message', + clickClose: true, + }, + { + id: '000000008', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '标题', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: 'message', + clickClose: true, + }, + { + id: '000000009', + title: '任务名称', + description: '任务需要在 2017-01-12 20:00 前启动', + extra: '未开始', + status: 'todo', + type: 'event', + }, + { + id: '000000010', + title: '第三方紧急代码变更', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '马上到期', + status: 'urgent', + type: 'event', + }, + { + id: '000000011', + title: '信息安全考试', + description: '指派竹尔于 2017-01-09 前完成更新并发布', + extra: '已耗时 8 天', + status: 'doing', + type: 'event', + }, + { + id: '000000012', + title: 'ABCD 版本发布', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '进行中', + status: 'processing', + type: 'event', + }, + ], + }); +}; + +export default { + 'GET /api/notices': getNotices, +}; diff --git a/code/app/mock/route.ts b/code/app/mock/route.ts new file mode 100644 index 0000000..418d10f --- /dev/null +++ b/code/app/mock/route.ts @@ -0,0 +1,5 @@ +export default { + '/api/auth_routes': { + '/form/advanced-form': { authority: ['admin', 'user'] }, + }, +}; diff --git a/code/app/mock/user.ts b/code/app/mock/user.ts new file mode 100644 index 0000000..4d3ccdf --- /dev/null +++ b/code/app/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: 'Geminy', + avatar: 'https://avatars.githubusercontent.com/u/14195576?s=60&v=4', + 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 === '123' && username === 'admin') { + res.send({ + status: 'ok', + type, + currentAuthority: 'admin', + }); + access = 'admin'; + return; + } + if (password === '123' && 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: 'Unauthorized', + message: 'Unauthorized', + 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, +}; diff --git a/code/app/package.json b/code/app/package.json new file mode 100644 index 0000000..0e9bbea --- /dev/null +++ b/code/app/package.json @@ -0,0 +1,110 @@ +{ + "name": "ant-design-pro", + "version": "5.0.0", + "private": true, + "description": "An out-of-box UI solution for enterprise applications", + "scripts": { + "analyze": "cross-env ANALYZE=1 umi build", + "build": "umi 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": "umi g tmp", + "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier", + "lint-staged": "lint-staged", + "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style", + "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", + "lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto", + "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", + "openapi": "umi openapi", + "precommit": "lint-staged", + "prettier": "prettier -c --write \"src/**/*\"", + "start": "cross-env UMI_ENV=dev umi dev", + "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev", + "start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev", + "start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev", + "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev", + "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev", + "pretest": "node ./tests/beforeTest", + "test": "umi test", + "test:all": "node ./tests/run-tests.js", + "test:component": "umi test ./src/components", + "serve": "umi-serve", + "tsc": "tsc --noEmit", + "pkgup:taobao": "npm install --registry=https://registry.npm.taobao.org" + }, + "lint-staged": { + "**/*.less": "stylelint --syntax less", + "**/*.{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.5.0", + "@ant-design/pro-descriptions": "^1.6.8", + "@ant-design/pro-form": "^1.18.3", + "@ant-design/pro-layout": "^6.15.3", + "@ant-design/pro-table": "^2.30.8", + "@umijs/route-utils": "^1.0.36", + "antd": "^4.14.0", + "classnames": "^2.2.6", + "lodash": "^4.17.11", + "moment": "^2.25.3", + "omit.js": "^2.0.2", + "react": "^17.0.0", + "react-dev-inspector": "^1.1.1", + "react-dom": "^17.0.0", + "react-helmet-async": "^1.0.4", + "umi": "^3.5.0", + "umi-serve": "^1.9.10" + }, + "devDependencies": { + "@ant-design/pro-cli": "^2.0.2", + "@types/express": "^4.17.0", + "@types/history": "^4.7.2", + "@types/jest": "^26.0.0", + "@types/lodash": "^4.14.144", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-helmet": "^6.1.0", + "@umijs/fabric": "^2.6.2", + "@umijs/openapi": "^1.1.14", + "@umijs/plugin-blocks": "^2.0.5", + "@umijs/plugin-esbuild": "^1.0.1", + "@umijs/plugin-openapi": "^1.2.0", + "@umijs/preset-ant-design-pro": "^1.2.0", + "@umijs/preset-dumi": "^1.1.7", + "@umijs/preset-react": "^1.8.17", + "@umijs/preset-ui": "^2.2.9", + "@umijs/yorkie": "^2.0.3", + "carlo": "^0.9.46", + "cross-env": "^7.0.0", + "cross-port-killer": "^1.1.1", + "detect-installer": "^1.0.1", + "enzyme": "^3.11.0", + "eslint": "^7.1.0", + "express": "^4.17.1", + "gh-pages": "^3.0.0", + "jsdom-global": "^3.0.2", + "lint-staged": "^10.0.0", + "mockjs": "^1.0.1-beta3", + "prettier": "^2.3.2", + "puppeteer-core": "^8.0.0", + "stylelint": "^13.0.0", + "typescript": "^4.2.2" + }, + "engines": { + "node": ">=10.0.0" + }, + "gitHooks": { + "commit-msg": "fabric verify-commit" + } +} diff --git a/code/app/public/CNAME b/code/app/public/CNAME new file mode 100644 index 0000000..30c2d4d --- /dev/null +++ b/code/app/public/CNAME @@ -0,0 +1 @@ +preview.pro.ant.design \ No newline at end of file diff --git a/code/app/public/favicon.ico b/code/app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..74edb26cfa5015b393b237a4ed71ff4ab3582d70 GIT binary patch literal 23045 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≈nVcF#IP=Z0=+mUiOC1>|GdeU(|^RIsG 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

*C7T$?5!b= z&nPVm(<;~Sw_*kJlio|*qhI8#F?9y#@jvQ4Y?j2~r_a^)YoQ-co4WFOwF)i5mFnrV zp3rz^`K|nrI`guQ$X^%JuCI5qSS(}Tc|%+~o}P6dC={o1Q&<)3lnFnXE=l2Po$K4W zYZ0|2<3Cp|&)NE>;p=Muwg#BLXuboTJ(mwtjLbtdOE_A4n^}l29p9@>4vpS+t*^qG zb2Uo-W$@vKeR}iztf6tJTUh8=^kh_EBe^3f?1b(p_oBq&dpqW$pAyBB**bIt4BhwK zX02JE@mc}FExq0q+5_8B#EP&@;6-lx4t~)@$HBEBXnIqX0E$mhb$(} zt|vYUzekLn0SQ;Cd)J1 zXF3gv$u52+$>oQVvh7DxPpKeWD4yJgdWQQ`D#($Z=j%Df!iAxvR)-y7Ie5nI39+zG z4a3_4mE;SBh@MK8p%(n|yGj>Ao>j51ETLI#vuUt#g|%FDZG}&kbEb(|*9!RyL&U?| zvNnv+oEL_*R{Mw_;x+O`vNWUXL6>@M)sQr_lmlng7?sCaDH0YaGf%+R1ZM}9#$j$3 zi}N01^7uoRgvG74Fxi{415}={tg=gDHuqwR$1?9qlK<#~ zLG@an>_SlEY6+k8PD+S)C&NU7!~`$+Sin{JS2aLn_Jm#q|8H%OnLqt&n-f~C7KW9K z>QxTE1D!cV+(E>-8N#r>Aw98T*8=odDM_y2&?=ag7-=Rj&p%xvU zq=o{%T2@T(b@VqinV#`~IN4t3^z$HE=3~ZiDQ=xP6Gt)g;q1Wv2N&CBx=Ndw5 z6U*fS@;@VBF^MGsyag~+Eq&2UZ@5SG$!SM2ckBL!E1lBg>*t^{Lqn=cK^-Rhc#S(% zuAqbI(^+PqN#)t&5xr~v&*Y$qZ$3Yz)nRe`ZZcJUU8hd=)9$7 z6n-k7p||Esq}igp(IoxtD&_5A!OwcTvM3p-HHJGcVh|>N13O;!W%RXd{Y6@BEp1Jz zHCH!~H~lR%<3E0b>}>_dD6Ef8r_)LSp}O5L`DerkKk|Jc@n6*BmU&r@TKhG$-x=>`wv*Vj%M=6<7!P7~3+)?^D>AA~&^F+{mzBAMLh zuTiItS}IqcPpj-t;W15ahnBV9rHnrShN-@GCY+EyXV6Gass&4cM~^;F?z{3+SXKe7 zX8rBU7Fc!Kr5M!K@lDd)Bl)VMR)yb6;AHxpH?Q1Bx14XQy~`&ggyC$4u>az;q|C7G ziTI-A3y9BZbxP7rW4_G!ju{^(NOZstl}tvVR+j8_B`~)2cWn2~;amI9u@_9oSC+Xc z_+DcYbc7S~MIl7Fjk6qEWXDo6>n5U0O4P_k15Z^c%P@0?*Oaj8%BIU*uy|Vgi@eqH zPKZqrW$wXWQ9=QoQrY4VBCFzACRJhEv+U(FOmx1_uwVirtukLVbyF&%?xKU6D>Yot zj6ZW-1$@|ASwJlAu1;!K9!5uPdov-?7RK^;L%bs%6z9abcvP!i20^{1|g}`{&O=}R+#D;J+z#Fj(x6k%xY0!5_DrzSsTCn;pu^6 zIZV~_c4b?yxFutJZTc`BT4+(N21dx%Bxx5_=*+^Gl`PCmvS7QnNZUr>RqS>smtAo7 z%1fV4h6YI+Y2X$?ZuQmeFj!XWfPJxAc1<0;;3C|?V?=BITB{AnSkYW;+Vbas9ODwo zC?uf|l|KjLdt@iP*&!CGbh&OT1l3&s3RD|7knGOc{T%o)WLqmqSnTE5-M^OKN+8ct1C(XwQJJg%OozV)cJ^w# zVxbg~z7OMwn+eEPaBRR0KIDX14Kb^G-Strc8r6QmoINnF;F1;|U5n3%rdT9(U0dcP zI*}ZU>2wFp1u1KmK2cwSwk|=oZ8?l*@yIPBD3BPN(BINzFh#VAKBpVxZ71*JgwpWXLZfS%KkGa_tQn0bX5)h8 z6Q7LGdb=)NQ}a}>SD{8~!ga|bE*aS_Nxg_+ojhQ1{b@sVzaX)Z{rV-1%Qa*ioSy65 zb4ke$#&Mf>xuBg{&aCGNbhbOGbN4XT4?qQpRW^!2z;9eCH6fH7)+qpWe;n~*tCIq= z&w&jc@_tBWRiS3IyovPwt~fl#1Z-%o%T_B%)sO*FrOIkz-1r$#mr+&N2J06a?u1<~QB}EoR?wnU)c32Ew0(EP^w=K@< zM;7MeoWTwa-$09MwD#{-p5K@kGTM6q@*9`~NBz#snAQt$W%fZr`qk|A%IEM4|B(?x zOpx+g1-*Q?a%JHpKf|Cs>MCBj^^EmKDb?*U1(_uF({whek67oi^wL)^XH3=`&u$U^ z*K&AEIc$ISs)O2r7emK9XW_i^6Jxw-Ug6tIn{3gqAYG+j_U1<)>HovW7l7Z>STIh4 z7OH$pfQM^<6ZPN`%FY^PFKzq89tYsIi0BKNBRW9y7-Fqz~yA;*b2Ok9qG$!ts}#>6CM z%!oS18AH=_ON=@$A&CZ&UJ!(66hV}Tf?`8A^!BQ5zW>(i_qtx)dhd19pohbMPQB&U zt-o&F`|tmMOQEXqML>@Ql)`|3juY2MQ2I-#oxT=_p>I&VGzNX#>G1SjoXbg^%Ux_; zjM9P{JThf(sQ+aRv*D@5{Ph_gA4A~@IGewPn#)}rEST4DA+-7@GX{+oj-ejQE5C(9 z2uRd8m5U+Y8Uq4`;l}c-9EYJFFkvj2G+z}_xlm=%GpRn2F@^X}!Z z$;p<{T*XmvCkT@q973&XnhULe$X`dB{|aMBO7k(VF^1a8f50IE4v+5SZ#6)N4_*rI zVaOLQZX87Ia0P!I$)hmY>iCEH8s@E?7`(v!Jkm@eAoDMbbsef#mEb_br7sR4Q(TB1 zCP@!N81@QOZy1b21ROrlfgosVC+Fa){zCMG+Ww;U^NomKjHTP z`J7oGQRpG=Mc*gx9{4dFB#0-Efd{1uaTxjznyg}tKQc(7GtSgZ?!xSqPocc58D95j zYLb0u8~!$)n%IfMFrX0U=*_O303}z{W2CsQIqI4IO;9y zb+cE-QC>QVCdNp(v0}s|N|tDcc>pGP3d*LLuqvrHo*8WwE;+P-F{zay5arfJJ^t3$~ zDEQ6UqJjRDJoXP{9$#5g#x*&@kGLtWOYt_;Oxk-;G*muNwfblpD@So%%iy8_F|M(z zIG^4Q>f-HqqH-Vl3qqSCO1QQK61i(ZOY7F;{3a#T1#cDwE;welui`(%Rs6hz$8i}b zw^P7Ob-S%e1RkV#Ul4 zvf};H%3|ypHUrw7N!&s>|Baf8O{ji_q1(h|>~XdU!vbPnw(=gY(fnD8<GcD_ZW-@~B<9bdX4=kF%f*eO5a(Jc(t&b{%{TPJTw=ZDc zn)%pQ)`XJXBj|hIPIS^g(+4Dzd1ql=v*bCJV}D@I+VLFIw>iRVdLEG;_M4c~a3kuc zKNaZf;otZ;IsO@QUS2{XT);*Vig6#0>Xlhj7xI}F;H!<#;Qq?~MM*`d zxwwUEd;{loXdrDT_tSa{8E6}S1sO%O+=ixexdwOel9|bzUk0QW`Vv0peCC|)AyY1) zcKI;S?XsqG;Od|8*^f;eSkBFY`~*s>S`@d?xh7F+^%Wp0P@*Psu@+)pb0zi%V(woP z*EM0>>c`>dvplcDP_E^*h{Yb^`9+BISJila+>f2q5PgBp&h$OqqJ^B78_YdJz(}3K zQpC2@1)9aoUEf?y;;-lVzeIli^pTwVJDG*L(^NSGZli?`!}Oxmjr@2Z%_J$1`<+Q) zv^Y1Tc*w}G>p|9=MZumSx3K?hFs4VJ;8aFK@5J1u&xX#lbqT@#EBX%?VtwP+11(~o zwk9a;nb-q@rYO8SDKMVqk6AhJ(qvKp&gZ=@j3Wf6GONA-{RI&~T91i1I}#f=2ANc= z)`-57Nx&B%Hyp)zILq8OUx*Fz)yABcPv8K)Zf6n*q5jB}ykuGriFUc}=jbc#^axvT zLU4XPL`7mH@Kg|SPS)}K+>D}BX+}e#Z^PvQRSjpzC+w0yRQS6*-r=z;;^tp~qrVtv zl7&+06hwg*f5hP3Z2S_0G@^j`7v}|9%F7v8(845^Ig#fz&pgl}@6Y(YOh4d*qjh8s zLov|O*`hMq_P>@-VGde{=P4xVe~9lRTEU4u%!UyW9hXrc{v^~Y^CCjWPCE(TcfxZ# zngG}w_>t^$3#yybf?jFy5&M(dY0u6mB4wd7C8KcOP~-*1{FrJv(alN!f7+xMFoJOw zA4u%=-BuJDa|0&I-ksgXX|+QqC--g=(D}q$!F$j-#Y+)mD>sgC>dF?C%td4)sBU8> z(2ziG@ZkG%(F>!0wFjiTXwjZz7&E@ZFyJ0)BlkOjm%V|i z$mmxX9sMC{PTdkZ<(h;{x4>MC9%-WF56td&T!_bhlvklZI{j4LA9*K6EbQTpZO zqrJj;dXV3LL^QLMFtV0E;T>4m#G{7CAF}jeq*CW;22lg=?IuBkrjxRmYYePtEW)yi z7ec3umi4Z&7_>Ab}{?`CgJ9GKN4COtfJBu*Dy*uI2GId~0VZ*XSGB8Kt#D z9x$9Z)H?U#dP)lRjXoK{O14&N4NTQN=OW+m)58fn7VxnV1DFXSA2g<<}`VPN5SloouB|L3Fr)Q_?= zNb1ZTCY)91G`Ug9IDoi3$#cgX!+E4FnQWa>}q`o z@h$(EVv>v2m(f4El%Z{rG7IbyjRfuKJk1l$oR0TgDq$Hfd5L4KiRjE^ty+Z9t{=y* zsoc$g(=#yTB_X_=fBi|9`iRtgH1%xl%ztR+$Rc3?#;d8lw9#O8@DijsHO_iY(f=$d zwj_>Cu)-F)jxpDBB67nrYN>R%UW9kATuhE|n_FTMQ<*YBQ(AOS6iL{U<>Dh@wM!Yk zOeFpV^BP|#Nu{-wGGjT-Iwp-V)EL5a8B$z{^44Tp1@HK&sA`(UeoPW}oz8I&B~CFD zBBB{&5+_f`PN^pLeK+0k2by`uwMCfonz8yO++UG4;~a0Z$yp%NWaap^{$vXWHG>|K zi0y&4)-H@3yrs7S4}fy^KSQr@JZ~&!@9KyS!x0@dWO%>hB2+cD8$qLF4rL+qL|$iG z>sT`3E8TUL9QI~pf$WUZMSC>H3~h*0R15{KIP}qsq9UUll99jMWX%JJWo49vG%fLd zO`>;#(Km^v=uU=MaTN)ZnSM_k^FhfX&%yzS@$?#+-VHaFmSQ{070P6htex9Y5Ze=| zD)3~1qEMKH~`g-V$L;ogP*^3z^VR79n zDXJp392k1i{#F1Lh167SWGw;8&L}aDOyYz&Tcm#%&9AJW3nK_a8cWQPnK_RHH9GaM z0%CJ|oy_ujSD7SY-*Ys51m&8N@<5Z0Abi!0?(U4fo`V6~YCXUpA?6{@C$Y)yBAUKS z`5k5yk5)kT+3Jh=7vnRI&(UWmWczv+$dN~hmqhp|aTFN!7!3ouAxsJ{wePz~U0PgI zBk6DY43xn`)Q!^%FpH8N*0!_b;y=lf4ZRs5})puSX zMBf7r_F~|`B`v&denqXBBx4<&^`6-^ik2+AHMAEe^6ZQvMQ+g`Vc-{t<9D>*voDI~ z{k2+;uEJI3f9alCltc?HJv}n#BUIb>aiS@uwzQ$Nya0O!OH%3{4ru?%MGjR^_~e*c zsVf=5k`VHPfp+cg$|}RPex4VvFhLfTG`V`rs@}|Cwk%M{YULNJDxo#Y<0_^Xa%^4C z&M4vxGj$clt>W;!vK-gGCvhc&pICdCMHXo4MVe-RzoiPfLmzALGg~PwzMR1%K2$YbLF;15r_zx=w1I+6Z=`#- z(5&^`1}R?;7gZ|pRf3p$3p-lu&{ zVr<5|tkK6DK8nf0+Dxw)QI}@DRB6c_Y_beWHdakZZfNpBDqW=nTGK?5ir_^11Ex+a zGgs3dFXeqLK?JJZm@r@llgwK)iR*P^vikKf99(GSTHTr!i0T)~I=oC^V8&Di_kz-$ zz9zE5iDZb+2i`Jc3TK_~@S6L`nD?QiNS8SIG@yy%^T>*6`agD{ihL=pu zo~OUe_9U~(?o3Rf87>0#o59;;&7)+6>>9;0j=bIU3RM+bK)L_JPutrfMXOv+{B_KM zEFrFklY0Srk7p`kDoEs#<%Y5)hGa6O^ny;I^Dyx~!U?&b|0@tl^rQqY6nihQ_7|_J z&`GzKmrD*MV(xSSN}M1LKe1}*Z^_K7s04=AfjmIT(KLUZsQn|cQ1x&WXpW)N3*w16 z1Mwb(n5mHH0e`Es&_dW3Q1m;s4(^h4+qap0J$4Xld*-qpj@zTIQe{-7ir75cyLyf+l;Anj;IB%$B6_SL$A8X{e3zG7A@ zwUBJ~+lc7wr{IrR7ZvtV{P&&N7{#Gsla&{8F-k;IQC{AFvB(GFMe@4MyV3W309#Ed zNG6m!3=`|`AwQ6dEHX+ zY=5snZ$?%LA<}38;_eF)WnG1Yg16f{tLb#auMad$n%QDP$bM3?FL}AGob$)R&3X&u zwNGJq2c1sV_b6m%6y0NX>DJ(3_GRR^;YjRFd0~RTy;j1(LPQA!2~sE9gcyaOnkX$V zr;WQNgwO2RHd^Z5gX`-o(lSBjw|mXeh7XcXsu4RzDQg`F`^HiB(P-mfh=21!sq z8rol<0XY(m>PZ=X(sXc>ZBFv-CQZl}gjqm)b57GUl#18#dI|f<&aSEU(F?>EQ!;(Xg&1p0 z4ar7JrE=}`C(}sj<<;o77=XBtKj$UivO-@(*Wu*Kb)nZl)K{`^{R*qkhJ3L-&_NtoB~Group 28 Copy 5Created with Sketch. \ No newline at end of file diff --git a/code/app/public/pro_icon.svg b/code/app/public/pro_icon.svg new file mode 100644 index 0000000..e075b78 --- /dev/null +++ b/code/app/public/pro_icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/code/app/src/access.ts b/code/app/src/access.ts new file mode 100644 index 0000000..2ec89bd --- /dev/null +++ b/code/app/src/access.ts @@ -0,0 +1,9 @@ +/** + * @see https://umijs.org/zh-CN/plugins/plugin-access + * */ +export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) { + const { currentUser } = initialState || {}; + return { + canAdmin: currentUser && currentUser.access === 'admin', + }; +} diff --git a/code/app/src/app.tsx b/code/app/src/app.tsx new file mode 100644 index 0000000..74e15b3 --- /dev/null +++ b/code/app/src/app.tsx @@ -0,0 +1,133 @@ +import type { Settings as LayoutSettings } from '@ant-design/pro-layout'; +import { PageLoading } from '@ant-design/pro-layout'; +import { notification } from 'antd'; +import type { RequestConfig, RunTimeLayoutConfig } from 'umi'; +import { history, Link } from 'umi'; +import RightContent from '@/components/RightContent'; +import Footer from '@/components/Footer'; +import { UserService } from './services/api'; +import { BookOutlined, LinkOutlined } from '@ant-design/icons'; + +const isDev = process.env.NODE_ENV === 'development'; +const loginPath = '/user/login'; + +/** 获取用户信息比较慢的时候会展示一个 loading */ +export const initialStateConfig = { + loading: , +}; + +/** + * @see https://umijs.org/zh-CN/plugins/plugin-initial-state + * */ +export async function getInitialState(): Promise<{ + settings?: Partial; + currentUser?: API.CurrentUser; + fetchUserInfo?: () => Promise; +}> { + const fetchUserInfo = async () => { + try { + const msg = await UserService.currentUser(); + return msg.data; + } catch (error) { + history.push(loginPath); + } + return undefined; + }; + // 如果是登录页面,不执行 + if (history.location.pathname !== loginPath) { + const currentUser = await fetchUserInfo(); + return { + fetchUserInfo, + currentUser, + settings: {}, + }; + } + return { + fetchUserInfo, + settings: {}, + }; +} + +/** + * 异常处理程序 + 200: '服务器成功返回请求的数据。', + 201: '新建或修改数据成功。', + 202: '一个请求已经进入后台排队(异步任务)。', + 204: '删除数据成功。', + 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', + 401: '用户没有权限(令牌、用户名、密码错误)。', + 403: '用户得到授权,但是访问是被禁止的。', + 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', + 405: '请求方法不被允许。', + 406: '请求的格式不可得。', + 410: '请求的资源被永久删除,且不会再得到的。', + 422: '当创建一个对象时,发生一个验证错误。', + 500: '服务器发生错误,请检查服务器。', + 502: '网关错误。', + 503: '服务不可用,服务器暂时过载或维护。', + 504: '网关超时。', + //-----English + 200: The server successfully returned the requested data. ', + 201: New or modified data is successful. ', + 202: A request has entered the background queue (asynchronous task). ', + 204: Data deleted successfully. ', + 400: 'There was an error in the request sent, and the server did not create or modify data. ', + 401: The user does not have permission (token, username, password error). ', + 403: The user is authorized, but access is forbidden. ', + 404: The request sent was for a record that did not exist. ', + 405: The request method is not allowed. ', + 406: The requested format is not available. ', + 410': + 'The requested resource is permanently deleted and will no longer be available. ', + 422: When creating an object, a validation error occurred. ', + 500: An error occurred on the server, please check the server. ', + 502: Gateway error. ', + 503: The service is unavailable. ', + 504: The gateway timed out. ', + * @see https://beta-pro.ant.design/docs/request-cn + */ +export const request: RequestConfig = { + errorHandler: (error: any) => { + const { response } = error; + + if (!response) { + notification.error({ + description: '您的网络发生异常,无法连接服务器', + message: '网络异常', + }); + } + throw error; + }, +}; + +// ProLayout 支持的api https://procomponents.ant.design/components/layout +export const layout: RunTimeLayoutConfig = ({ initialState }) => { + return { + rightContentRender: () => , + disableContentMargin: false, + footerRender: () =>