From d13eede0d37d4201be921d66b27ffabd9b827f0d Mon Sep 17 00:00:00 2001 From: medmin Date: Thu, 10 Oct 2019 22:47:40 -0700 Subject: [PATCH] init --- .editorconfig | 16 + .eslintignore | 4 + .eslintrc.js | 9 + .github/ISSUE_TEMPLATE/bug_report.md | 24 + .github/ISSUE_TEMPLATE/feature_request.md | 13 + .github/ISSUE_TEMPLATE/question.md | 13 + .gitignore | 39 + .prettierignore | 20 + .prettierrc.js | 5 + .stylelintrc.js | 5 + README.md | 57 + config/config.ts | 191 + config/defaultSettings.ts | 60 + config/plugin.config.ts | 104 + jest-puppeteer.config.js | 12 + jest.config.js | 7 + jsconfig.json | 10 + mock/notices.ts | 105 + mock/route.ts | 5 + mock/user.ts | 145 + package.json | 106 + public/favicon.png | Bin 0 -> 2849 bytes public/icons/icon-128x128.png | Bin 0 -> 1329 bytes public/icons/icon-192x192.png | Bin 0 -> 1856 bytes public/icons/icon-512x512.png | Bin 0 -> 5082 bytes src/assets/logo.svg | 43 + src/components/Authorized/Authorized.tsx | 28 + src/components/Authorized/AuthorizedRoute.tsx | 33 + .../Authorized/CheckPermissions.tsx | 83 + src/components/Authorized/PromiseRender.tsx | 93 + src/components/Authorized/Secured.tsx | 66 + src/components/Authorized/index.tsx | 13 + src/components/Authorized/renderAuthorize.ts | 30 + src/components/CopyBlock/index.less | 29 + src/components/CopyBlock/index.tsx | 80 + .../GlobalHeader/AvatarDropdown.tsx | 75 + .../GlobalHeader/NoticeIconView.tsx | 154 + src/components/GlobalHeader/RightContent.tsx | 74 + src/components/GlobalHeader/index.less | 134 + src/components/HeaderDropdown/index.less | 16 + src/components/HeaderDropdown/index.tsx | 19 + src/components/HeaderSearch/index.less | 32 + src/components/HeaderSearch/index.tsx | 146 + src/components/NoticeIcon/NoticeList.less | 105 + src/components/NoticeIcon/NoticeList.tsx | 114 + src/components/NoticeIcon/index.less | 31 + src/components/NoticeIcon/index.tsx | 175 + src/components/PageLoading/index.tsx | 11 + src/components/SelectLang/index.less | 24 + src/components/SelectLang/index.tsx | 51 + .../SettingDrawer/themeColorClient.ts | 31 + src/e2e/__mocks__/antd-pro-merge-less.js | 1 + src/e2e/baseLayout.e2e.js | 39 + src/e2e/topMenu.e2e.js | 15 + src/global.less | 47 + src/global.tsx | 83 + src/layouts/BasicLayout.tsx | 146 + src/layouts/BlankLayout.tsx | 5 + src/layouts/SecurityLayout.tsx | 58 + src/layouts/UserLayout.less | 71 + src/layouts/UserLayout.tsx | 65 + src/locales/en-US.ts | 22 + src/locales/en-US/component.ts | 5 + src/locales/en-US/globalHeader.ts | 17 + src/locales/en-US/menu.ts | 50 + src/locales/en-US/pwa.ts | 6 + src/locales/en-US/settingDrawer.ts | 31 + src/locales/en-US/settings.ts | 60 + src/locales/pt-BR.ts | 20 + src/locales/pt-BR/component.ts | 5 + src/locales/pt-BR/globalHeader.ts | 18 + src/locales/pt-BR/menu.ts | 51 + src/locales/pt-BR/pwa.ts | 7 + src/locales/pt-BR/settingDrawer.ts | 32 + src/locales/pt-BR/settings.ts | 60 + src/locales/zh-CN.ts | 22 + src/locales/zh-CN/component.ts | 5 + src/locales/zh-CN/globalHeader.ts | 17 + src/locales/zh-CN/menu.ts | 50 + src/locales/zh-CN/pwa.ts | 6 + src/locales/zh-CN/settingDrawer.ts | 31 + src/locales/zh-CN/settings.ts | 55 + src/locales/zh-TW.ts | 20 + src/locales/zh-TW/component.ts | 5 + src/locales/zh-TW/globalHeader.ts | 17 + src/locales/zh-TW/menu.ts | 51 + src/locales/zh-TW/pwa.ts | 6 + src/locales/zh-TW/settingDrawer.ts | 31 + src/locales/zh-TW/settings.ts | 55 + src/manifest.json | 22 + src/models/connect.d.ts | 40 + src/models/global.ts | 139 + src/models/login.ts | 95 + src/models/setting.ts | 89 + src/models/user.ts | 86 + src/pages/404.tsx | 21 + src/pages/Authorized.tsx | 58 + src/pages/Welcome.tsx | 66 + src/pages/document.ejs | 168 + .../login/components/Login/LoginContext.tsx | 13 + .../user/login/components/Login/LoginItem.tsx | 196 + .../login/components/Login/LoginSubmit.tsx | 23 + .../user/login/components/Login/LoginTab.tsx | 53 + .../user/login/components/Login/index.less | 53 + .../user/login/components/Login/index.tsx | 173 + src/pages/user/login/components/Login/map.tsx | 65 + src/pages/user/login/index.tsx | 205 + src/pages/user/login/locales/en-US.ts | 78 + src/pages/user/login/locales/zh-CN.ts | 74 + src/pages/user/login/locales/zh-TW.ts | 74 + src/pages/user/login/style.less | 39 + src/service-worker.js | 66 + src/services/login.ts | 19 + src/services/user.ts | 13 + src/typings.d.ts | 43 + src/utils/Authorized.ts | 13 + src/utils/authority.test.ts | 16 + src/utils/authority.ts | 29 + src/utils/request.ts | 56 + src/utils/utils.less | 50 + src/utils/utils.test.ts | 37 + src/utils/utils.ts | 24 + tests/run-tests.js | 49 + tsconfig.json | 34 + yarn.lock | 17612 ++++++++++++++++ 125 files changed, 23816 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 .stylelintrc.js create mode 100644 README.md create mode 100644 config/config.ts create mode 100644 config/defaultSettings.ts create mode 100644 config/plugin.config.ts create mode 100644 jest-puppeteer.config.js create mode 100644 jest.config.js create mode 100644 jsconfig.json create mode 100644 mock/notices.ts create mode 100644 mock/route.ts create mode 100644 mock/user.ts create mode 100644 package.json create mode 100644 public/favicon.png create mode 100644 public/icons/icon-128x128.png create mode 100644 public/icons/icon-192x192.png create mode 100644 public/icons/icon-512x512.png create mode 100644 src/assets/logo.svg create mode 100644 src/components/Authorized/Authorized.tsx create mode 100644 src/components/Authorized/AuthorizedRoute.tsx create mode 100644 src/components/Authorized/CheckPermissions.tsx create mode 100644 src/components/Authorized/PromiseRender.tsx create mode 100644 src/components/Authorized/Secured.tsx create mode 100644 src/components/Authorized/index.tsx create mode 100644 src/components/Authorized/renderAuthorize.ts create mode 100644 src/components/CopyBlock/index.less create mode 100644 src/components/CopyBlock/index.tsx create mode 100644 src/components/GlobalHeader/AvatarDropdown.tsx create mode 100644 src/components/GlobalHeader/NoticeIconView.tsx create mode 100644 src/components/GlobalHeader/RightContent.tsx create mode 100644 src/components/GlobalHeader/index.less create mode 100644 src/components/HeaderDropdown/index.less create mode 100644 src/components/HeaderDropdown/index.tsx create mode 100644 src/components/HeaderSearch/index.less create mode 100644 src/components/HeaderSearch/index.tsx create mode 100644 src/components/NoticeIcon/NoticeList.less create mode 100644 src/components/NoticeIcon/NoticeList.tsx create mode 100644 src/components/NoticeIcon/index.less create mode 100644 src/components/NoticeIcon/index.tsx create mode 100644 src/components/PageLoading/index.tsx create mode 100644 src/components/SelectLang/index.less create mode 100644 src/components/SelectLang/index.tsx create mode 100644 src/components/SettingDrawer/themeColorClient.ts create mode 100644 src/e2e/__mocks__/antd-pro-merge-less.js create mode 100644 src/e2e/baseLayout.e2e.js create mode 100644 src/e2e/topMenu.e2e.js create mode 100644 src/global.less create mode 100644 src/global.tsx create mode 100644 src/layouts/BasicLayout.tsx create mode 100644 src/layouts/BlankLayout.tsx create mode 100644 src/layouts/SecurityLayout.tsx create mode 100644 src/layouts/UserLayout.less create mode 100644 src/layouts/UserLayout.tsx create mode 100644 src/locales/en-US.ts create mode 100644 src/locales/en-US/component.ts create mode 100644 src/locales/en-US/globalHeader.ts create mode 100644 src/locales/en-US/menu.ts create mode 100644 src/locales/en-US/pwa.ts create mode 100644 src/locales/en-US/settingDrawer.ts create mode 100644 src/locales/en-US/settings.ts create mode 100644 src/locales/pt-BR.ts create mode 100644 src/locales/pt-BR/component.ts create mode 100644 src/locales/pt-BR/globalHeader.ts create mode 100644 src/locales/pt-BR/menu.ts create mode 100644 src/locales/pt-BR/pwa.ts create mode 100644 src/locales/pt-BR/settingDrawer.ts create mode 100644 src/locales/pt-BR/settings.ts create mode 100644 src/locales/zh-CN.ts create mode 100644 src/locales/zh-CN/component.ts create mode 100644 src/locales/zh-CN/globalHeader.ts create mode 100644 src/locales/zh-CN/menu.ts create mode 100644 src/locales/zh-CN/pwa.ts create mode 100644 src/locales/zh-CN/settingDrawer.ts create mode 100644 src/locales/zh-CN/settings.ts create mode 100644 src/locales/zh-TW.ts create mode 100644 src/locales/zh-TW/component.ts create mode 100644 src/locales/zh-TW/globalHeader.ts create mode 100644 src/locales/zh-TW/menu.ts create mode 100644 src/locales/zh-TW/pwa.ts create mode 100644 src/locales/zh-TW/settingDrawer.ts create mode 100644 src/locales/zh-TW/settings.ts create mode 100644 src/manifest.json create mode 100644 src/models/connect.d.ts create mode 100644 src/models/global.ts create mode 100644 src/models/login.ts create mode 100644 src/models/setting.ts create mode 100644 src/models/user.ts create mode 100644 src/pages/404.tsx create mode 100644 src/pages/Authorized.tsx create mode 100644 src/pages/Welcome.tsx create mode 100644 src/pages/document.ejs create mode 100644 src/pages/user/login/components/Login/LoginContext.tsx create mode 100644 src/pages/user/login/components/Login/LoginItem.tsx create mode 100644 src/pages/user/login/components/Login/LoginSubmit.tsx create mode 100644 src/pages/user/login/components/Login/LoginTab.tsx create mode 100644 src/pages/user/login/components/Login/index.less create mode 100644 src/pages/user/login/components/Login/index.tsx create mode 100644 src/pages/user/login/components/Login/map.tsx create mode 100644 src/pages/user/login/index.tsx create mode 100644 src/pages/user/login/locales/en-US.ts create mode 100644 src/pages/user/login/locales/zh-CN.ts create mode 100644 src/pages/user/login/locales/zh-TW.ts create mode 100644 src/pages/user/login/style.less create mode 100644 src/service-worker.js create mode 100644 src/services/login.ts create mode 100644 src/services/user.ts create mode 100644 src/typings.d.ts create mode 100644 src/utils/Authorized.ts create mode 100644 src/utils/authority.test.ts create mode 100644 src/utils/authority.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/utils.less create mode 100644 src/utils/utils.test.ts create mode 100644 src/utils/utils.ts create mode 100644 tests/run-tests.js create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7e3649a --- /dev/null +++ b/.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/.eslintignore b/.eslintignore new file mode 100644 index 0000000..16116a2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +/lambda/ +/scripts +/config +.history \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..929f3ee --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,9 @@ +const { strictEslint } = require('@umijs/fabric'); + +module.exports = { + ...strictEslint, + globals: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, + page: true, + }, +}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..eb90f97 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: '报告Bug 🐛' +about: 报告 Ant Design Pro 的 bug +title: '[BUG]' +labels: bug +assignees: '' +--- + +**bug 描述** [详细地描述 bug,让大家都能理解] + +**复现步骤** [清晰描述复现步骤,让别人也能看到问题] + +**期望结果** [描述你原本期望看到的结果] + +**复现代码** [提供可复现的代码,仓库,或线上示例] + +**版本信息:** + +- Ant Design Pro 版本: [e.g. 4.0.0] +- umi 版本 +- 浏览器环境 +- 开发环境 [e.g. mac OS] + +**其他信息** [如截图等其他信息可以贴在这里] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bdc3858 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,13 @@ +--- +name: '功能需求 ✨' +about: 对 Ant Design Pro 的需求或建议 +title: '[需求]' +labels: feature +assignees: '' +--- + +**需求描述** [详细地描述需求,让大家都能理解] + +**解决方案** [如果你有解决方案,在这里清晰地阐述] + +**其他信息** [如截图等其他信息可以贴在这里] diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..3ad4067 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +--- +name: '疑问或需要帮助 ❓' +about: 对 Ant Design Pro 使用的疑问或需要帮助 +title: '[问题]' +labels: question +assignees: '' +--- + +**问题描述** [详细地描述问题,让大家都能理解] + +**示例代码** [如果有必要,展示代码,线上示例,或仓库] + +**其他信息** [如截图等其他信息可以贴在这里] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b08dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# 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 +package-lock.json +*bak +.vscode + +# visual studio code +.history +*.log +functions/* +.temp/** + +# umi +.umi +.umi-production + +# screenshot +screenshot +.firebase +.eslintcache + +build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4fa82fc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +**/*.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 \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..7b597d7 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.prettier, +}; diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000..c203078 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.stylelint, +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c89a72 --- /dev/null +++ b/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/config/config.ts b/config/config.ts new file mode 100644 index 0000000..43c2006 --- /dev/null +++ b/config/config.ts @@ -0,0 +1,191 @@ +import { IConfig, IPlugin } from 'umi-types'; +import defaultSettings from './defaultSettings'; // https://umijs.org/config/ + +import slash from 'slash2'; +import webpackPlugin from './plugin.config'; +const { pwa, primaryColor } = defaultSettings; + +// preview.pro.ant.design only do not use in your production ; +// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 +const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env; +const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site'; +const plugins: IPlugin[] = [ + [ + 'umi-plugin-react', + { + antd: true, + dva: { + hmr: true, + }, + locale: { + // default false + enable: true, + // default zh-CN + default: 'zh-CN', + // default true, when it is true, will use `navigator.language` overwrite default + baseNavigator: true, + }, + // dynamicImport: { + // loadingComponent: './components/PageLoading/index', + // webpackChunkName: true, + // level: 3, + // }, + pwa: pwa + ? { + workboxPluginMode: 'InjectManifest', + workboxOptions: { + importWorkboxFrom: 'local', + }, + } + : false, + // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665 + // dll features https://webpack.js.org/plugins/dll-plugin/ + // dll: { + // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'], + // exclude: ['@babel/runtime', 'netlify-lambda'], + // }, + }, + ], + [ + 'umi-plugin-pro-block', + { + moveMock: false, + moveService: false, + modifyRequest: true, + autoAddMenu: true, + }, + ], +]; // 针对 preview.pro.ant.design 的 GA 统计代码 + +if (isAntDesignProPreview) { + plugins.push([ + 'umi-plugin-ga', + { + code: 'UA-72788897-6', + }, + ]); + plugins.push([ + 'umi-plugin-pro', + { + serverUrl: 'https://ant-design-pro.netlify.com', + }, + ]); +} + +export default { + plugins, + block: { + // 国内用户可以使用码云 + // defaultGitUrl: 'https://gitee.com/ant-design/pro-blocks', + defaultGitUrl: 'https://github.com/ant-design/pro-blocks', + }, + hash: true, + targets: { + ie: 11, + }, + devtool: isAntDesignProPreview ? 'source-map' : false, + // umi routes: https://umijs.org/zh/guide/router.html + routes: [ + { + path: '/user', + component: '../layouts/UserLayout', + routes: [ + { + name: 'login', + path: '/user/login', + component: './user/login', + }, + ], + }, + { + path: '/', + component: '../layouts/SecurityLayout', + routes: [ + { + path: '/', + component: '../layouts/BasicLayout', + authority: ['admin', 'user'], + routes: [ + { + path: '/', + redirect: '/welcome', + }, + { + path: '/welcome', + name: 'welcome', + icon: 'smile', + component: './Welcome', + }, + { + component: './404', + }, + ], + }, + { + component: './404', + }, + ], + }, + + { + component: './404', + }, + ], + // Theme for antd: https://ant.design/docs/react/customize-theme-cn + theme: { + 'primary-color': primaryColor, + }, + define: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 + }, + ignoreMomentLocale: true, + lessLoaderOptions: { + javascriptEnabled: true, + }, + disableRedirectHoist: true, + cssLoaderOptions: { + modules: true, + getLocalIdent: ( + context: { + resourcePath: string; + }, + _: string, + localName: string, + ) => { + if ( + context.resourcePath.includes('node_modules') || + context.resourcePath.includes('ant.design.pro.less') || + context.resourcePath.includes('global.less') + ) { + return localName; + } + + const match = context.resourcePath.match(/src(.*)/); + + if (match && match[1]) { + const antdProPath = match[1].replace('.less', ''); + const arr = slash(antdProPath) + .split('/') + .map((a: string) => a.replace(/([A-Z])/g, '-$1')) + .map((a: string) => a.toLowerCase()); + return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-'); + } + + return localName; + }, + }, + manifest: { + basePath: '/', + }, + chainWebpack: webpackPlugin, + /* + proxy: { + '/server/api/': { + target: 'https://preview.pro.ant.design/', + changeOrigin: true, + pathRewrite: { '^/server': '' }, + }, + }, + */ +} as IConfig; diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts new file mode 100644 index 0000000..4d86094 --- /dev/null +++ b/config/defaultSettings.ts @@ -0,0 +1,60 @@ +import { MenuTheme } from 'antd/es/menu/MenuContext'; + +export type ContentWidth = 'Fluid' | 'Fixed'; + +export interface DefaultSettings { + /** + * theme for nav menu + */ + navTheme: MenuTheme; + /** + * primary color of ant design + */ + primaryColor: string; + /** + * nav menu position: `sidemenu` or `topmenu` + */ + layout: 'sidemenu' | 'topmenu'; + /** + * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu + */ + contentWidth: ContentWidth; + /** + * sticky header + */ + fixedHeader: boolean; + /** + * auto hide header + */ + autoHideHeader: boolean; + /** + * sticky siderbar + */ + fixSiderbar: boolean; + menu: { locale: boolean }; + title: string; + pwa: boolean; + // Your custom iconfont Symbol script Url + // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js + // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理 + // Usage: https://github.com/ant-design/ant-design-pro/pull/3517 + iconfontUrl: string; + colorWeak: boolean; +} + +export default { + navTheme: 'dark', + primaryColor: '#1890FF', + layout: 'sidemenu', + contentWidth: 'Fluid', + fixedHeader: false, + autoHideHeader: false, + fixSiderbar: false, + colorWeak: false, + menu: { + locale: true, + }, + title: 'Ant Design Pro', + pwa: false, + iconfontUrl: '', +} as DefaultSettings; diff --git a/config/plugin.config.ts b/config/plugin.config.ts new file mode 100644 index 0000000..92a6140 --- /dev/null +++ b/config/plugin.config.ts @@ -0,0 +1,104 @@ +// Change theme plugin +// eslint-disable-next-line eslint-comments/abdeils - enable - pair; +/* eslint-disable import/no-extraneous-dependencies */ +import ThemeColorReplacer from 'webpack-theme-color-replacer'; +import generate from '@ant-design/colors/lib/generate'; +import path from 'path'; + +function getModulePackageName(module: { context: string }) { + if (!module.context) return null; + + const nodeModulesPath = path.join(__dirname, '../node_modules/'); + if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) { + return null; + } + + const moduleRelativePath = module.context.substring(nodeModulesPath.length); + const [moduleDirName] = moduleRelativePath.split(path.sep); + let packageName: string | null = moduleDirName; + // handle tree shaking + if (packageName && packageName.match('^_')) { + // eslint-disable-next-line prefer-destructuring + packageName = packageName.match(/^_(@?[^@]+)/)![1]; + } + return packageName; +} + +export default (config: any) => { + // preview.pro.ant.design only do not use in your production; + if ( + process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' || + process.env.NODE_ENV !== 'production' + ) { + config.plugin('webpack-theme-color-replacer').use(ThemeColorReplacer, [ + { + fileName: 'css/theme-colors-[contenthash:8].css', + matchColors: getAntdSerials('#1890ff'), // 主色系列 + // 改变样式选择器,解决样式覆盖问题 + changeSelector(selector: string): string { + switch (selector) { + case '.ant-calendar-today .ant-calendar-date': + return ':not(.ant-calendar-selected-date)' + selector; + case '.ant-btn:focus,.ant-btn:hover': + return '.ant-btn:focus:not(.ant-btn-primary),.ant-btn:hover:not(.ant-btn-primary)'; + case '.ant-btn.active,.ant-btn:active': + return '.ant-btn.active:not(.ant-btn-primary),.ant-btn:active:not(.ant-btn-primary)'; + default: + return selector; + } + }, + // isJsUgly: true, + }, + ]); + } + + // optimize chunks + config.optimization + // share the same chunks across different modules + .runtimeChunk(false) + .splitChunks({ + chunks: 'async', + name: 'vendors', + maxInitialRequests: Infinity, + minSize: 0, + cacheGroups: { + vendors: { + test: (module: { context: string }) => { + const packageName = getModulePackageName(module) || ''; + if (packageName) { + return [ + 'bizcharts', + 'gg-editor', + 'g6', + '@antv', + 'gg-editor-core', + 'bizcharts-plugin-slider', + ].includes(packageName); + } + return false; + }, + name(module: { context: string }) { + const packageName = getModulePackageName(module); + if (packageName) { + if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) { + return 'viz'; // visualization package + } + } + return 'misc'; + }, + }, + }, + }); +}; + +const getAntdSerials = (color: string) => { + const lightNum = 9; + const devide10 = 10; + // 淡化(即less的tint) + const lightens = new Array(lightNum).fill(undefined).map((_, i: number) => { + return ThemeColorReplacer.varyColor.lighten(color, i / devide10); + }); + const colorPalettes = generate(color); + const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(','); + return lightens.concat(colorPalettes).concat(rgb); +}; diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js new file mode 100644 index 0000000..21b41e4 --- /dev/null +++ b/jest-puppeteer.config.js @@ -0,0 +1,12 @@ +// ps https://github.com/GoogleChrome/puppeteer/issues/3120 +module.exports = { + launch: { + args: [ + '--disable-gpu', + '--disable-dev-shm-usage', + '--no-first-run', + '--no-zygote', + '--no-sandbox', + ], + }, +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..502fe33 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testURL: 'http://localhost:8000', + preset: 'jest-puppeteer', + globals: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, + }, +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..f87334d --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/mock/notices.ts b/mock/notices.ts new file mode 100644 index 0000000..b9e3bf2 --- /dev/null +++ b/mock/notices.ts @@ -0,0 +1,105 @@ +import { Request, Response } from 'express'; + +const getNotices = (req: Request, res: Response) => { + res.json([ + { + 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/mock/route.ts b/mock/route.ts new file mode 100644 index 0000000..418d10f --- /dev/null +++ b/mock/route.ts @@ -0,0 +1,5 @@ +export default { + '/api/auth_routes': { + '/form/advanced-form': { authority: ['admin', 'user'] }, + }, +}; diff --git a/mock/user.ts b/mock/user.ts new file mode 100644 index 0000000..80cbd91 --- /dev/null +++ b/mock/user.ts @@ -0,0 +1,145 @@ +import { Request, Response } from 'express'; + +function getFakeCaptcha(req: Request, res: Response) { + return res.json('captcha-xxx'); +} +// 代码中会兼容本地 service mock 以及部署站点的静态数据 +export default { + // 支持值为 Object 和 Array + 'GET /api/currentUser': { + 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', + 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': (req: Request, res: Response) => { + const { password, userName, type } = req.body; + if (password === 'ant.design' && userName === 'admin') { + res.send({ + status: 'ok', + type, + currentAuthority: 'admin', + }); + return; + } + if (password === 'ant.design' && userName === 'user') { + res.send({ + status: 'ok', + type, + currentAuthority: 'user', + }); + return; + } + res.send({ + status: 'error', + type, + currentAuthority: 'guest', + }); + }, + 'POST /api/register': (req: Request, res: Response) => { + res.send({ status: 'ok', currentAuthority: 'user' }); + }, + '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/package.json b/package.json new file mode 100644 index 0000000..4b4915c --- /dev/null +++ b/package.json @@ -0,0 +1,106 @@ +{ + "name": "ant-design-pro", + "version": "1.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": "cross-env ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION=site npm run site && npm run gh-pages", + "fetch:blocks": "pro fetch-blocks", + "format-imports": "cross-env import-sort --write '**/*.{js,jsx,ts,tsx}'", + "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist", + "i18n-remove": "pro i18n-remove --locale=zh-CN --write", + "lint": "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": "check-prettier lint", + "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", + "prettier": "prettier -c --write \"**/*\"", + "start": "umi dev", + "start:no-mock": "cross-env MOCK=none umi dev", + "test": "umi test", + "test:all": "node ./tests/run-tests.js", + "test:component": "umi test ./src/components", + "ui": "umi ui" + }, + "husky": { "hooks": { "pre-commit": "npm run lint-staged" } }, + "lint-staged": { + "**/*.less": "stylelint --syntax less", + "**/*.{js,jsx,tsx,ts,less,md,json}": ["prettier --write", "git add"], + "**/*.{js,jsx}": "npm run lint-staged:js", + "**/*.{js,ts,tsx}": "npm run lint-staged:js" + }, + "browserslist": ["> 1%", "last 2 versions", "not ie <= 10"], + "dependencies": { + "@ant-design/pro-layout": "^4.5.16", + "@antv/data-set": "^0.10.2", + "antd": "^3.23.6", + "classnames": "^2.2.6", + "dva": "^2.4.1", + "lodash": "^4.17.11", + "moment": "^2.24.0", + "omit.js": "^1.0.2", + "path-to-regexp": "^3.1.0", + "qs": "^6.9.0", + "react": "^16.8.6", + "react-copy-to-clipboard": "^5.0.1", + "react-document-title": "^2.0.3", + "react-dom": "^16.8.6", + "redux": "^4.0.1", + "umi": "^2.9.6", + "umi-plugin-pro-block": "^1.3.4", + "umi-plugin-react": "^1.10.1", + "umi-request": "^1.2.7" + }, + "devDependencies": { + "@ant-design/colors": "^3.1.0", + "@ant-design/pro-cli": "^1.0.13", + "@types/classnames": "^2.2.7", + "@types/express": "^4.17.0", + "@types/history": "^4.7.2", + "@types/jest": "^24.0.13", + "@types/lodash": "^4.14.144", + "@types/qs": "^6.5.3", + "@types/react": "^16.8.19", + "@types/react-document-title": "^2.0.3", + "@types/react-dom": "^16.8.4", + "@umijs/fabric": "^1.1.0", + "chalk": "^2.4.2", + "check-prettier": "^1.0.3", + "cross-env": "^6.0.0", + "cross-port-killer": "^1.1.1", + "enzyme": "^3.9.0", + "eslint": "5.16.0", + "express": "^4.17.1", + "gh-pages": "^2.0.1", + "husky": "^3.0.0", + "import-sort-cli": "^6.0.0", + "import-sort-parser-babylon": "^6.0.0", + "import-sort-parser-typescript": "^6.0.0", + "import-sort-style-module": "^6.0.0", + "jest-puppeteer": "^4.2.0", + "lint-staged": "^9.0.0", + "mockjs": "^1.0.1-beta3", + "node-fetch": "^2.6.0", + "prettier": "^1.17.1", + "pro-download": "1.0.1", + "slash2": "^2.0.0", + "stylelint": "^10.1.0", + "umi-plugin-ga": "^1.1.3", + "umi-plugin-pro": "^1.0.2", + "umi-types": "^0.5.0", + "webpack-theme-color-replacer": "^1.2.15" + }, + "optionalDependencies": { "puppeteer": "^1.17.0" }, + "engines": { "node": ">=10.0.0" }, + "checkFiles": [ + "src/**/*.js*", + "src/**/*.ts*", + "src/**/*.less", + "config/**/*.js*", + "scripts/**/*.js" + ] +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..ece59ce54690c0e1c1e6984ec9dd815645ab4cb8 GIT binary patch literal 2849 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 literal 0 HcmV?d00001 diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..48d0e2339a60a637b94319c65e8654289b4f4b6c GIT binary patch literal 1329 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

*C7T$?5!b= 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 + + + Group 28 Copy 5 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx new file mode 100644 index 0000000..b5eff80 --- /dev/null +++ b/src/components/Authorized/Authorized.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import check, { IAuthorityType } from './CheckPermissions'; + +import AuthorizedRoute from './AuthorizedRoute'; +import Secured from './Secured'; + +interface AuthorizedProps { + authority: IAuthorityType; + noMatch?: React.ReactNode; +} + +type IAuthorizedType = React.FunctionComponent & { + Secured: typeof Secured; + check: typeof check; + AuthorizedRoute: typeof AuthorizedRoute; +}; + +const Authorized: React.FunctionComponent = ({ + children, + authority, + noMatch = null, +}) => { + const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; + const dom = check(authority, childrenRender, noMatch); + return <>{dom}; +}; + +export default Authorized as IAuthorizedType; diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx new file mode 100644 index 0000000..7743eae --- /dev/null +++ b/src/components/Authorized/AuthorizedRoute.tsx @@ -0,0 +1,33 @@ +import { Redirect, Route } from 'umi'; + +import React from 'react'; +import Authorized from './Authorized'; +import { IAuthorityType } from './CheckPermissions'; + +interface AuthorizedRoutePops { + currentAuthority: string; + component: React.ComponentClass; + render: (props: any) => React.ReactNode; + redirectPath: string; + authority: IAuthorityType; +} + +const AuthorizedRoute: React.SFC = ({ + component: Component, + render, + authority, + redirectPath, + ...rest +}) => ( + } />} + > + (Component ? : render(props))} + /> + +); + +export default AuthorizedRoute; diff --git a/src/components/Authorized/CheckPermissions.tsx b/src/components/Authorized/CheckPermissions.tsx new file mode 100644 index 0000000..caa15a3 --- /dev/null +++ b/src/components/Authorized/CheckPermissions.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { CURRENT } from './renderAuthorize'; +// eslint-disable-next-line import/no-cycle +import PromiseRender from './PromiseRender'; + +export type IAuthorityType = + | undefined + | string + | string[] + | Promise + | ((currentAuthority: string | string[]) => IAuthorityType); + +/** + * 通用权限检查方法 + * Common check permissions method + * @param { 权限判定 | Permission judgment } authority + * @param { 你的权限 | Your permission description } currentAuthority + * @param { 通过的组件 | Passing components } target + * @param { 未通过的组件 | no pass components } Exception + */ +const checkPermissions = ( + authority: IAuthorityType, + currentAuthority: string | string[], + target: T, + Exception: K, +): T | K | React.ReactNode => { + // 没有判定权限.默认查看所有 + // Retirement authority, return target; + if (!authority) { + return target; + } + // 数组处理 + if (Array.isArray(authority)) { + if (Array.isArray(currentAuthority)) { + if (currentAuthority.some(item => authority.includes(item))) { + return target; + } + } else if (authority.includes(currentAuthority)) { + return target; + } + return Exception; + } + // string 处理 + if (typeof authority === 'string') { + if (Array.isArray(currentAuthority)) { + if (currentAuthority.some(item => authority === item)) { + return target; + } + } else if (authority === currentAuthority) { + return target; + } + return Exception; + } + // Promise 处理 + if (authority instanceof Promise) { + return ok={target} error={Exception} promise={authority} />; + } + // Function 处理 + if (typeof authority === 'function') { + try { + const bool = authority(currentAuthority); + // 函数执行后返回值是 Promise + if (bool instanceof Promise) { + return ok={target} error={Exception} promise={bool} />; + } + if (bool) { + return target; + } + return Exception; + } catch (error) { + throw error; + } + } + throw new Error('unsupported parameters'); +}; + +export { checkPermissions }; + +function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { + return checkPermissions(authority, CURRENT, target, Exception); +} + +export default check; diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx new file mode 100644 index 0000000..10b8fc9 --- /dev/null +++ b/src/components/Authorized/PromiseRender.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Spin } from 'antd'; +import isEqual from 'lodash/isEqual'; +import { isComponentClass } from './Secured'; +// eslint-disable-next-line import/no-cycle + +interface PromiseRenderProps { + ok: T; + error: K; + promise: Promise; +} + +interface PromiseRenderState { + component: React.ComponentClass | React.FunctionComponent; +} + +export default class PromiseRender extends React.Component< + PromiseRenderProps, + PromiseRenderState +> { + state: PromiseRenderState = { + component: () => null, + }; + + componentDidMount() { + this.setRenderComponent(this.props); + } + + shouldComponentUpdate = (nextProps: PromiseRenderProps, nextState: PromiseRenderState) => { + const { component } = this.state; + if (!isEqual(nextProps, this.props)) { + this.setRenderComponent(nextProps); + } + if (nextState.component !== component) return true; + return false; + }; + + // set render Component : ok or error + setRenderComponent(props: PromiseRenderProps) { + const ok = this.checkIsInstantiation(props.ok); + const error = this.checkIsInstantiation(props.error); + props.promise + .then(() => { + this.setState({ + component: ok, + }); + return true; + }) + .catch(() => { + this.setState({ + component: error, + }); + }); + } + + // Determine whether the incoming component has been instantiated + // AuthorizedRoute is already instantiated + // Authorized render is already instantiated, children is no instantiated + // Secured is not instantiated + checkIsInstantiation = ( + target: React.ReactNode | React.ComponentClass, + ): React.FunctionComponent => { + if (isComponentClass(target)) { + const Target = target as React.ComponentClass; + return (props: any) => ; + } + if (React.isValidElement(target)) { + return (props: any) => React.cloneElement(target, props); + } + return () => target as (React.ReactNode & null); + }; + + render() { + const { component: Component } = this.state; + const { ok, error, promise, ...rest } = this.props; + + return Component ? ( + + ) : ( +

+ +
+ ); + } +} diff --git a/src/components/Authorized/Secured.tsx b/src/components/Authorized/Secured.tsx new file mode 100644 index 0000000..0bdbbe4 --- /dev/null +++ b/src/components/Authorized/Secured.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import CheckPermissions from './CheckPermissions'; + +/** + * 默认不能访问任何页面 + * default is "NULL" + */ +const Exception403 = () => 403; + +export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { + if (!component) return false; + const proto = Object.getPrototypeOf(component); + if (proto === React.Component || proto === Function.prototype) return true; + return isComponentClass(proto); +}; + +// Determine whether the incoming component has been instantiated +// AuthorizedRoute is already instantiated +// Authorized render is already instantiated, children is no instantiated +// Secured is not instantiated +const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { + if (isComponentClass(target)) { + const Target = target as React.ComponentClass; + return (props: any) => ; + } + if (React.isValidElement(target)) { + return (props: any) => React.cloneElement(target, props); + } + return () => target; +}; + +/** + * 用于判断是否拥有权限访问此 view 权限 + * authority 支持传入 string, () => boolean | Promise + * e.g. 'user' 只有 user 用户能访问 + * e.g. 'user,admin' user 和 admin 都能访问 + * e.g. ()=>boolean 返回true能访问,返回false不能访问 + * e.g. Promise then 能访问 catch不能访问 + * e.g. authority support incoming string, () => boolean | Promise + * e.g. 'user' only user user can access + * e.g. 'user, admin' user and admin can access + * e.g. () => boolean true to be able to visit, return false can not be accessed + * e.g. Promise then can not access the visit to catch + * @param {string | function | Promise} authority + * @param {ReactNode} error 非必需参数 + */ +const authorize = (authority: string, error?: React.ReactNode) => { + /** + * conversion into a class + * 防止传入字符串时找不到staticContext造成报错 + * String parameters can cause staticContext not found error + */ + let classError: boolean | React.FunctionComponent = false; + if (error) { + classError = (() => error) as React.FunctionComponent; + } + if (!authority) { + throw new Error('authority is required'); + } + return function decideAuthority(target: React.ComponentClass | React.ReactNode) { + const component = CheckPermissions(authority, target, classError || Exception403); + return checkIsInstantiation(component); + }; +}; + +export default authorize; diff --git a/src/components/Authorized/index.tsx b/src/components/Authorized/index.tsx new file mode 100644 index 0000000..9bed796 --- /dev/null +++ b/src/components/Authorized/index.tsx @@ -0,0 +1,13 @@ +import Authorized from './Authorized'; +import AuthorizedRoute from './AuthorizedRoute'; +import Secured from './Secured'; +import check from './CheckPermissions'; +import renderAuthorize from './renderAuthorize'; + +Authorized.Secured = Secured; +Authorized.AuthorizedRoute = AuthorizedRoute; +Authorized.check = check; + +const RenderAuthorize = renderAuthorize(Authorized); + +export default RenderAuthorize; diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts new file mode 100644 index 0000000..df00875 --- /dev/null +++ b/src/components/Authorized/renderAuthorize.ts @@ -0,0 +1,30 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable import/no-mutable-exports */ +let CURRENT: string | string[] = 'NULL'; + +type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); +/** + * use authority or getAuthority + * @param {string|()=>String} currentAuthority + */ +const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( + currentAuthority: CurrentAuthorityType, +): T => { + if (currentAuthority) { + if (typeof currentAuthority === 'function') { + CURRENT = currentAuthority(); + } + if ( + Object.prototype.toString.call(currentAuthority) === '[object String]' || + Array.isArray(currentAuthority) + ) { + CURRENT = currentAuthority as string[]; + } + } else { + CURRENT = 'NULL'; + } + return Authorized; +}; + +export { CURRENT }; +export default (Authorized: T) => renderAuthorize(Authorized); diff --git a/src/components/CopyBlock/index.less b/src/components/CopyBlock/index.less new file mode 100644 index 0000000..83d899a --- /dev/null +++ b/src/components/CopyBlock/index.less @@ -0,0 +1,29 @@ +.copy-block { + position: fixed; + right: 80px; + bottom: 40px; + z-index: 99; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + font-size: 20px; + background: #fff; + border-radius: 40px; + box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12); + cursor: pointer; +} + +.copy-block-view { + position: relative; + .copy-block-code { + display: inline-block; + margin: 0 0.2em; + padding: 0.2em 0.4em 0.1em; + font-size: 85%; + border-radius: 3px; + } +} diff --git a/src/components/CopyBlock/index.tsx b/src/components/CopyBlock/index.tsx new file mode 100644 index 0000000..ccdda63 --- /dev/null +++ b/src/components/CopyBlock/index.tsx @@ -0,0 +1,80 @@ +import { Icon, Popover, Typography } from 'antd'; +import React, { useRef } from 'react'; + +import { FormattedMessage } from 'umi-plugin-react/locale'; +import { connect } from 'dva'; +import { isAntDesignPro } from '@/utils/utils'; +import styles from './index.less'; + +const firstUpperCase = (pathString: string): string => + pathString + .replace('.', '') + .split(/\/|-/) + .map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) + .filter((s): boolean => !!s) + .join(''); + +// when click block copy, send block url to ga +const onBlockCopy = (label: string) => { + if (!isAntDesignPro()) { + return; + } + + const ga = window && window.ga; + if (ga) { + ga('send', 'event', { + eventCategory: 'block', + eventAction: 'copy', + eventLabel: label, + }); + } +}; + +const BlockCodeView: React.SFC<{ + url: string; +}> = ({ url }) => { + const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`; + return ( +
+ onBlockCopy(url), + }} + style={{ + display: 'flex', + }} + > +
+          {blockUrl}
+        
+
+
+ ); +}; + +interface RoutingType { + location: { + pathname: string; + }; +} + +export default connect(({ routing }: { routing: RoutingType }) => ({ + location: routing.location, +}))(({ location }: RoutingType) => { + const url = location.pathname; + const divDom = useRef(null); + return ( + } + placement="topLeft" + content={} + trigger="click" + getPopupContainer={dom => (divDom.current ? divDom.current : dom)} + > +
+ +
+
+ ); +}); diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx new file mode 100644 index 0000000..9a7f021 --- /dev/null +++ b/src/components/GlobalHeader/AvatarDropdown.tsx @@ -0,0 +1,75 @@ +import { Avatar, Icon, Menu, Spin } from 'antd'; +import { ClickParam } from 'antd/es/menu'; +import { FormattedMessage } from 'umi-plugin-react/locale'; +import React from 'react'; +import { connect } from 'dva'; +import router from 'umi/router'; + +import { ConnectProps, ConnectState } from '@/models/connect'; +import { CurrentUser } from '@/models/user'; +import HeaderDropdown from '../HeaderDropdown'; +import styles from './index.less'; + +export interface GlobalHeaderRightProps extends ConnectProps { + currentUser?: CurrentUser; + menu?: boolean; +} + +class AvatarDropdown extends React.Component { + onMenuClick = (event: ClickParam) => { + const { key } = event; + + if (key === 'logout') { + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'login/logout', + }); + } + + return; + } + router.push(`/account/${key}`); + }; + + render(): React.ReactNode { + const { currentUser = { avatar: '', name: '' }, menu } = this.props; + + const menuHeaderDropdown = ( + + {menu && ( + + + + + )} + {menu && ( + + + + + )} + {menu && } + + + + + + + ); + + return currentUser && currentUser.name ? ( + + + + {currentUser.name} + + + ) : ( + + ); + } +} +export default connect(({ user }: ConnectState) => ({ + currentUser: user.currentUser, +}))(AvatarDropdown); diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx new file mode 100644 index 0000000..0f01906 --- /dev/null +++ b/src/components/GlobalHeader/NoticeIconView.tsx @@ -0,0 +1,154 @@ +import React, { Component } from 'react'; +import { Tag, message } from 'antd'; +import { connect } from 'dva'; +import { formatMessage } from 'umi-plugin-react/locale'; +import groupBy from 'lodash/groupBy'; +import moment from 'moment'; + +import { NoticeItem } from '@/models/global'; +import NoticeIcon from '../NoticeIcon'; +import { CurrentUser } from '@/models/user'; +import { ConnectProps, ConnectState } from '@/models/connect'; +import styles from './index.less'; + +export interface GlobalHeaderRightProps extends ConnectProps { + notices?: NoticeItem[]; + currentUser?: CurrentUser; + fetchingNotices?: boolean; + onNoticeVisibleChange?: (visible: boolean) => void; + onNoticeClear?: (tabName?: string) => void; +} + +class GlobalHeaderRight extends Component { + componentDidMount() { + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'global/fetchNotices', + }); + } + } + + changeReadState = (clickedItem: NoticeItem): void => { + const { id } = clickedItem; + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'global/changeNoticeReadState', + payload: id, + }); + } + }; + + handleNoticeClear = (title: string, key: string) => { + const { dispatch } = this.props; + message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); + if (dispatch) { + dispatch({ + type: 'global/clearNotices', + payload: key, + }); + } + }; + + getNoticeData = (): { [key: string]: NoticeItem[] } => { + const { notices = [] } = this.props; + if (notices.length === 0) { + return {}; + } + const newNotices = notices.map(notice => { + const newNotice = { ...notice }; + if (newNotice.datetime) { + newNotice.datetime = moment(notice.datetime as string).fromNow(); + } + if (newNotice.id) { + newNotice.key = newNotice.id; + } + if (newNotice.extra && newNotice.status) { + const color = { + todo: '', + processing: 'blue', + urgent: 'red', + doing: 'gold', + }[newNotice.status]; + newNotice.extra = ( + + {newNotice.extra} + + ); + } + return newNotice; + }); + return groupBy(newNotices, 'type'); + }; + + getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => { + const unreadMsg: { [key: string]: number } = {}; + Object.keys(noticeData).forEach(key => { + const value = noticeData[key]; + if (!unreadMsg[key]) { + unreadMsg[key] = 0; + } + if (Array.isArray(value)) { + unreadMsg[key] = value.filter(item => !item.read).length; + } + }); + return unreadMsg; + }; + + render() { + const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; + const noticeData = this.getNoticeData(); + const unreadMsg = this.getUnreadData(noticeData); + + return ( + { + this.changeReadState(item as NoticeItem); + }} + loading={fetchingNotices} + clearText={formatMessage({ id: 'component.noticeIcon.clear' })} + viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })} + onClear={this.handleNoticeClear} + onPopupVisibleChange={onNoticeVisibleChange} + onViewMore={() => message.info('Click on view more')} + clearClose + > + + + + + ); + } +} + +export default connect(({ user, global, loading }: ConnectState) => ({ + currentUser: user.currentUser, + collapsed: global.collapsed, + fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], + fetchingNotices: loading.effects['global/fetchNotices'], + notices: global.notices, +}))(GlobalHeaderRight); diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx new file mode 100644 index 0000000..3516fc5 --- /dev/null +++ b/src/components/GlobalHeader/RightContent.tsx @@ -0,0 +1,74 @@ +import { Icon, Tooltip } from 'antd'; +import React from 'react'; +import { connect } from 'dva'; +import { formatMessage } from 'umi-plugin-react/locale'; +import { ConnectProps, ConnectState } from '@/models/connect'; + +import Avatar from './AvatarDropdown'; +import HeaderSearch from '../HeaderSearch'; +import SelectLang from '../SelectLang'; +import styles from './index.less'; + +export type SiderTheme = 'light' | 'dark'; +export interface GlobalHeaderRightProps extends ConnectProps { + theme?: SiderTheme; + layout: 'sidemenu' | 'topmenu'; +} + +const GlobalHeaderRight: React.SFC = props => { + const { theme, layout } = props; + let className = styles.right; + + if (theme === 'dark' && layout === 'topmenu') { + className = `${styles.right} ${styles.dark}`; + } + + return ( +
+ { + console.log('input', value); + }} + onPressEnter={value => { + console.log('enter', value); + }} + /> + + + + + + + +
+ ); +}; + +export default connect(({ settings }: ConnectState) => ({ + theme: settings.navTheme, + layout: settings.layout, +}))(GlobalHeaderRight); diff --git a/src/components/GlobalHeader/index.less b/src/components/GlobalHeader/index.less new file mode 100644 index 0000000..590a14f --- /dev/null +++ b/src/components/GlobalHeader/index.less @@ -0,0 +1,134 @@ +@import '~antd/es/style/themes/default.less'; + +@pro-header-hover-bg: rgba(0, 0, 0, 0.025); + +.logo { + display: inline-block; + height: @layout-header-height; + padding: 0 0 0 24px; + font-size: 20px; + line-height: @layout-header-height; + vertical-align: top; + cursor: pointer; + img { + display: inline-block; + vertical-align: middle; + } +} + +.menu { + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + min-width: 160px; + } +} + +.trigger { + height: @layout-header-height; + padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px; + font-size: 20px; + cursor: pointer; + transition: all 0.3s, padding 0s; + &:hover { + background: @pro-header-hover-bg; + } +} + +.right { + float: right; + height: 100%; + margin-left: auto; + overflow: hidden; + .action { + display: inline-block; + height: 100%; + padding: 0 12px; + cursor: pointer; + transition: all 0.3s; + > i { + color: @text-color; + vertical-align: middle; + } + &:hover { + background: @pro-header-hover-bg; + } + &:global(.opened) { + background: @pro-header-hover-bg; + } + } + .search { + padding: 0 12px; + &:hover { + background: transparent; + } + } + .account { + .avatar { + margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; + margin-right: 8px; + color: @primary-color; + vertical-align: top; + background: rgba(255, 255, 255, 0.85); + } + } +} + +.dark { + height: @layout-header-height; + .action { + color: rgba(255, 255, 255, 0.85); + > i { + color: rgba(255, 255, 255, 0.85); + } + &:hover, + &:global(.opened) { + background: @primary-color; + } + } +} + +:global(.ant-pro-global-header) { + .dark { + .action { + color: @text-color; + > i { + color: @text-color; + } + &:hover { + color: rgba(255, 255, 255, 0.85); + > i { + color: rgba(255, 255, 255, 0.85); + } + } + } + } +} + +@media only screen and (max-width: @screen-md) { + :global(.ant-divider-vertical) { + vertical-align: unset; + } + .name { + display: none; + } + i.trigger { + padding: 22px 12px; + } + .logo { + position: relative; + padding-right: 12px; + padding-left: 12px; + } + .right { + position: absolute; + top: 0; + right: 12px; + .account { + .avatar { + margin-right: 0; + } + } + } +} diff --git a/src/components/HeaderDropdown/index.less b/src/components/HeaderDropdown/index.less new file mode 100644 index 0000000..c2b9858 --- /dev/null +++ b/src/components/HeaderDropdown/index.less @@ -0,0 +1,16 @@ +@import '~antd/es/style/themes/default.less'; + +.container > * { + background-color: #fff; + border-radius: 4px; + box-shadow: @shadow-1-down; +} + +@media screen and (max-width: @screen-xs) { + .container { + width: 100% !important; + } + .container > * { + border-radius: 0 !important; + } +} diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx new file mode 100644 index 0000000..f668a2b --- /dev/null +++ b/src/components/HeaderDropdown/index.tsx @@ -0,0 +1,19 @@ +import { DropDownProps } from 'antd/es/dropdown'; +import { Dropdown } from 'antd'; +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +declare type OverlayFunc = () => React.ReactNode; + +export interface HeaderDropdownProps extends DropDownProps { + overlayClassName?: string; + overlay: React.ReactNode | OverlayFunc; + placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; +} + +const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( + +); + +export default HeaderDropdown; diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less new file mode 100644 index 0000000..8f40cc7 --- /dev/null +++ b/src/components/HeaderSearch/index.less @@ -0,0 +1,32 @@ +@import '~antd/es/style/themes/default.less'; + +.headerSearch { + :global(.anticon-search) { + font-size: 16px; + cursor: pointer; + } + .input { + width: 0; + background: transparent; + border-radius: 0; + transition: width 0.3s, margin-left 0.3s; + :global(.ant-select-selection) { + background: transparent; + } + input { + padding-right: 0; + padding-left: 0; + border: 0; + box-shadow: none !important; + } + &, + &:hover, + &:focus { + border-bottom: 1px solid @border-color-base; + } + &.show { + width: 210px; + margin-left: 8px; + } + } +} diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx new file mode 100644 index 0000000..9e05b6a --- /dev/null +++ b/src/components/HeaderSearch/index.tsx @@ -0,0 +1,146 @@ +import { AutoComplete, Icon, Input } from 'antd'; +import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete'; +import React, { Component } from 'react'; + +import classNames from 'classnames'; +import debounce from 'lodash/debounce'; +import styles from './index.less'; + +export interface HeaderSearchProps { + onPressEnter: (value: string) => void; + onSearch: (value: string) => void; + onChange: (value: string) => void; + onVisibleChange: (b: boolean) => void; + className: string; + placeholder: string; + defaultActiveFirstOption: boolean; + dataSource: DataSourceItemType[]; + defaultOpen: boolean; + open?: boolean; +} + +interface HeaderSearchState { + value: string; + searchMode: boolean; +} + +export default class HeaderSearch extends Component { + static defaultProps = { + defaultActiveFirstOption: false, + onPressEnter: () => {}, + onSearch: () => {}, + onChange: () => {}, + className: '', + placeholder: '', + dataSource: [], + defaultOpen: false, + onVisibleChange: () => {}, + }; + + static getDerivedStateFromProps(props: HeaderSearchProps) { + if ('open' in props) { + return { + searchMode: props.open, + }; + } + return null; + } + + private inputRef: Input | null = null; + + constructor(props: HeaderSearchProps) { + super(props); + this.state = { + searchMode: props.defaultOpen, + value: '', + }; + this.debouncePressEnter = debounce(this.debouncePressEnter, 500, { + leading: true, + trailing: false, + }); + } + + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + this.debouncePressEnter(); + } + }; + + onChange: AutoCompleteProps['onChange'] = value => { + if (typeof value === 'string') { + const { onSearch, onChange } = this.props; + this.setState({ value }); + if (onSearch) { + onSearch(value); + } + if (onChange) { + onChange(value); + } + } + }; + + enterSearchMode = () => { + const { onVisibleChange } = this.props; + onVisibleChange(true); + this.setState({ searchMode: true }, () => { + const { searchMode } = this.state; + if (searchMode && this.inputRef) { + this.inputRef.focus(); + } + }); + }; + + leaveSearchMode = () => { + this.setState({ + searchMode: false, + value: '', + }); + }; + + debouncePressEnter = () => { + const { onPressEnter } = this.props; + const { value } = this.state; + onPressEnter(value); + }; + + render() { + const { className, placeholder, open, ...restProps } = this.props; + const { searchMode, value } = this.state; + delete restProps.defaultOpen; // for rc-select not affected + const inputClass = classNames(styles.input, { + [styles.show]: searchMode, + }); + + return ( + { + if (propertyName === 'width' && !searchMode) { + const { onVisibleChange } = this.props; + onVisibleChange(searchMode); + } + }} + > + + + { + this.inputRef = node; + }} + aria-label={placeholder} + placeholder={placeholder} + onKeyDown={this.onKeyDown} + onBlur={this.leaveSearchMode} + /> + + + ); + } +} diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less new file mode 100644 index 0000000..ce07d71 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.less @@ -0,0 +1,105 @@ +@import '~antd/es/style/themes/default.less'; + +.list { + max-height: 400px; + overflow: auto; + &::-webkit-scrollbar { + display: none; + } + .item { + padding-right: 24px; + padding-left: 24px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s; + + .meta { + width: 100%; + } + + .avatar { + margin-top: 4px; + background: #fff; + } + .iconElement { + font-size: 32px; + } + + &.read { + opacity: 0.4; + } + &:last-child { + border-bottom: 0; + } + &:hover { + background: @primary-1; + } + .title { + margin-bottom: 8px; + font-weight: normal; + } + .description { + font-size: 12px; + line-height: @line-height-base; + } + .datetime { + margin-top: 4px; + font-size: 12px; + line-height: @line-height-base; + } + .extra { + float: right; + margin-top: -1.5px; + margin-right: 0; + color: @text-color-secondary; + font-weight: normal; + } + } + .loadMore { + padding: 8px 0; + color: @primary-6; + text-align: center; + cursor: pointer; + &.loadedAll { + color: rgba(0, 0, 0, 0.25); + cursor: unset; + } + } +} + +.notFound { + padding: 73px 0 88px; + color: @text-color-secondary; + text-align: center; + img { + display: inline-block; + height: 76px; + margin-bottom: 16px; + } +} + +.bottomBar { + height: 46px; + color: @text-color; + line-height: 46px; + text-align: center; + border-top: 1px solid @border-color-split; + border-radius: 0 0 @border-radius-base @border-radius-base; + transition: all 0.3s; + div { + display: inline-block; + width: 50%; + cursor: pointer; + transition: all 0.3s; + user-select: none; + &:hover { + color: @heading-color; + } + &:only-child { + width: 100%; + } + &:not(:only-child):last-child { + border-left: 1px solid @border-color-split; + } + } +} diff --git a/src/components/NoticeIcon/NoticeList.tsx b/src/components/NoticeIcon/NoticeList.tsx new file mode 100644 index 0000000..f056b5b --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.tsx @@ -0,0 +1,114 @@ +import { Avatar, List } from 'antd'; + +import React from 'react'; +import classNames from 'classnames'; +import { NoticeIconData } from './index'; +import styles from './NoticeList.less'; + +export interface NoticeIconTabProps { + count?: number; + name?: string; + showClear?: boolean; + showViewMore?: boolean; + style?: React.CSSProperties; + title: string; + tabKey: string; + data?: NoticeIconData[]; + onClick?: (item: NoticeIconData) => void; + onClear?: () => void; + emptyText?: string; + clearText?: string; + viewMoreText?: string; + list: NoticeIconData[]; + onViewMore?: (e: any) => void; +} +const NoticeList: React.SFC = ({ + data = [], + onClick, + onClear, + title, + onViewMore, + emptyText, + showClear = true, + clearText, + viewMoreText, + showViewMore = false, +}) => { + if (data.length === 0) { + return ( +
+ not found +
{emptyText}
+
+ ); + } + return ( +
+ + className={styles.list} + dataSource={data} + renderItem={(item, i) => { + const itemCls = classNames(styles.item, { + [styles.read]: item.read, + }); + // eslint-disable-next-line no-nested-ternary + const leftIcon = item.avatar ? ( + typeof item.avatar === 'string' ? ( + + ) : ( + {item.avatar} + ) + ) : null; + + return ( + onClick && onClick(item)} + > + + {item.title} +
{item.extra}
+
+ } + description={ +
+
{item.description}
+
{item.datetime}
+
+ } + /> + + ); + }} + /> +
+ {showClear ? ( +
+ {clearText} {title} +
+ ) : null} + {showViewMore ? ( +
{ + if (onViewMore) { + onViewMore(e); + } + }} + > + {viewMoreText} +
+ ) : null} +
+ + ); +}; + +export default NoticeList; diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less new file mode 100644 index 0000000..650ccd2 --- /dev/null +++ b/src/components/NoticeIcon/index.less @@ -0,0 +1,31 @@ +@import '~antd/es/style/themes/default.less'; + +.popover { + position: relative; + width: 336px; +} + +.noticeButton { + display: inline-block; + cursor: pointer; + transition: all 0.3s; +} +.icon { + padding: 4px; + vertical-align: middle; +} + +.badge { + font-size: 16px; +} + +.tabs { + :global { + .ant-tabs-nav-scroll { + text-align: center; + } + .ant-tabs-bar { + margin-bottom: 0; + } + } +} diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx new file mode 100644 index 0000000..bb3caa4 --- /dev/null +++ b/src/components/NoticeIcon/index.tsx @@ -0,0 +1,175 @@ +import { Badge, Icon, Spin, Tabs } from 'antd'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import NoticeList, { NoticeIconTabProps } from './NoticeList'; + +import HeaderDropdown from '../HeaderDropdown'; +import styles from './index.less'; + +const { TabPane } = Tabs; + +export interface NoticeIconData { + avatar?: string | React.ReactNode; + title?: React.ReactNode; + description?: React.ReactNode; + datetime?: React.ReactNode; + extra?: React.ReactNode; + style?: React.CSSProperties; + key?: string | number; + read?: boolean; +} + +export interface NoticeIconProps { + count?: number; + bell?: React.ReactNode; + className?: string; + loading?: boolean; + onClear?: (tabName: string, tabKey: string) => void; + onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void; + onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void; + onTabChange?: (tabTile: string) => void; + style?: React.CSSProperties; + onPopupVisibleChange?: (visible: boolean) => void; + popupVisible?: boolean; + clearText?: string; + viewMoreText?: string; + clearClose?: boolean; + children: React.ReactElement[]; +} + +export default class NoticeIcon extends Component { + public static Tab: typeof NoticeList = NoticeList; + + static defaultProps = { + onItemClick: (): void => {}, + onPopupVisibleChange: (): void => {}, + onTabChange: (): void => {}, + onClear: (): void => {}, + onViewMore: (): void => {}, + loading: false, + clearClose: false, + emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', + }; + + state = { + visible: false, + }; + + onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => { + const { onItemClick } = this.props; + if (onItemClick) { + onItemClick(item, tabProps); + } + }; + + onClear = (name: string, key: string): void => { + const { onClear } = this.props; + if (onClear) { + onClear(name, key); + } + }; + + onTabChange = (tabType: string): void => { + const { onTabChange } = this.props; + if (onTabChange) { + onTabChange(tabType); + } + }; + + onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => { + const { onViewMore } = this.props; + if (onViewMore) { + onViewMore(tabProps, event); + } + }; + + getNotificationBox(): React.ReactNode { + const { children, loading, clearText, viewMoreText } = this.props; + if (!children) { + return null; + } + const panes = React.Children.map( + children, + (child: React.ReactElement): React.ReactNode => { + if (!child) { + return null; + } + const { list, title, count, tabKey, showClear, showViewMore } = child.props; + const len = list && list.length ? list.length : 0; + const msgCount = count || count === 0 ? count : len; + const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; + return ( + + this.onClear(title, tabKey)} + onClick={(item): void => this.onItemClick(item, child.props)} + onViewMore={(event): void => this.onViewMore(child.props, event)} + showClear={showClear} + showViewMore={showViewMore} + title={title} + {...child.props} + /> + + ); + }, + ); + return ( + <> + + + {panes} + + + + ); + } + + handleVisibleChange = (visible: boolean): void => { + const { onPopupVisibleChange } = this.props; + this.setState({ visible }); + if (onPopupVisibleChange) { + onPopupVisibleChange(visible); + } + }; + + render(): React.ReactNode { + const { className, count, popupVisible, bell } = this.props; + const { visible } = this.state; + const noticeButtonClass = classNames(className, styles.noticeButton); + const notificationBox = this.getNotificationBox(); + const NoticeBellIcon = bell || ; + const trigger = ( + + + {NoticeBellIcon} + + + ); + if (!notificationBox) { + return trigger; + } + const popoverProps: { + visible?: boolean; + } = {}; + if ('popupVisible' in this.props) { + popoverProps.visible = popupVisible; + } + + return ( + + {trigger} + + ); + } +} diff --git a/src/components/PageLoading/index.tsx b/src/components/PageLoading/index.tsx new file mode 100644 index 0000000..0fa952c --- /dev/null +++ b/src/components/PageLoading/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Spin } from 'antd'; + +// loading components from code split +// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport +const PageLoading: React.FC = () => ( +
+ +
+); +export default PageLoading; diff --git a/src/components/SelectLang/index.less b/src/components/SelectLang/index.less new file mode 100644 index 0000000..7cb057e --- /dev/null +++ b/src/components/SelectLang/index.less @@ -0,0 +1,24 @@ +@import '~antd/es/style/themes/default.less'; + +.menu { + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + min-width: 160px; + } +} + +.dropDown { + line-height: @layout-header-height; + vertical-align: top; + cursor: pointer; + > i { + font-size: 16px !important; + transform: none !important; + svg { + position: relative; + top: -1px; + } + } +} diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx new file mode 100644 index 0000000..dc168a5 --- /dev/null +++ b/src/components/SelectLang/index.tsx @@ -0,0 +1,51 @@ +import { Icon, Menu } from 'antd'; +import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale'; + +import { ClickParam } from 'antd/es/menu'; +import React from 'react'; +import classNames from 'classnames'; +import HeaderDropdown from '../HeaderDropdown'; +import styles from './index.less'; + +interface SelectLangProps { + className?: string; +} +const SelectLang: React.FC = props => { + const { className } = props; + const selectedLang = getLocale(); + const changeLang = ({ key }: ClickParam): void => setLocale(key, false); + const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; + const languageLabels = { + 'zh-CN': '简体中文', + 'zh-TW': '繁体中文', + 'en-US': 'English', + 'pt-BR': 'Português', + }; + const languageIcons = { + 'zh-CN': '🇨🇳', + 'zh-TW': '🇭🇰', + 'en-US': '🇺🇸', + 'pt-BR': '🇧🇷', + }; + const langMenu = ( + + {locales.map(locale => ( + + + {languageIcons[locale]} + {' '} + {languageLabels[locale]} + + ))} + + ); + return ( + + + + + + ); +}; + +export default SelectLang; diff --git a/src/components/SettingDrawer/themeColorClient.ts b/src/components/SettingDrawer/themeColorClient.ts new file mode 100644 index 0000000..19bcb05 --- /dev/null +++ b/src/components/SettingDrawer/themeColorClient.ts @@ -0,0 +1,31 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable import/no-extraneous-dependencies */ +import client from 'webpack-theme-color-replacer/client'; +import generate from '@ant-design/colors/lib/generate'; + +export default { + getAntdSerials(color: string): string[] { + const lightCount = 9; + const divide = 10; + // 淡化(即less的tint) + let lightens = new Array(lightCount).fill(0); + lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide)); + const colorPalettes = generate(color); + const rgb = client.varyColor.toNum3(color.replace('#', '')).join(','); + return lightens.concat(colorPalettes).concat(rgb); + }, + changeColor(color?: string): Promise { + if (!color) { + return Promise.resolve(); + } + const options = { + // new colors array, one-to-one corresponde with `matchColors` + newColors: this.getAntdSerials(color), + changeUrl(cssUrl: string): string { + // while router is not `hash` mode, it needs absolute path + return `/${cssUrl}`; + }, + }; + return client.changer.changeColor(options, Promise); + }, +}; diff --git a/src/e2e/__mocks__/antd-pro-merge-less.js b/src/e2e/__mocks__/antd-pro-merge-less.js new file mode 100644 index 0000000..f237ddf --- /dev/null +++ b/src/e2e/__mocks__/antd-pro-merge-less.js @@ -0,0 +1 @@ +export default undefined; diff --git a/src/e2e/baseLayout.e2e.js b/src/e2e/baseLayout.e2e.js new file mode 100644 index 0000000..78c500c --- /dev/null +++ b/src/e2e/baseLayout.e2e.js @@ -0,0 +1,39 @@ +const RouterConfig = require('../../config/config').default.routes; +const { uniq } = require('lodash'); + +const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; + +function formatter(routes, parentPath = '') { + const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); + let result = []; + routes.forEach(item => { + if (item.path) { + result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); + } + if (item.routes) { + result = result.concat( + formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), + ); + } + }); + return uniq(result.filter(item => !!item)); +} + +describe('Ant Design Pro E2E test', () => { + const testPage = path => async () => { + await page.goto(`${BASE_URL}${path}`); + await page.waitForSelector('footer', { + timeout: 2000, + }); + const haveFooter = await page.evaluate( + () => document.getElementsByTagName('footer').length > 0, + ); + expect(haveFooter).toBeTruthy(); + }; + + const routers = formatter(RouterConfig); + console.log('routers', routers); + routers.forEach(route => { + it(`test pages ${route}`, testPage(route)); + }); +}); diff --git a/src/e2e/topMenu.e2e.js b/src/e2e/topMenu.e2e.js new file mode 100644 index 0000000..7c5f855 --- /dev/null +++ b/src/e2e/topMenu.e2e.js @@ -0,0 +1,15 @@ +const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; + +describe('Homepage', () => { + it('topmenu should have footer', async () => { + const params = '/form/basic-form?navTheme=light&layout=topmenu'; + await page.goto(`${BASE_URL}${params}`); + await page.waitForSelector('footer', { + timeout: 2000, + }); + const haveFooter = await page.evaluate( + () => document.getElementsByTagName('footer').length > 0, + ); + expect(haveFooter).toBeTruthy(); + }); +}); diff --git a/src/global.less b/src/global.less new file mode 100644 index 0000000..b4237f7 --- /dev/null +++ b/src/global.less @@ -0,0 +1,47 @@ +@import '~antd/es/style/themes/default.less'; + +html, +body, +#root { + height: 100%; +} + +.colorWeak { + filter: invert(80%); +} + +.ant-layout { + min-height: 100vh; +} + +canvas { + display: block; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +ul, +ol { + list-style: none; +} + +@media (max-width: @screen-xs) { + .ant-table { + width: 100%; + overflow-x: auto; + &-thead > tr, + &-tbody > tr { + > th, + > td { + white-space: pre; + > span { + display: block; + } + } + } + } +} diff --git a/src/global.tsx b/src/global.tsx new file mode 100644 index 0000000..626399f --- /dev/null +++ b/src/global.tsx @@ -0,0 +1,83 @@ +import { Button, message, notification } from 'antd'; + +import React from 'react'; +import { formatMessage } from 'umi-plugin-react/locale'; +import defaultSettings from '../config/defaultSettings'; + +const { pwa } = defaultSettings; +// if pwa is true +if (pwa) { + // Notify user if offline now + window.addEventListener('sw.offline', () => { + message.warning(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]); + }); + // Refresh current page to use the updated HTML and other assets after SW has skiped waiting + window.location.reload(true); + return true; + }; + const key = `open${Date.now()}`; + const btn = ( + + ); + notification.open({ + message: formatMessage({ id: 'app.pwa.serviceworker.updated' }), + description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }), + btn, + key, + onClose: async () => {}, + }); + }); +} else if ('serviceWorker' in navigator) { + // 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(); + }); + + // remove all caches + if (window.caches && window.caches.keys) { + caches.keys().then(keys => { + keys.forEach(key => { + caches.delete(key); + }); + }); + } +} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx new file mode 100644 index 0000000..93f9e9e --- /dev/null +++ b/src/layouts/BasicLayout.tsx @@ -0,0 +1,146 @@ +/** + * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout. + * You can view component api by: + * https://github.com/ant-design/ant-design-pro-layout + */ + +import ProLayout, { + MenuDataItem, + BasicLayoutProps as ProLayoutProps, + Settings, +} from '@ant-design/pro-layout'; +import React, { useEffect } from 'react'; +import Link from 'umi/link'; +import { Dispatch } from 'redux'; +import { connect } from 'dva'; +import { formatMessage } from 'umi-plugin-react/locale'; + +import Authorized from '@/utils/Authorized'; +import RightContent from '@/components/GlobalHeader/RightContent'; +import { ConnectState } from '@/models/connect'; +import { isAntDesignPro } from '@/utils/utils'; +import logo from '../assets/logo.svg'; + +export interface BasicLayoutProps extends ProLayoutProps { + breadcrumbNameMap: { + [path: string]: MenuDataItem; + }; + settings: Settings; + dispatch: Dispatch; +} +export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & { + breadcrumbNameMap: { + [path: string]: MenuDataItem; + }; +}; + +/** + * use Authorized check all menu item + */ +const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => + menuList.map(item => { + const localItem = { + ...item, + children: item.children ? menuDataRender(item.children) : [], + }; + return Authorized.check(item.authority, localItem, null) as MenuDataItem; + }); + +const footerRender: BasicLayoutProps['footerRender'] = (_, defaultDom) => { + if (!isAntDesignPro()) { + return defaultDom; + } + return ( + <> + {defaultDom} +
+ + netlify logo + +
+ + ); +}; + +const BasicLayout: React.FC = props => { + const { dispatch, children, settings } = props; + /** + * constructor + */ + + useEffect(() => { + if (dispatch) { + dispatch({ + type: 'user/fetchCurrent', + }); + dispatch({ + type: 'settings/getSetting', + }); + } + }, []); + + /** + * init variables + */ + const handleMenuCollapse = (payload: boolean): void => { + if (dispatch) { + dispatch({ + type: 'global/changeLayoutCollapsed', + payload, + }); + } + }; + + return ( + { + if (menuItemProps.isUrl) { + return defaultDom; + } + return {defaultDom}; + }} + breadcrumbRender={(routers = []) => [ + { + path: '/', + breadcrumbName: formatMessage({ + id: 'menu.home', + defaultMessage: 'Home', + }), + }, + ...routers, + ]} + itemRender={(route, params, routes, paths) => { + const first = routes.indexOf(route) === 0; + return first ? ( + {route.breadcrumbName} + ) : ( + {route.breadcrumbName} + ); + }} + footerRender={footerRender} + menuDataRender={menuDataRender} + formatMessage={formatMessage} + rightContentRender={rightProps => } + {...props} + {...settings} + > + {children} + + ); +}; + +export default connect(({ global, settings }: ConnectState) => ({ + collapsed: global.collapsed, + settings, +}))(BasicLayout); diff --git a/src/layouts/BlankLayout.tsx b/src/layouts/BlankLayout.tsx new file mode 100644 index 0000000..a5ff8c4 --- /dev/null +++ b/src/layouts/BlankLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const Layout: React.FC = ({ children }) =>
{children}
; + +export default Layout; diff --git a/src/layouts/SecurityLayout.tsx b/src/layouts/SecurityLayout.tsx new file mode 100644 index 0000000..b485b8c --- /dev/null +++ b/src/layouts/SecurityLayout.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { connect } from 'dva'; +import { Redirect } from 'umi'; +import { stringify } from 'querystring'; +import { ConnectState, ConnectProps } from '@/models/connect'; +import { CurrentUser } from '@/models/user'; +import PageLoading from '@/components/PageLoading'; + +interface SecurityLayoutProps extends ConnectProps { + loading: boolean; + currentUser: CurrentUser; +} + +interface SecurityLayoutState { + isReady: boolean; +} + +class SecurityLayout extends React.Component { + state: SecurityLayoutState = { + isReady: false, + }; + + componentDidMount() { + this.setState({ + isReady: true, + }); + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'user/fetchCurrent', + }); + } + } + + render() { + const { isReady } = this.state; + const { children, loading, currentUser } = this.props; + // You can replace it to your authentication rule (such as check token exists) + // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) + const isLogin = currentUser && currentUser.userid; + const queryString = stringify({ + redirect: window.location.href, + }); + + if ((!isLogin && loading) || !isReady) { + return ; + } + if (!isLogin) { + return ; + } + return children; + } +} + +export default connect(({ user, loading }: ConnectState) => ({ + currentUser: user.currentUser, + loading: loading.models.user, +}))(SecurityLayout); diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less new file mode 100644 index 0000000..cdc207e --- /dev/null +++ b/src/layouts/UserLayout.less @@ -0,0 +1,71 @@ +@import '~antd/es/style/themes/default.less'; + +.container { + display: flex; + flex-direction: column; + height: 100vh; + overflow: auto; + background: @layout-body-background; +} + +.lang { + width: 100%; + height: 40px; + line-height: 44px; + text-align: right; + :global(.ant-dropdown-trigger) { + margin-right: 24px; + } +} + +.content { + flex: 1; + padding: 32px 0; +} + +@media (min-width: @screen-md-min) { + .container { + background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); + background-repeat: no-repeat; + background-position: center 110px; + background-size: 100%; + } + + .content { + padding: 32px 0 24px; + } +} + +.top { + text-align: center; +} + +.header { + height: 44px; + line-height: 44px; + a { + text-decoration: none; + } +} + +.logo { + height: 44px; + margin-right: 16px; + vertical-align: top; +} + +.title { + position: relative; + top: 2px; + color: @heading-color; + font-weight: 600; + font-size: 33px; + font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; +} + +.desc { + margin-top: 12px; + margin-bottom: 40px; + color: @text-color-secondary; + font-size: @font-size-base; +} diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx new file mode 100644 index 0000000..6a792ff --- /dev/null +++ b/src/layouts/UserLayout.tsx @@ -0,0 +1,65 @@ +import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout'; +import DocumentTitle from 'react-document-title'; +import Link from 'umi/link'; +import React from 'react'; +import { connect } from 'dva'; +import { formatMessage } from 'umi-plugin-react/locale'; + +import SelectLang from '@/components/SelectLang'; +import { ConnectProps, ConnectState } from '@/models/connect'; +import logo from '../assets/logo.svg'; +import styles from './UserLayout.less'; + +export interface UserLayoutProps extends ConnectProps { + breadcrumbNameMap: { [path: string]: MenuDataItem }; +} + +const UserLayout: React.SFC = props => { + const { + route = { + routes: [], + }, + } = props; + const { routes = [] } = route; + const { + children, + location = { + pathname: '', + }, + } = props; + const { breadcrumb } = getMenuData(routes); + + return ( + +
+
+ +
+
+
+
+ + logo + Ant Design + +
+
Ant Design 是西湖区最具影响力的 Web 设计规范
+
+ {children} +
+ +
+
+ ); +}; + +export default connect(({ settings }: ConnectState) => ({ + ...settings, +}))(UserLayout); diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts new file mode 100644 index 0000000..3d57988 --- /dev/null +++ b/src/locales/en-US.ts @@ -0,0 +1,22 @@ +import component from './en-US/component'; +import globalHeader from './en-US/globalHeader'; +import menu from './en-US/menu'; +import pwa from './en-US/pwa'; +import settingDrawer from './en-US/settingDrawer'; +import settings from './en-US/settings'; + +export default { + 'navBar.lang': 'Languages', + 'layout.user.link.help': 'Help', + 'layout.user.link.privacy': 'Privacy', + 'layout.user.link.terms': 'Terms', + 'app.preview.down.block': 'Download this page to your local project', + 'app.welcome.link.fetch-blocks': 'Get all block', + 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', + ...globalHeader, + ...menu, + ...settingDrawer, + ...settings, + ...pwa, + ...component, +}; diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts new file mode 100644 index 0000000..3ba7eed --- /dev/null +++ b/src/locales/en-US/component.ts @@ -0,0 +1,5 @@ +export default { + 'component.tagSelect.expand': 'Expand', + 'component.tagSelect.collapse': 'Collapse', + 'component.tagSelect.all': 'All', +}; diff --git a/src/locales/en-US/globalHeader.ts b/src/locales/en-US/globalHeader.ts new file mode 100644 index 0000000..60b6d4e --- /dev/null +++ b/src/locales/en-US/globalHeader.ts @@ -0,0 +1,17 @@ +export default { + 'component.globalHeader.search': 'Search', + 'component.globalHeader.search.example1': 'Search example 1', + 'component.globalHeader.search.example2': 'Search example 2', + 'component.globalHeader.search.example3': 'Search example 3', + 'component.globalHeader.help': 'Help', + 'component.globalHeader.notification': 'Notification', + 'component.globalHeader.notification.empty': 'You have viewed all notifications.', + 'component.globalHeader.message': 'Message', + 'component.globalHeader.message.empty': 'You have viewed all messsages.', + 'component.globalHeader.event': 'Event', + 'component.globalHeader.event.empty': 'You have viewed all events.', + 'component.noticeIcon.clear': 'Clear', + 'component.noticeIcon.cleared': 'Cleared', + 'component.noticeIcon.empty': 'No notifications', + 'component.noticeIcon.view-more': 'View more', +}; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts new file mode 100644 index 0000000..8e0026f --- /dev/null +++ b/src/locales/en-US/menu.ts @@ -0,0 +1,50 @@ +export default { + 'menu.welcome': 'Welcome', + 'menu.more-blocks': 'More Blocks', + 'menu.home': 'Home', + 'menu.login': 'Login', + 'menu.register': 'Register', + 'menu.register.result': 'Register Result', + 'menu.dashboard': 'Dashboard', + 'menu.dashboard.analysis': 'Analysis', + 'menu.dashboard.monitor': 'Monitor', + 'menu.dashboard.workplace': 'Workplace', + 'menu.exception.403': '403', + 'menu.exception.404': '404', + 'menu.exception.500': '500', + 'menu.form': 'Form', + 'menu.form.basic-form': 'Basic Form', + 'menu.form.step-form': 'Step Form', + 'menu.form.step-form.info': 'Step Form(write transfer information)', + 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', + 'menu.form.step-form.result': 'Step Form(finished)', + 'menu.form.advanced-form': 'Advanced Form', + 'menu.list': 'List', + 'menu.list.table-list': 'Search Table', + 'menu.list.basic-list': 'Basic List', + 'menu.list.card-list': 'Card List', + 'menu.list.search-list': 'Search List', + 'menu.list.search-list.articles': 'Search List(articles)', + 'menu.list.search-list.projects': 'Search List(projects)', + 'menu.list.search-list.applications': 'Search List(applications)', + 'menu.profile': 'Profile', + 'menu.profile.basic': 'Basic Profile', + 'menu.profile.advanced': 'Advanced Profile', + 'menu.result': 'Result', + 'menu.result.success': 'Success', + 'menu.result.fail': 'Fail', + 'menu.exception': 'Exception', + 'menu.exception.not-permission': '403', + 'menu.exception.not-find': '404', + 'menu.exception.server-error': '500', + 'menu.exception.trigger': 'Trigger', + 'menu.account': 'Account', + 'menu.account.center': 'Account Center', + 'menu.account.settings': 'Account Settings', + 'menu.account.trigger': 'Trigger Error', + 'menu.account.logout': 'Logout', + 'menu.editor': 'Graphic Editor', + 'menu.editor.flow': 'Flow Editor', + 'menu.editor.mind': 'Mind Editor', + 'menu.editor.koni': 'Koni Editor', +}; diff --git a/src/locales/en-US/pwa.ts b/src/locales/en-US/pwa.ts new file mode 100644 index 0000000..ed8d199 --- /dev/null +++ b/src/locales/en-US/pwa.ts @@ -0,0 +1,6 @@ +export default { + 'app.pwa.offline': 'You are offline now', + 'app.pwa.serviceworker.updated': 'New content is available', + 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', + 'app.pwa.serviceworker.updated.ok': 'Refresh', +}; diff --git a/src/locales/en-US/settingDrawer.ts b/src/locales/en-US/settingDrawer.ts new file mode 100644 index 0000000..a644905 --- /dev/null +++ b/src/locales/en-US/settingDrawer.ts @@ -0,0 +1,31 @@ +export default { + 'app.setting.pagestyle': 'Page style setting', + 'app.setting.pagestyle.dark': 'Dark style', + 'app.setting.pagestyle.light': 'Light style', + 'app.setting.content-width': 'Content Width', + 'app.setting.content-width.fixed': 'Fixed', + 'app.setting.content-width.fluid': 'Fluid', + 'app.setting.themecolor': 'Theme Color', + 'app.setting.themecolor.dust': 'Dust Red', + 'app.setting.themecolor.volcano': 'Volcano', + 'app.setting.themecolor.sunset': 'Sunset Orange', + 'app.setting.themecolor.cyan': 'Cyan', + 'app.setting.themecolor.green': 'Polar Green', + 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', + 'app.setting.themecolor.geekblue': 'Geek Glue', + 'app.setting.themecolor.purple': 'Golden Purple', + 'app.setting.navigationmode': 'Navigation Mode', + 'app.setting.sidemenu': 'Side Menu Layout', + 'app.setting.topmenu': 'Top Menu Layout', + 'app.setting.fixedheader': 'Fixed Header', + 'app.setting.fixedsidebar': 'Fixed Sidebar', + 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', + 'app.setting.hideheader': 'Hidden Header when scrolling', + 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', + 'app.setting.othersettings': 'Other Settings', + 'app.setting.weakmode': 'Weak Mode', + 'app.setting.copy': 'Copy Setting', + 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', + 'app.setting.production.hint': + 'Setting panel shows in development environment only, please manually modify', +}; diff --git a/src/locales/en-US/settings.ts b/src/locales/en-US/settings.ts new file mode 100644 index 0000000..822dd00 --- /dev/null +++ b/src/locales/en-US/settings.ts @@ -0,0 +1,60 @@ +export default { + 'app.settings.menuMap.basic': 'Basic Settings', + 'app.settings.menuMap.security': 'Security Settings', + 'app.settings.menuMap.binding': 'Account Binding', + 'app.settings.menuMap.notification': 'New Message Notification', + 'app.settings.basic.avatar': 'Avatar', + 'app.settings.basic.change-avatar': 'Change avatar', + 'app.settings.basic.email': 'Email', + 'app.settings.basic.email-message': 'Please input your email!', + 'app.settings.basic.nickname': 'Nickname', + 'app.settings.basic.nickname-message': 'Please input your Nickname!', + 'app.settings.basic.profile': 'Personal profile', + 'app.settings.basic.profile-message': 'Please input your personal profile!', + 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', + 'app.settings.basic.country': 'Country/Region', + 'app.settings.basic.country-message': 'Please input your country!', + 'app.settings.basic.geographic': 'Province or city', + 'app.settings.basic.geographic-message': 'Please input your geographic info!', + 'app.settings.basic.address': 'Street Address', + 'app.settings.basic.address-message': 'Please input your address!', + 'app.settings.basic.phone': 'Phone Number', + 'app.settings.basic.phone-message': 'Please input your phone!', + 'app.settings.basic.update': 'Update Information', + 'app.settings.security.strong': 'Strong', + 'app.settings.security.medium': 'Medium', + 'app.settings.security.weak': 'Weak', + 'app.settings.security.password': 'Account Password', + 'app.settings.security.password-description': 'Current password strength', + 'app.settings.security.phone': 'Security Phone', + 'app.settings.security.phone-description': 'Bound phone', + 'app.settings.security.question': 'Security Question', + 'app.settings.security.question-description': + 'The security question is not set, and the security policy can effectively protect the account security', + 'app.settings.security.email': 'Backup Email', + 'app.settings.security.email-description': 'Bound Email', + 'app.settings.security.mfa': 'MFA Device', + 'app.settings.security.mfa-description': + 'Unbound MFA device, after binding, can be confirmed twice', + 'app.settings.security.modify': 'Modify', + 'app.settings.security.set': 'Set', + 'app.settings.security.bind': 'Bind', + 'app.settings.binding.taobao': 'Binding Taobao', + 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', + 'app.settings.binding.alipay': 'Binding Alipay', + 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', + 'app.settings.binding.dingding': 'Binding DingTalk', + 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', + 'app.settings.binding.bind': 'Bind', + 'app.settings.notification.password': 'Account Password', + 'app.settings.notification.password-description': + 'Messages from other users will be notified in the form of a station letter', + 'app.settings.notification.messages': 'System Messages', + 'app.settings.notification.messages-description': + 'System messages will be notified in the form of a station letter', + 'app.settings.notification.todo': 'To-do Notification', + 'app.settings.notification.todo-description': + 'The to-do list will be notified in the form of a letter from the station', + 'app.settings.open': 'Open', + 'app.settings.close': 'Close', +}; diff --git a/src/locales/pt-BR.ts b/src/locales/pt-BR.ts new file mode 100644 index 0000000..ee3733b --- /dev/null +++ b/src/locales/pt-BR.ts @@ -0,0 +1,20 @@ +import component from './pt-BR/component'; +import globalHeader from './pt-BR/globalHeader'; +import menu from './pt-BR/menu'; +import pwa from './pt-BR/pwa'; +import settingDrawer from './pt-BR/settingDrawer'; +import settings from './pt-BR/settings'; + +export default { + 'navBar.lang': 'Idiomas', + 'layout.user.link.help': 'ajuda', + 'layout.user.link.privacy': 'política de privacidade', + 'layout.user.link.terms': 'termos de serviços', + 'app.preview.down.block': 'Download this page to your local project', + ...globalHeader, + ...menu, + ...settingDrawer, + ...settings, + ...pwa, + ...component, +}; diff --git a/src/locales/pt-BR/component.ts b/src/locales/pt-BR/component.ts new file mode 100644 index 0000000..7cf9999 --- /dev/null +++ b/src/locales/pt-BR/component.ts @@ -0,0 +1,5 @@ +export default { + 'component.tagSelect.expand': 'Expandir', + 'component.tagSelect.collapse': 'Diminuir', + 'component.tagSelect.all': 'Todas', +}; diff --git a/src/locales/pt-BR/globalHeader.ts b/src/locales/pt-BR/globalHeader.ts new file mode 100644 index 0000000..c927399 --- /dev/null +++ b/src/locales/pt-BR/globalHeader.ts @@ -0,0 +1,18 @@ +export default { + 'component.globalHeader.search': 'Busca', + 'component.globalHeader.search.example1': 'Exemplo de busca 1', + 'component.globalHeader.search.example2': 'Exemplo de busca 2', + 'component.globalHeader.search.example3': 'Exemplo de busca 3', + 'component.globalHeader.help': 'Ajuda', + 'component.globalHeader.notification': 'Notificação', + 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', + 'component.globalHeader.message': 'Mensagem', + 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', + 'component.globalHeader.event': 'Evento', + 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', + 'component.noticeIcon.clear': 'Limpar', + 'component.noticeIcon.cleared': 'Limpo', + 'component.noticeIcon.empty': 'Sem notificações', + 'component.noticeIcon.loaded': 'Carregado', + 'component.noticeIcon.view-more': 'Veja mais', +}; diff --git a/src/locales/pt-BR/menu.ts b/src/locales/pt-BR/menu.ts new file mode 100644 index 0000000..3666b6b --- /dev/null +++ b/src/locales/pt-BR/menu.ts @@ -0,0 +1,51 @@ +export default { + 'menu.welcome': 'Welcome', + 'menu.more-blocks': 'More Blocks', + + 'menu.home': 'Início', + 'menu.login': 'Login', + 'menu.register': 'Registro', + 'menu.register.result': 'Resultado de registro', + 'menu.dashboard': 'Dashboard', + 'menu.dashboard.analysis': 'Análise', + 'menu.dashboard.monitor': 'Monitor', + 'menu.dashboard.workplace': 'Ambiente de Trabalho', + 'menu.exception.403': '403', + 'menu.exception.404': '404', + 'menu.exception.500': '500', + 'menu.form': 'Formulário', + 'menu.form.basic-form': 'Formulário Básico', + 'menu.form.step-form': 'Formulário Assistido', + 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', + 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', + 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', + 'menu.form.advanced-form': 'Formulário Avançado', + 'menu.list': 'Lista', + 'menu.list.table-list': 'Tabela de Busca', + 'menu.list.basic-list': 'Lista Básica', + 'menu.list.card-list': 'Lista de Card', + 'menu.list.search-list': 'Lista de Busca', + 'menu.list.search-list.articles': 'Lista de Busca(artigos)', + 'menu.list.search-list.projects': 'Lista de Busca(projetos)', + 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', + 'menu.profile': 'Perfil', + 'menu.profile.basic': 'Perfil Básico', + 'menu.profile.advanced': 'Perfil Avançado', + 'menu.result': 'Resultado', + 'menu.result.success': 'Sucesso', + 'menu.result.fail': 'Falha', + 'menu.exception': 'Exceção', + 'menu.exception.not-permission': '403', + 'menu.exception.not-find': '404', + 'menu.exception.server-error': '500', + 'menu.exception.trigger': 'Disparar', + 'menu.account': 'Conta', + 'menu.account.center': 'Central da Conta', + 'menu.account.settings': 'Configurar Conta', + 'menu.account.trigger': 'Disparar Erro', + 'menu.account.logout': 'Sair', + 'menu.editor': 'Graphic Editor', + 'menu.editor.flow': 'Flow Editor', + 'menu.editor.mind': 'Mind Editor', + 'menu.editor.koni': 'Koni Editor', +}; diff --git a/src/locales/pt-BR/pwa.ts b/src/locales/pt-BR/pwa.ts new file mode 100644 index 0000000..05cc797 --- /dev/null +++ b/src/locales/pt-BR/pwa.ts @@ -0,0 +1,7 @@ +export default { + 'app.pwa.offline': 'Você está offline agora', + 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', + 'app.pwa.serviceworker.updated.hint': + 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', + 'app.pwa.serviceworker.updated.ok': 'Atualizar', +}; diff --git a/src/locales/pt-BR/settingDrawer.ts b/src/locales/pt-BR/settingDrawer.ts new file mode 100644 index 0000000..8a10b57 --- /dev/null +++ b/src/locales/pt-BR/settingDrawer.ts @@ -0,0 +1,32 @@ +export default { + 'app.setting.pagestyle': 'Configuração de estilo da página', + 'app.setting.pagestyle.dark': 'Dark style', + 'app.setting.pagestyle.light': 'Light style', + 'app.setting.content-width': 'Largura do conteúdo', + 'app.setting.content-width.fixed': 'Fixo', + 'app.setting.content-width.fluid': 'Fluido', + 'app.setting.themecolor': 'Cor do Tema', + 'app.setting.themecolor.dust': 'Dust Red', + 'app.setting.themecolor.volcano': 'Volcano', + 'app.setting.themecolor.sunset': 'Sunset Orange', + 'app.setting.themecolor.cyan': 'Cyan', + 'app.setting.themecolor.green': 'Polar Green', + 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', + 'app.setting.themecolor.geekblue': 'Geek Glue', + 'app.setting.themecolor.purple': 'Golden Purple', + 'app.setting.navigationmode': 'Modo de Navegação', + 'app.setting.sidemenu': 'Layout do Menu Lateral', + 'app.setting.topmenu': 'Layout do Menu Superior', + 'app.setting.fixedheader': 'Cabeçalho fixo', + 'app.setting.fixedsidebar': 'Barra lateral fixa', + 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', + 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', + 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', + 'app.setting.othersettings': 'Outras configurações', + 'app.setting.weakmode': 'Weak Mode', + 'app.setting.copy': 'Copiar Configuração', + 'app.setting.copyinfo': + 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', + 'app.setting.production.hint': + 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', +}; diff --git a/src/locales/pt-BR/settings.ts b/src/locales/pt-BR/settings.ts new file mode 100644 index 0000000..aad2e38 --- /dev/null +++ b/src/locales/pt-BR/settings.ts @@ -0,0 +1,60 @@ +export default { + 'app.settings.menuMap.basic': 'Configurações Básicas', + 'app.settings.menuMap.security': 'Configurações de Segurança', + 'app.settings.menuMap.binding': 'Vinculação de Conta', + 'app.settings.menuMap.notification': 'Mensagens de Notificação', + 'app.settings.basic.avatar': 'Avatar', + 'app.settings.basic.change-avatar': 'Alterar avatar', + 'app.settings.basic.email': 'Email', + 'app.settings.basic.email-message': 'Por favor insira seu email!', + 'app.settings.basic.nickname': 'Nome de usuário', + 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!', + 'app.settings.basic.profile': 'Perfil pessoal', + 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!', + 'app.settings.basic.profile-placeholder': 'Breve introdução sua', + 'app.settings.basic.country': 'País/Região', + 'app.settings.basic.country-message': 'Por favor insira país!', + 'app.settings.basic.geographic': 'Província, estado ou cidade', + 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!', + 'app.settings.basic.address': 'Endereço', + 'app.settings.basic.address-message': 'Por favor insira seu endereço!', + 'app.settings.basic.phone': 'Número de telefone', + 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!', + 'app.settings.basic.update': 'Atualizar Informações', + 'app.settings.security.strong': 'Forte', + 'app.settings.security.medium': 'Média', + 'app.settings.security.weak': 'Fraca', + 'app.settings.security.password': 'Senha da Conta', + 'app.settings.security.password-description': 'Força da senha', + 'app.settings.security.phone': 'Telefone de Seguraça', + 'app.settings.security.phone-description': 'Telefone vinculado', + 'app.settings.security.question': 'Pergunta de Segurança', + 'app.settings.security.question-description': + 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta', + 'app.settings.security.email': 'Email de Backup', + 'app.settings.security.email-description': 'Email vinculado', + 'app.settings.security.mfa': 'Dispositivo MFA', + 'app.settings.security.mfa-description': + 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes', + 'app.settings.security.modify': 'Modificar', + 'app.settings.security.set': 'Atribuir', + 'app.settings.security.bind': 'Vincular', + 'app.settings.binding.taobao': 'Vincular Taobao', + 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao', + 'app.settings.binding.alipay': 'Vincular Alipay', + 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay', + 'app.settings.binding.dingding': 'Vincular DingTalk', + 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk', + 'app.settings.binding.bind': 'Vincular', + 'app.settings.notification.password': 'Senha da Conta', + 'app.settings.notification.password-description': + 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra', + 'app.settings.notification.messages': 'Mensagens de Sistema', + 'app.settings.notification.messages-description': + 'Mensagens de sistema serão notificadas na forma de uma estação de letra', + 'app.settings.notification.todo': 'Notificação de To-do', + 'app.settings.notification.todo-description': + 'A lista de to-do será notificada na forma de uma estação de letra', + 'app.settings.open': 'Aberto', + 'app.settings.close': 'Fechado', +}; diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts new file mode 100644 index 0000000..1822d7b --- /dev/null +++ b/src/locales/zh-CN.ts @@ -0,0 +1,22 @@ +import component from './zh-CN/component'; +import globalHeader from './zh-CN/globalHeader'; +import menu from './zh-CN/menu'; +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 开发,快速构建标准页面', + ...globalHeader, + ...menu, + ...settingDrawer, + ...settings, + ...pwa, + ...component, +}; diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts new file mode 100644 index 0000000..1f1fead --- /dev/null +++ b/src/locales/zh-CN/component.ts @@ -0,0 +1,5 @@ +export default { + 'component.tagSelect.expand': '展开', + 'component.tagSelect.collapse': '收起', + 'component.tagSelect.all': '全部', +}; diff --git a/src/locales/zh-CN/globalHeader.ts b/src/locales/zh-CN/globalHeader.ts new file mode 100644 index 0000000..9fd66a5 --- /dev/null +++ b/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': '查看更多', +}; diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts new file mode 100644 index 0000000..f851fe9 --- /dev/null +++ b/src/locales/zh-CN/menu.ts @@ -0,0 +1,50 @@ +export default { + 'menu.welcome': '欢迎', + 'menu.more-blocks': '更多区块', + 'menu.home': '首页', + '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': '拓扑编辑器', +}; diff --git a/src/locales/zh-CN/pwa.ts b/src/locales/zh-CN/pwa.ts new file mode 100644 index 0000000..e950484 --- /dev/null +++ b/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': '刷新', +}; diff --git a/src/locales/zh-CN/settingDrawer.ts b/src/locales/zh-CN/settingDrawer.ts new file mode 100644 index 0000000..15685a4 --- /dev/null +++ b/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': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', + 'app.setting.production.hint': + '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', +}; diff --git a/src/locales/zh-CN/settings.ts b/src/locales/zh-CN/settings.ts new file mode 100644 index 0000000..df8af43 --- /dev/null +++ b/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': '关', +}; diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts new file mode 100644 index 0000000..6ad5f93 --- /dev/null +++ b/src/locales/zh-TW.ts @@ -0,0 +1,20 @@ +import component from './zh-TW/component'; +import globalHeader from './zh-TW/globalHeader'; +import menu from './zh-TW/menu'; +import pwa from './zh-TW/pwa'; +import settingDrawer from './zh-TW/settingDrawer'; +import settings from './zh-TW/settings'; + +export default { + 'navBar.lang': '語言', + 'layout.user.link.help': '幫助', + 'layout.user.link.privacy': '隱私', + 'layout.user.link.terms': '條款', + 'app.preview.down.block': '下載此頁面到本地項目', + ...globalHeader, + ...menu, + ...settingDrawer, + ...settings, + ...pwa, + ...component, +}; diff --git a/src/locales/zh-TW/component.ts b/src/locales/zh-TW/component.ts new file mode 100644 index 0000000..ba48e29 --- /dev/null +++ b/src/locales/zh-TW/component.ts @@ -0,0 +1,5 @@ +export default { + 'component.tagSelect.expand': '展開', + 'component.tagSelect.collapse': '收起', + 'component.tagSelect.all': '全部', +}; diff --git a/src/locales/zh-TW/globalHeader.ts b/src/locales/zh-TW/globalHeader.ts new file mode 100644 index 0000000..ed58451 --- /dev/null +++ b/src/locales/zh-TW/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': '查看更多', +}; diff --git a/src/locales/zh-TW/menu.ts b/src/locales/zh-TW/menu.ts new file mode 100644 index 0000000..414affe --- /dev/null +++ b/src/locales/zh-TW/menu.ts @@ -0,0 +1,51 @@ +export default { + 'menu.welcome': '歡迎', + 'menu.more-blocks': '更多區塊', + + 'menu.home': '首頁', + 'menu.login': '登錄', + 'menu.exception.403': '403', + 'menu.exception.404': '404', + 'menu.exception.500': '500', + 'menu.register': '註冊', + 'menu.register.result': '註冊結果', + 'menu.dashboard': 'Dashboard', + 'menu.dashboard.analysis': '分析頁', + 'menu.dashboard.monitor': '監控頁', + 'menu.dashboard.workplace': '工作臺', + '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.account': '個人頁', + 'menu.account.center': '個人中心', + 'menu.account.settings': '個人設置', + 'menu.account.trigger': '觸發報錯', + 'menu.account.logout': '退出登錄', + 'menu.exception': '异常页', + 'menu.exception.not-permission': '403', + 'menu.exception.not-find': '404', + 'menu.exception.server-error': '500', + 'menu.exception.trigger': '触发错误', + 'menu.editor': '圖形編輯器', + 'menu.editor.flow': '流程編輯器', + 'menu.editor.mind': '腦圖編輯器', + 'menu.editor.koni': '拓撲編輯器', +}; diff --git a/src/locales/zh-TW/pwa.ts b/src/locales/zh-TW/pwa.ts new file mode 100644 index 0000000..108a6e4 --- /dev/null +++ b/src/locales/zh-TW/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': '刷新', +}; diff --git a/src/locales/zh-TW/settingDrawer.ts b/src/locales/zh-TW/settingDrawer.ts new file mode 100644 index 0000000..24dc281 --- /dev/null +++ b/src/locales/zh-TW/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': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', + 'app.setting.production.hint': + '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', +}; diff --git a/src/locales/zh-TW/settings.ts b/src/locales/zh-TW/settings.ts new file mode 100644 index 0000000..dd45151 --- /dev/null +++ b/src/locales/zh-TW/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': '關', +}; diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..839bc5b --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Ant Design Pro", + "short_name": "Ant Design Pro", + "display": "standalone", + "start_url": "./?utm_source=homescreen", + "theme_color": "#002140", + "background_color": "#001529", + "icons": [ + { + "src": "icons/icon-192x192.png", + "sizes": "192x192" + }, + { + "src": "icons/icon-128x128.png", + "sizes": "128x128" + }, + { + "src": "icons/icon-512x512.png", + "sizes": "512x512" + } + ] +} diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts new file mode 100644 index 0000000..0875800 --- /dev/null +++ b/src/models/connect.d.ts @@ -0,0 +1,40 @@ +import { AnyAction, Dispatch } from 'redux'; +import { MenuDataItem } from '@ant-design/pro-layout'; +import { RouterTypes } from 'umi'; +import { GlobalModelState } from './global'; +import { DefaultSettings as SettingModelState } from '../../config/defaultSettings'; +import { UserModelState } from './user'; +import { LoginModelType } from './login'; + +export { GlobalModelState, SettingModelState, UserModelState }; + +export interface Loading { + global: boolean; + effects: { [key: string]: boolean | undefined }; + models: { + global?: boolean; + menu?: boolean; + setting?: boolean; + user?: boolean; + login?: boolean; + }; +} + +export interface ConnectState { + global: GlobalModelState; + loading: Loading; + settings: SettingModelState; + user: UserModelState; + login: LoginModelType; +} + +export interface Route extends MenuDataItem { + routes?: Route[]; +} + +/** + * @type T: Params matched in dynamic routing + */ +export interface ConnectProps extends Partial> { + dispatch?: Dispatch; +} diff --git a/src/models/global.ts b/src/models/global.ts new file mode 100644 index 0000000..e143402 --- /dev/null +++ b/src/models/global.ts @@ -0,0 +1,139 @@ +import { Reducer } from 'redux'; +import { Subscription, Effect } from 'dva'; + +import { NoticeIconData } from '@/components/NoticeIcon'; +import { queryNotices } from '@/services/user'; +import { ConnectState } from './connect.d'; + +export interface NoticeItem extends NoticeIconData { + id: string; + type: string; + status: string; +} + +export interface GlobalModelState { + collapsed: boolean; + notices: NoticeItem[]; +} + +export interface GlobalModelType { + namespace: 'global'; + state: GlobalModelState; + effects: { + fetchNotices: Effect; + clearNotices: Effect; + changeNoticeReadState: Effect; + }; + reducers: { + changeLayoutCollapsed: Reducer; + saveNotices: Reducer; + saveClearedNotices: Reducer; + }; + subscriptions: { setup: Subscription }; +} + +const GlobalModel: GlobalModelType = { + namespace: 'global', + + state: { + collapsed: false, + notices: [], + }, + + effects: { + *fetchNotices(_, { call, put, select }) { + const data = yield call(queryNotices); + yield put({ + type: 'saveNotices', + payload: data, + }); + const unreadCount: number = yield select( + (state: ConnectState) => state.global.notices.filter(item => !item.read).length, + ); + yield put({ + type: 'user/changeNotifyCount', + payload: { + totalCount: data.length, + unreadCount, + }, + }); + }, + *clearNotices({ payload }, { put, select }) { + yield put({ + type: 'saveClearedNotices', + payload, + }); + const count: number = yield select((state: ConnectState) => state.global.notices.length); + const unreadCount: number = yield select( + (state: ConnectState) => state.global.notices.filter(item => !item.read).length, + ); + yield put({ + type: 'user/changeNotifyCount', + payload: { + totalCount: count, + unreadCount, + }, + }); + }, + *changeNoticeReadState({ payload }, { put, select }) { + const notices: NoticeItem[] = yield select((state: ConnectState) => + state.global.notices.map(item => { + const notice = { ...item }; + if (notice.id === payload) { + notice.read = true; + } + return notice; + }), + ); + + yield put({ + type: 'saveNotices', + payload: notices, + }); + + yield put({ + type: 'user/changeNotifyCount', + payload: { + totalCount: notices.length, + unreadCount: notices.filter(item => !item.read).length, + }, + }); + }, + }, + + reducers: { + changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { + return { + ...state, + collapsed: payload, + }; + }, + saveNotices(state, { payload }): GlobalModelState { + return { + collapsed: false, + ...state, + notices: payload, + }; + }, + saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { + return { + collapsed: false, + ...state, + notices: state.notices.filter((item): boolean => item.type !== payload), + }; + }, + }, + + subscriptions: { + setup({ history }): void { + // Subscribe history(url) change, trigger `load` action if pathname is `/` + history.listen(({ pathname, search }): void => { + if (typeof window.ga !== 'undefined') { + window.ga('send', 'pageview', pathname + search); + } + }); + }, + }, +}; + +export default GlobalModel; diff --git a/src/models/login.ts b/src/models/login.ts new file mode 100644 index 0000000..740c8d6 --- /dev/null +++ b/src/models/login.ts @@ -0,0 +1,95 @@ +import { Reducer } from 'redux'; +import { routerRedux } from 'dva/router'; +import { Effect } from 'dva'; +import { stringify } from 'querystring'; + +import { fakeAccountLogin, getFakeCaptcha } from '@/services/login'; +import { setAuthority } from '@/utils/authority'; +import { getPageQuery } from '@/utils/utils'; + +export interface StateType { + status?: 'ok' | 'error'; + type?: string; + currentAuthority?: 'user' | 'guest' | 'admin'; +} + +export interface LoginModelType { + namespace: string; + state: StateType; + effects: { + login: Effect; + getCaptcha: Effect; + logout: Effect; + }; + reducers: { + changeLoginStatus: Reducer; + }; +} + +const Model: LoginModelType = { + namespace: 'login', + + state: { + status: undefined, + }, + + effects: { + *login({ payload }, { call, put }) { + const response = yield call(fakeAccountLogin, payload); + yield put({ + type: 'changeLoginStatus', + payload: response, + }); + // Login successfully + if (response.status === 'ok') { + const urlParams = new URL(window.location.href); + const params = getPageQuery(); + let { redirect } = params as { redirect: string }; + if (redirect) { + const redirectUrlParams = new URL(redirect); + if (redirectUrlParams.origin === urlParams.origin) { + redirect = redirect.substr(urlParams.origin.length); + if (redirect.match(/^\/.*#/)) { + redirect = redirect.substr(redirect.indexOf('#') + 1); + } + } else { + window.location.href = redirect; + return; + } + } + yield put(routerRedux.replace(redirect || '/')); + } + }, + + *getCaptcha({ payload }, { call }) { + yield call(getFakeCaptcha, payload); + }, + *logout(_, { put }) { + const { redirect } = getPageQuery(); + // redirect + if (window.location.pathname !== '/user/login' && !redirect) { + yield put( + routerRedux.replace({ + pathname: '/user/login', + search: stringify({ + redirect: window.location.href, + }), + }), + ); + } + }, + }, + + reducers: { + changeLoginStatus(state, { payload }) { + setAuthority(payload.currentAuthority); + return { + ...state, + status: payload.status, + type: payload.type, + }; + }, + }, +}; + +export default Model; diff --git a/src/models/setting.ts b/src/models/setting.ts new file mode 100644 index 0000000..7209d9b --- /dev/null +++ b/src/models/setting.ts @@ -0,0 +1,89 @@ +import { Reducer } from 'redux'; +import { message } from 'antd'; +import defaultSettings, { DefaultSettings } from '../../config/defaultSettings'; +import themeColorClient from '../components/SettingDrawer/themeColorClient'; + +export interface SettingModelType { + namespace: 'settings'; + state: DefaultSettings; + reducers: { + getSetting: Reducer; + changeSetting: Reducer; + }; +} + +const updateTheme = (newPrimaryColor?: string) => { + if (newPrimaryColor) { + const timeOut = 0; + const hideMessage = message.loading('正在切换主题!', timeOut); + themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage()); + } +}; + +const updateColorWeak: (colorWeak: boolean) => void = colorWeak => { + const root = document.getElementById('root'); + if (root) { + root.className = colorWeak ? 'colorWeak' : ''; + } +}; + +const SettingModel: SettingModelType = { + namespace: 'settings', + state: defaultSettings, + reducers: { + getSetting(state = defaultSettings) { + const setting: Partial = {}; + const urlParams = new URL(window.location.href); + Object.keys(state).forEach(key => { + if (urlParams.searchParams.has(key)) { + const value = urlParams.searchParams.get(key); + setting[key] = value === '1' ? true : value; + } + }); + const { primaryColor, colorWeak } = setting; + + if (primaryColor && state.primaryColor !== primaryColor) { + updateTheme(primaryColor); + } + updateColorWeak(!!colorWeak); + return { + ...state, + ...setting, + }; + }, + changeSetting(state = defaultSettings, { payload }) { + const urlParams = new URL(window.location.href); + Object.keys(defaultSettings).forEach(key => { + if (urlParams.searchParams.has(key)) { + urlParams.searchParams.delete(key); + } + }); + Object.keys(payload).forEach(key => { + if (key === 'collapse') { + return; + } + let value = payload[key]; + if (value === true) { + value = 1; + } + if (defaultSettings[key] !== value) { + urlParams.searchParams.set(key, value); + } + }); + const { primaryColor, colorWeak, contentWidth } = payload; + if (primaryColor && state.primaryColor !== primaryColor) { + updateTheme(primaryColor); + } + if (state.contentWidth !== contentWidth && window.dispatchEvent) { + window.dispatchEvent(new Event('resize')); + } + updateColorWeak(!!colorWeak); + window.history.replaceState(null, 'setting', urlParams.href); + return { + ...state, + ...payload, + }; + }, + }, +}; +export default SettingModel; diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..360ba8e --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,86 @@ +import { Effect } from 'dva'; +import { Reducer } from 'redux'; + +import { queryCurrent, query as queryUsers } from '@/services/user'; + +export interface CurrentUser { + avatar?: string; + name?: string; + title?: string; + group?: string; + signature?: string; + tags?: { + key: string; + label: string; + }[]; + userid?: string; + unreadCount?: number; +} + +export interface UserModelState { + currentUser?: CurrentUser; +} + +export interface UserModelType { + namespace: 'user'; + state: UserModelState; + effects: { + fetch: Effect; + fetchCurrent: Effect; + }; + reducers: { + saveCurrentUser: Reducer; + changeNotifyCount: Reducer; + }; +} + +const UserModel: UserModelType = { + namespace: 'user', + + state: { + currentUser: {}, + }, + + effects: { + *fetch(_, { call, put }) { + const response = yield call(queryUsers); + yield put({ + type: 'save', + payload: response, + }); + }, + *fetchCurrent(_, { call, put }) { + const response = yield call(queryCurrent); + yield put({ + type: 'saveCurrentUser', + payload: response, + }); + }, + }, + + reducers: { + saveCurrentUser(state, action) { + return { + ...state, + currentUser: action.payload || {}, + }; + }, + changeNotifyCount( + state = { + currentUser: {}, + }, + action, + ) { + return { + ...state, + currentUser: { + ...state.currentUser, + notifyCount: action.payload.totalCount, + unreadCount: action.payload.unreadCount, + }, + }; + }, + }, +}; + +export default UserModel; diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..05b0a2d --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,21 @@ +import { Button, Result } from 'antd'; +import React from 'react'; +import router from 'umi/router'; + +// 这里应该使用 antd 的 404 result 组件, +// 但是还没发布,先来个简单的。 + +const NoFoundPage: React.FC<{}> = () => ( + router.push('/')}> + Back Home + + } + > +); + +export default NoFoundPage; diff --git a/src/pages/Authorized.tsx b/src/pages/Authorized.tsx new file mode 100644 index 0000000..9c94e47 --- /dev/null +++ b/src/pages/Authorized.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import Redirect from 'umi/redirect'; +import { connect } from 'dva'; +import pathToRegexp from 'path-to-regexp'; +import Authorized from '@/utils/Authorized'; +import { ConnectProps, ConnectState, Route, UserModelState } from '@/models/connect'; + +interface AuthComponentProps extends ConnectProps { + user: UserModelState; +} + +const getRouteAuthority = (path: string, routeData: Route[]) => { + let authorities: string[] | string | undefined; + routeData.forEach(route => { + if (route.authority) { + authorities = route.authority; + } + // match prefix + if (pathToRegexp(`${route.path}(.*)`).test(path)) { + // exact match + if (route.path === path) { + authorities = route.authority || authorities; + } + // get children authority recursively + if (route.routes) { + authorities = getRouteAuthority(path, route.routes) || authorities; + } + } + }); + return authorities; +}; + +const AuthComponent: React.FC = ({ + children, + route = { + routes: [], + }, + location = { + pathname: '', + }, + user, +}) => { + const { currentUser } = user; + const { routes = [] } = route; + const isLogin = currentUser && currentUser.name; + return ( + : } + > + {children} + + ); +}; + +export default connect(({ user }: ConnectState) => ({ + user, +}))(AuthComponent); diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx new file mode 100644 index 0000000..52c8549 --- /dev/null +++ b/src/pages/Welcome.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Card, Typography, Alert } from 'antd'; +import { PageHeaderWrapper } from '@ant-design/pro-layout'; +import { FormattedMessage } from 'umi-plugin-react/locale'; + +const CodePreview: React.FC<{}> = ({ children }) => ( +
+    
+      {children}
+    
+  
+); + +export default (): React.ReactNode => ( + + + + + + + + + npx umi block list + + + + + + npm run fetch:blocks + +

+ Want to add more pages? Please refer to{' '} + + use block + + 。 +

+
+); diff --git a/src/pages/document.ejs b/src/pages/document.ejs new file mode 100644 index 0000000..ea08d4e --- /dev/null +++ b/src/pages/document.ejs @@ -0,0 +1,168 @@ + + + + + + + Ant Design Pro + + + + +
+ +
+
+ +
+
+
+ + diff --git a/src/pages/user/login/components/Login/LoginContext.tsx b/src/pages/user/login/components/Login/LoginContext.tsx new file mode 100644 index 0000000..ae571e0 --- /dev/null +++ b/src/pages/user/login/components/Login/LoginContext.tsx @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +export interface LoginContextProps { + tabUtil?: { + addTab: (id: string) => void; + removeTab: (id: string) => void; + }; + updateActive?: (activeItem: { [key: string]: string } | string) => void; +} + +const LoginContext: React.Context = createContext({}); + +export default LoginContext; diff --git a/src/pages/user/login/components/Login/LoginItem.tsx b/src/pages/user/login/components/Login/LoginItem.tsx new file mode 100644 index 0000000..75e0fe5 --- /dev/null +++ b/src/pages/user/login/components/Login/LoginItem.tsx @@ -0,0 +1,196 @@ +import { Button, Col, Form, Input, Row } from 'antd'; +import React, { Component } from 'react'; +import { FormComponentProps } from 'antd/es/form'; +import { GetFieldDecoratorOptions } from 'antd/es/form/Form'; + +import omit from 'omit.js'; +import ItemMap from './map'; +import LoginContext, { LoginContextProps } from './LoginContext'; +import styles from './index.less'; + +type Omit = Pick>; + +export type WrappedLoginItemProps = Omit; +export type LoginItemKeyType = keyof typeof ItemMap; +export interface LoginItemType { + UserName: React.FC; + Password: React.FC; + Mobile: React.FC; + Captcha: React.FC; +} + +export interface LoginItemProps extends GetFieldDecoratorOptions { + name?: string; + style?: React.CSSProperties; + onGetCaptcha?: (event?: MouseEvent) => void | Promise | false; + placeholder?: string; + buttonText?: React.ReactNode; + onPressEnter?: (e: React.KeyboardEvent) => void; + countDown?: number; + getCaptchaButtonText?: string; + getCaptchaSecondText?: string; + updateActive?: LoginContextProps['updateActive']; + type?: string; + defaultValue?: string; + form?: FormComponentProps['form']; + customProps?: { [key: string]: unknown }; + onChange?: (e: React.ChangeEvent) => void; + tabUtil?: LoginContextProps['tabUtil']; +} + +interface LoginItemState { + count: number; +} + +const FormItem = Form.Item; + +class WrapFormItem extends Component { + static defaultProps = { + getCaptchaButtonText: 'captcha', + getCaptchaSecondText: 'second', + }; + + interval: number | undefined = undefined; + + constructor(props: LoginItemProps) { + super(props); + this.state = { + count: 0, + }; + } + + componentDidMount() { + const { updateActive, name = '' } = this.props; + if (updateActive) { + updateActive(name); + } + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onGetCaptcha = () => { + const { onGetCaptcha } = this.props; + const result = onGetCaptcha ? onGetCaptcha() : null; + if (result === false) { + return; + } + if (result instanceof Promise) { + result.then(this.runGetCaptchaCountDown); + } else { + this.runGetCaptchaCountDown(); + } + }; + + getFormItemOptions = ({ onChange, defaultValue, customProps = {}, rules }: LoginItemProps) => { + const options: { + rules?: LoginItemProps['rules']; + onChange?: LoginItemProps['onChange']; + initialValue?: LoginItemProps['defaultValue']; + } = { + rules: rules || (customProps.rules as LoginItemProps['rules']), + }; + if (onChange) { + options.onChange = onChange; + } + if (defaultValue) { + options.initialValue = defaultValue; + } + return options; + }; + + runGetCaptchaCountDown = () => { + const { countDown } = this.props; + let count = countDown || 59; + this.setState({ count }); + this.interval = window.setInterval(() => { + count -= 1; + this.setState({ count }); + if (count === 0) { + clearInterval(this.interval); + } + }, 1000); + }; + + render() { + const { count } = this.state; + + // 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil + const { + onChange, + customProps, + defaultValue, + rules, + name, + getCaptchaButtonText, + getCaptchaSecondText, + updateActive, + type, + form, + tabUtil, + ...restProps + } = this.props; + if (!name) { + return null; + } + if (!form) { + return null; + } + const { getFieldDecorator } = form; + // get getFieldDecorator props + const options = this.getFormItemOptions(this.props); + const otherProps = restProps || {}; + + if (type === 'Captcha') { + const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']); + + return ( + + + + {getFieldDecorator(name, options)()} + + + + + + + ); + } + return ( + + {getFieldDecorator(name, options)()} + + ); + } +} + +const LoginItem: Partial = {}; + +Object.keys(ItemMap).forEach(key => { + const item = ItemMap[key]; + LoginItem[key] = (props: LoginItemProps) => ( + + {context => ( + + )} + + ); +}); + +export default LoginItem as LoginItemType; diff --git a/src/pages/user/login/components/Login/LoginSubmit.tsx b/src/pages/user/login/components/Login/LoginSubmit.tsx new file mode 100644 index 0000000..280fb0f --- /dev/null +++ b/src/pages/user/login/components/Login/LoginSubmit.tsx @@ -0,0 +1,23 @@ +import { Button, Form } from 'antd'; + +import { ButtonProps } from 'antd/es/button'; +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +const FormItem = Form.Item; + +interface LoginSubmitProps extends ButtonProps { + className?: string; +} + +const LoginSubmit: React.FC = ({ className, ...rest }) => { + const clsString = classNames(styles.submit, className); + return ( + +