├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── README.md ├── config ├── config.js ├── defaultSettings.js ├── plugin.config.js ├── proxy.js └── themePluginConfig.js ├── demo.png ├── demo1.png ├── demo3.gif ├── jest.config.js ├── jsconfig.json ├── mock ├── notices.js ├── route.js └── user.js ├── package.json ├── public ├── favicon.png ├── home_bg.png ├── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png └── pro_icon.svg ├── src ├── assets │ └── logo.svg ├── components │ ├── Authorized │ │ ├── Authorized.jsx │ │ ├── AuthorizedRoute.jsx │ │ ├── CheckPermissions.jsx │ │ ├── PromiseRender.jsx │ │ ├── Secured.jsx │ │ ├── index.jsx │ │ └── renderAuthorize.js │ ├── GlobalHeader │ │ ├── AvatarDropdown.jsx │ │ ├── NoticeIconView.jsx │ │ ├── RightContent.jsx │ │ └── index.less │ ├── HeaderDropdown │ │ ├── index.jsx │ │ └── index.less │ ├── HeaderSearch │ │ ├── index.jsx │ │ └── index.less │ ├── NoticeIcon │ │ ├── NoticeList.jsx │ │ ├── NoticeList.less │ │ ├── index.jsx │ │ └── index.less │ ├── PageLoading │ │ └── index.jsx │ └── SelectLang │ │ ├── index.jsx │ │ └── index.less ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ ├── baseLayout.e2e.js │ └── topMenu.e2e.js ├── global.jsx ├── global.less ├── layouts │ ├── BasicLayout.jsx │ ├── BlankLayout.jsx │ ├── DraggableTabs.jsx │ ├── PageTab.jsx │ ├── PageTab.less │ ├── SecurityLayout.jsx │ ├── UserLayout.jsx │ └── UserLayout.less ├── locales │ ├── en-US.js │ ├── en-US │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js │ ├── pt-BR.js │ ├── pt-BR │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js │ ├── zh-CN.js │ ├── zh-CN │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js │ ├── zh-TW.js │ └── zh-TW │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js ├── manifest.json ├── models │ ├── global.js │ ├── login.js │ ├── setting.js │ ├── tabs.js │ └── user.js ├── pages │ ├── 404.jsx │ ├── Admin.jsx │ ├── Authorized.jsx │ ├── CustomPage │ │ ├── index.jsx │ │ └── index.less │ ├── Test.jsx │ ├── Welcome.jsx │ ├── Welcome.less │ ├── document.ejs │ └── user │ │ └── login │ │ ├── components │ │ └── Login │ │ │ ├── LoginContext.jsx │ │ │ ├── LoginItem.jsx │ │ │ ├── LoginSubmit.jsx │ │ │ ├── LoginTab.jsx │ │ │ ├── index.jsx │ │ │ ├── index.less │ │ │ └── map.jsx │ │ ├── index.jsx │ │ ├── locales │ │ ├── en-US.js │ │ ├── zh-CN.js │ │ └── zh-TW.js │ │ └── style.less ├── service-worker.js ├── services │ ├── login.js │ └── user.js └── utils │ ├── Authorized.js │ ├── authority.js │ ├── authority.test.js │ ├── request.js │ ├── utils.js │ ├── utils.less │ └── utils.test.js └── tests ├── run-tests.js └── setupTests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ant Design Pro 2 | 3 | ## 基于 Ant Design Pro 二次开发,支持多 Tab 标签页面,模拟 Chrome 标签页功能 4 | 5 | ![demo](/demo.png) 6 | 7 | ![demo](/demo1.png) ![demo](/demo3.gif) 8 | 9 | ## 更新日志 10 | 11 | 1、添加标签支持拖动(2020-07-16); 12 | 13 | # 14 | 15 | This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use. 16 | 17 | ## Environment Prepare 18 | 19 | Install `node_modules`: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | or 26 | 27 | ```bash 28 | yarn 29 | ``` 30 | 31 | ## Provided Scripts 32 | 33 | Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test. 34 | 35 | Scripts provided in `package.json`. It's safe to modify or add additional script: 36 | 37 | ### Start project 38 | 39 | ```bash 40 | npm start 41 | ``` 42 | 43 | ### Build project 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | ### Check code style 50 | 51 | ```bash 52 | npm run lint 53 | ``` 54 | 55 | You can also use script to auto fix some lint error: 56 | 57 | ```bash 58 | npm run lint:fix 59 | ``` 60 | 61 | ### Test code 62 | 63 | ```bash 64 | npm test 65 | ``` 66 | 67 | ## More 68 | 69 | 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). 70 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | import slash from 'slash2'; 2 | import defaultSettings from './defaultSettings'; // https://umijs.org/config/ 3 | 4 | import themePluginConfig from './themePluginConfig'; 5 | import proxy from './proxy'; 6 | import webpackPlugin from './plugin.config'; 7 | 8 | const { pwa } = defaultSettings; // preview.pro.ant.design only do not use in your production ; 9 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 10 | 11 | const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, REACT_APP_ENV } = process.env; 12 | const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site'; 13 | const plugins = [ 14 | ['umi-plugin-antd-icon-config', {}], 15 | [ 16 | 'umi-plugin-react', 17 | { 18 | antd: true, 19 | dva: { 20 | hmr: true, 21 | }, 22 | locale: { 23 | // default false 24 | enable: true, 25 | // default zh-CN 26 | default: 'zh-CN', 27 | // default true, when it is true, will use `navigator.language` overwrite default 28 | baseNavigator: true, 29 | }, 30 | dynamicImport: { 31 | loadingComponent: './components/PageLoading/index', 32 | webpackChunkName: true, 33 | level: 3, 34 | }, 35 | pwa: pwa 36 | ? { 37 | workboxPluginMode: 'InjectManifest', 38 | workboxOptions: { 39 | importWorkboxFrom: 'local', 40 | }, 41 | } 42 | : false, // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665 43 | // dll features https://webpack.js.org/plugins/dll-plugin/ 44 | // dll: { 45 | // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'], 46 | // exclude: ['@babel/runtime', 'netlify-lambda'], 47 | // }, 48 | }, 49 | ], 50 | [ 51 | 'umi-plugin-pro-block', 52 | { 53 | moveMock: false, 54 | moveService: false, 55 | modifyRequest: true, 56 | autoAddMenu: true, 57 | }, 58 | ], 59 | ]; 60 | 61 | if (isAntDesignProPreview) { 62 | // 针对 preview.pro.ant.design 的 GA 统计代码 63 | plugins.push([ 64 | 'umi-plugin-ga', 65 | { 66 | code: 'UA-72788897-6', 67 | }, 68 | ]); 69 | plugins.push(['umi-plugin-antd-theme', themePluginConfig]); 70 | } 71 | 72 | export default { 73 | plugins, 74 | hash: true, 75 | targets: { 76 | ie: 11, 77 | }, 78 | // umi routes: https://umijs.org/zh/guide/router.html 79 | routes: [ 80 | { 81 | path: '/user', 82 | component: '../layouts/UserLayout', 83 | routes: [ 84 | { 85 | name: 'login', 86 | path: '/user/login', 87 | component: './user/login', 88 | }, 89 | ], 90 | }, 91 | { 92 | path: '/', 93 | component: '../layouts/SecurityLayout', 94 | routes: [ 95 | { 96 | path: '/', 97 | component: '../layouts/BasicLayout', 98 | authority: ['admin', 'user'], 99 | routes: [ 100 | { 101 | path: '/', 102 | redirect: '/welcome', 103 | }, 104 | { 105 | path: '/welcome', 106 | name: 'welcome', 107 | icon: 'smile', 108 | component: './Welcome', 109 | }, 110 | { 111 | path: '/CustomPage/:id', 112 | name: 'CustomPage', 113 | component: './CustomPage', 114 | hideInMenu: true, 115 | }, 116 | { 117 | path: '/test', 118 | name: 'test', 119 | icon: 'smile', 120 | component: './Test', 121 | }, 122 | { 123 | path: '/admin', 124 | name: 'admin', 125 | icon: 'crown', 126 | component: './Admin', 127 | authority: ['admin'], 128 | routes: [ 129 | { 130 | path: '/admin/sub-page', 131 | name: 'sub-page', 132 | icon: 'smile', 133 | component: './Welcome', 134 | authority: ['admin'], 135 | }, 136 | ], 137 | }, 138 | { 139 | component: './404', 140 | }, 141 | ], 142 | }, 143 | { 144 | component: './404', 145 | }, 146 | ], 147 | }, 148 | { 149 | component: './404', 150 | }, 151 | ], 152 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn 153 | theme: { 154 | // ...darkTheme, 155 | 'primary-color': defaultSettings.primaryColor, 156 | }, 157 | define: { 158 | REACT_APP_ENV: REACT_APP_ENV || false, 159 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 160 | 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 专用环境变量,请不要在你的项目中使用它。 161 | }, 162 | ignoreMomentLocale: true, 163 | lessLoaderOptions: { 164 | javascriptEnabled: true, 165 | }, 166 | disableRedirectHoist: true, 167 | cssLoaderOptions: { 168 | modules: true, 169 | getLocalIdent: (context, _, localName) => { 170 | if ( 171 | context.resourcePath.includes('node_modules') || 172 | context.resourcePath.includes('ant.design.pro.less') || 173 | context.resourcePath.includes('global.less') 174 | ) { 175 | return localName; 176 | } 177 | 178 | const match = context.resourcePath.match(/src(.*)/); 179 | 180 | if (match && match[1]) { 181 | const antdProPath = match[1].replace('.less', ''); 182 | const arr = slash(antdProPath) 183 | .split('/') 184 | .map(a => a.replace(/([A-Z])/g, '-$1')) 185 | .map(a => a.toLowerCase()); 186 | return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-'); 187 | } 188 | 189 | return localName; 190 | }, 191 | }, 192 | manifest: { 193 | basePath: '/', 194 | }, 195 | proxy: proxy[REACT_APP_ENV || 'dev'], 196 | chainWebpack: webpackPlugin, 197 | }; 198 | -------------------------------------------------------------------------------- /config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | navTheme: 'dark', 3 | // 拂晓蓝 4 | primaryColor: '#1890ff', 5 | layout: 'sidemenu', 6 | contentWidth: 'Fluid', 7 | fixedHeader: false, 8 | autoHideHeader: false, 9 | fixSiderbar: false, 10 | colorWeak: false, 11 | menu: { 12 | locale: true, 13 | }, 14 | title: 'Ant Design Plus', 15 | pwa: false, 16 | iconfontUrl: '', 17 | }; 18 | -------------------------------------------------------------------------------- /config/plugin.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | function getModulePackageName(module) { 4 | if (!module.context) return null; 5 | const nodeModulesPath = path.join(__dirname, '../node_modules/'); 6 | 7 | if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) { 8 | return null; 9 | } 10 | 11 | const moduleRelativePath = module.context.substring(nodeModulesPath.length); 12 | const [moduleDirName] = moduleRelativePath.split(path.sep); 13 | let packageName = moduleDirName; // handle tree shaking 14 | 15 | if (packageName && packageName.match('^_')) { 16 | // eslint-disable-next-line prefer-destructuring 17 | packageName = packageName.match(/^_(@?[^@]+)/)[1]; 18 | } 19 | 20 | return packageName; 21 | } 22 | 23 | const webpackPlugin = config => { 24 | // optimize chunks 25 | config.optimization // share the same chunks across different modules 26 | .runtimeChunk(false) 27 | .splitChunks({ 28 | chunks: 'async', 29 | name: 'vendors', 30 | maxInitialRequests: Infinity, 31 | minSize: 0, 32 | cacheGroups: { 33 | vendors: { 34 | test: module => { 35 | const packageName = getModulePackageName(module) || ''; 36 | 37 | if (packageName) { 38 | return [ 39 | 'bizcharts', 40 | 'gg-editor', 41 | 'g6', 42 | '@antv', 43 | 'l7', 44 | 'gg-editor-core', 45 | 'bizcharts-plugin-slider', 46 | ].includes(packageName); 47 | } 48 | 49 | return false; 50 | }, 51 | 52 | name(module) { 53 | const packageName = getModulePackageName(module); 54 | 55 | if (packageName) { 56 | if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) { 57 | return 'viz'; // visualization package 58 | } 59 | } 60 | 61 | return 'misc'; 62 | }, 63 | }, 64 | }, 65 | }); 66 | }; 67 | 68 | export default webpackPlugin; 69 | -------------------------------------------------------------------------------- /config/proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * The agent cannot take effect in the production environment 4 | * so there is no configuration of the production environment 5 | * For details, please see 6 | * https://pro.ant.design/docs/deploy 7 | */ 8 | export default { 9 | dev: { 10 | '/api/': { 11 | target: 'https://preview.pro.ant.design', 12 | changeOrigin: true, 13 | pathRewrite: { 14 | '^': '', 15 | }, 16 | }, 17 | }, 18 | test: { 19 | '/api/': { 20 | target: 'https://preview.pro.ant.design', 21 | changeOrigin: true, 22 | pathRewrite: { 23 | '^': '', 24 | }, 25 | }, 26 | }, 27 | pre: { 28 | '/api/': { 29 | target: 'your pre url', 30 | changeOrigin: true, 31 | pathRewrite: { 32 | '^': '', 33 | }, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /config/themePluginConfig.js: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: [ 3 | { 4 | key: 'dark', 5 | fileName: 'dark.css', 6 | theme: 'dark', 7 | }, 8 | { 9 | key: 'dust', 10 | fileName: 'dust.css', 11 | modifyVars: { 12 | '@primary-color': '#F5222D', 13 | }, 14 | }, 15 | { 16 | key: 'volcano', 17 | fileName: 'volcano.css', 18 | modifyVars: { 19 | '@primary-color': '#FA541C', 20 | }, 21 | }, 22 | { 23 | key: 'sunset', 24 | fileName: 'sunset.css', 25 | modifyVars: { 26 | '@primary-color': '#FAAD14', 27 | }, 28 | }, 29 | { 30 | key: 'cyan', 31 | fileName: 'cyan.css', 32 | modifyVars: { 33 | '@primary-color': '#13C2C2', 34 | }, 35 | }, 36 | { 37 | key: 'green', 38 | fileName: 'green.css', 39 | modifyVars: { 40 | '@primary-color': '#52C41A', 41 | }, 42 | }, 43 | { 44 | key: 'geekblue', 45 | fileName: 'geekblue.css', 46 | modifyVars: { 47 | '@primary-color': '#2F54EB', 48 | }, 49 | }, 50 | { 51 | key: 'purple', 52 | fileName: 'purple.css', 53 | modifyVars: { 54 | '@primary-color': '#722ED1', 55 | }, 56 | }, 57 | { 58 | key: 'dust', 59 | theme: 'dark', 60 | fileName: 'dark-dust.css', 61 | modifyVars: { 62 | '@primary-color': '#F5222D', 63 | }, 64 | }, 65 | { 66 | key: 'volcano', 67 | theme: 'dark', 68 | fileName: 'dark-volcano.css', 69 | modifyVars: { 70 | '@primary-color': '#FA541C', 71 | }, 72 | }, 73 | { 74 | key: 'sunset', 75 | theme: 'dark', 76 | fileName: 'dark-sunset.css', 77 | modifyVars: { 78 | '@primary-color': '#FAAD14', 79 | }, 80 | }, 81 | { 82 | key: 'cyan', 83 | theme: 'dark', 84 | fileName: 'dark-cyan.css', 85 | modifyVars: { 86 | '@primary-color': '#13C2C2', 87 | }, 88 | }, 89 | { 90 | key: 'green', 91 | theme: 'dark', 92 | fileName: 'dark-green.css', 93 | modifyVars: { 94 | '@primary-color': '#52C41A', 95 | }, 96 | }, 97 | { 98 | key: 'geekblue', 99 | theme: 'dark', 100 | fileName: 'dark-geekblue.css', 101 | modifyVars: { 102 | '@primary-color': '#2F54EB', 103 | }, 104 | }, 105 | { 106 | key: 'purple', 107 | theme: 'dark', 108 | fileName: 'dark-purple.css', 109 | modifyVars: { 110 | '@primary-color': '#722ED1', 111 | }, 112 | }, 113 | ], 114 | }; 115 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/demo.png -------------------------------------------------------------------------------- /demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/demo1.png -------------------------------------------------------------------------------- /demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/demo3.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | preset: 'jest-puppeteer', 4 | extraSetupFiles: ['./tests/setupTests.js'], 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mock/notices.js: -------------------------------------------------------------------------------- 1 | const getNotices = (req, res) => { 2 | res.json([ 3 | { 4 | id: '000000001', 5 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 6 | title: '你收到了 14 份新周报', 7 | datetime: '2017-08-09', 8 | type: 'notification', 9 | }, 10 | { 11 | id: '000000002', 12 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', 13 | title: '你推荐的 曲妮妮 已通过第三轮面试', 14 | datetime: '2017-08-08', 15 | type: 'notification', 16 | }, 17 | { 18 | id: '000000003', 19 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', 20 | title: '这种模板可以区分多种通知类型', 21 | datetime: '2017-08-07', 22 | read: true, 23 | type: 'notification', 24 | }, 25 | { 26 | id: '000000004', 27 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', 28 | title: '左侧图标用于区分不同的类型', 29 | datetime: '2017-08-07', 30 | type: 'notification', 31 | }, 32 | { 33 | id: '000000005', 34 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 35 | title: '内容不要超过两行字,超出时自动截断', 36 | datetime: '2017-08-07', 37 | type: 'notification', 38 | }, 39 | { 40 | id: '000000006', 41 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 42 | title: '曲丽丽 评论了你', 43 | description: '描述信息描述信息描述信息', 44 | datetime: '2017-08-07', 45 | type: 'message', 46 | clickClose: true, 47 | }, 48 | { 49 | id: '000000007', 50 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 51 | title: '朱偏右 回复了你', 52 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 53 | datetime: '2017-08-07', 54 | type: 'message', 55 | clickClose: true, 56 | }, 57 | { 58 | id: '000000008', 59 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 60 | title: '标题', 61 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 62 | datetime: '2017-08-07', 63 | type: 'message', 64 | clickClose: true, 65 | }, 66 | { 67 | id: '000000009', 68 | title: '任务名称', 69 | description: '任务需要在 2017-01-12 20:00 前启动', 70 | extra: '未开始', 71 | status: 'todo', 72 | type: 'event', 73 | }, 74 | { 75 | id: '000000010', 76 | title: '第三方紧急代码变更', 77 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 78 | extra: '马上到期', 79 | status: 'urgent', 80 | type: 'event', 81 | }, 82 | { 83 | id: '000000011', 84 | title: '信息安全考试', 85 | description: '指派竹尔于 2017-01-09 前完成更新并发布', 86 | extra: '已耗时 8 天', 87 | status: 'doing', 88 | type: 'event', 89 | }, 90 | { 91 | id: '000000012', 92 | title: 'ABCD 版本发布', 93 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 94 | extra: '进行中', 95 | status: 'processing', 96 | type: 'event', 97 | }, 98 | ]); 99 | }; 100 | 101 | export default { 102 | 'GET /api/notices': getNotices, 103 | }; 104 | -------------------------------------------------------------------------------- /mock/route.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '/api/auth_routes': { 3 | '/form/advanced-form': { 4 | authority: ['admin', 'user'], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | function getFakeCaptcha(req, res) { 2 | return res.json('captcha-xxx'); 3 | } // 代码中会兼容本地 service mock 以及部署站点的静态数据 4 | 5 | export default { 6 | // 支持值为 Object 和 Array 7 | 'GET /api/currentUser': { 8 | name: 'Serati Ma', 9 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', 10 | userid: '00000001', 11 | email: 'antdesign@alipay.com', 12 | signature: '海纳百川,有容乃大', 13 | title: '交互专家', 14 | group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', 15 | tags: [ 16 | { 17 | key: '0', 18 | label: '很有想法的', 19 | }, 20 | { 21 | key: '1', 22 | label: '专注设计', 23 | }, 24 | { 25 | key: '2', 26 | label: '辣~', 27 | }, 28 | { 29 | key: '3', 30 | label: '大长腿', 31 | }, 32 | { 33 | key: '4', 34 | label: '川妹子', 35 | }, 36 | { 37 | key: '5', 38 | label: '海纳百川', 39 | }, 40 | ], 41 | notifyCount: 12, 42 | unreadCount: 11, 43 | country: 'China', 44 | geographic: { 45 | province: { 46 | label: '浙江省', 47 | key: '330000', 48 | }, 49 | city: { 50 | label: '杭州市', 51 | key: '330100', 52 | }, 53 | }, 54 | address: '西湖区工专路 77 号', 55 | phone: '0752-268888888', 56 | }, 57 | // GET POST 可省略 58 | 'GET /api/users': [ 59 | { 60 | key: '1', 61 | name: 'John Brown', 62 | age: 32, 63 | address: 'New York No. 1 Lake Park', 64 | }, 65 | { 66 | key: '2', 67 | name: 'Jim Green', 68 | age: 42, 69 | address: 'London No. 1 Lake Park', 70 | }, 71 | { 72 | key: '3', 73 | name: 'Joe Black', 74 | age: 32, 75 | address: 'Sidney No. 1 Lake Park', 76 | }, 77 | ], 78 | 'POST /api/login/account': (req, res) => { 79 | const { password, userName, type } = req.body; 80 | 81 | if (password === 'ant.design' && userName === 'admin') { 82 | res.send({ 83 | status: 'ok', 84 | type, 85 | currentAuthority: 'admin', 86 | }); 87 | return; 88 | } 89 | 90 | if (password === 'ant.design' && userName === 'user') { 91 | res.send({ 92 | status: 'ok', 93 | type, 94 | currentAuthority: 'user', 95 | }); 96 | return; 97 | } 98 | 99 | res.send({ 100 | status: 'error', 101 | type, 102 | currentAuthority: 'guest', 103 | }); 104 | }, 105 | 'POST /api/register': (req, res) => { 106 | res.send({ 107 | status: 'ok', 108 | currentAuthority: 'user', 109 | }); 110 | }, 111 | 'GET /api/500': (req, res) => { 112 | res.status(500).send({ 113 | timestamp: 1513932555104, 114 | status: 500, 115 | error: 'error', 116 | message: 'error', 117 | path: '/base/category/list', 118 | }); 119 | }, 120 | 'GET /api/404': (req, res) => { 121 | res.status(404).send({ 122 | timestamp: 1513932643431, 123 | status: 404, 124 | error: 'Not Found', 125 | message: 'No message available', 126 | path: '/base/category/list/2121212', 127 | }); 128 | }, 129 | 'GET /api/403': (req, res) => { 130 | res.status(403).send({ 131 | timestamp: 1513932555104, 132 | status: 403, 133 | error: 'Unauthorized', 134 | message: 'Unauthorized', 135 | path: '/base/category/list', 136 | }); 137 | }, 138 | 'GET /api/401': (req, res) => { 139 | res.status(401).send({ 140 | timestamp: 1513932555104, 141 | status: 401, 142 | error: 'Unauthorized', 143 | message: 'Unauthorized', 144 | path: '/base/category/list', 145 | }); 146 | }, 147 | 'GET /api/login/captcha': getFakeCaptcha, 148 | }; 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ant-design-pro", 3 | "version": "4.0.0", 4 | "private": true, 5 | "description": "An out-of-box UI solution for enterprise applications", 6 | "scripts": { 7 | "analyze": "cross-env ANALYZE=1 umi build", 8 | "build": "umi build", 9 | "deploy": "npm run site && npm run gh-pages", 10 | "dev": "npm run start:dev", 11 | "fetch:blocks": "pro fetch-blocks && npm run prettier", 12 | "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist", 13 | "i18n-remove": "pro i18n-remove --locale=zh-CN --write", 14 | "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier", 15 | "lint-staged": "lint-staged", 16 | "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", 17 | "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style", 18 | "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", 19 | "lint:prettier": "prettier --check \"**/*\" --end-of-line auto", 20 | "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", 21 | "prettier": "prettier -c --write \"**/*\"", 22 | "start": "umi dev", 23 | "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev", 24 | "start:no-mock": "cross-env MOCK=none umi dev", 25 | "start:no-ui": "cross-env UMI_UI=none umi dev", 26 | "start:pre": "cross-env REACT_APP_ENV=pre MOCK=none umi dev", 27 | "start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev", 28 | "test": "umi test", 29 | "test:all": "node ./tests/run-tests.js", 30 | "test:component": "umi test ./src/components", 31 | "tsc": "tsc", 32 | "ui": "umi ui" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "npm run lint-staged" 37 | } 38 | }, 39 | "lint-staged": { 40 | "**/*.less": "stylelint --syntax less", 41 | "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", 42 | "**/*.{js,jsx,tsx,ts,less,md,json}": [ 43 | "prettier --write" 44 | ] 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions", 49 | "not ie <= 10" 50 | ], 51 | "dependencies": { 52 | "@ant-design/pro-layout": "^4.10.13", 53 | "@antv/data-set": "^0.11.0", 54 | "antd": "^3.23.6", 55 | "classnames": "^2.2.6", 56 | "dva": "^2.6.0-beta.16", 57 | "lodash": "^4.17.11", 58 | "moment": "^2.24.0", 59 | "omit.js": "^1.0.2", 60 | "path-to-regexp": "2.4.0", 61 | "qs": "^6.9.0", 62 | "react": "^16.8.6", 63 | "react-copy-to-clipboard": "^5.0.1", 64 | "react-dnd": "^11.1.1", 65 | "react-dnd-html5-backend": "^11.1.1", 66 | "react-dom": "^16.8.6", 67 | "react-helmet": "^5.2.1", 68 | "redux": "^4.0.1", 69 | "umi": "^2.13.0", 70 | "umi-plugin-antd-theme": "^1.0.1", 71 | "umi-plugin-pro-block": "^1.3.2", 72 | "umi-plugin-react": "^1.9.5", 73 | "umi-request": "^1.0.8" 74 | }, 75 | "devDependencies": { 76 | "@ant-design/pro-cli": "^1.0.18", 77 | "@types/classnames": "^2.2.7", 78 | "@types/express": "^4.17.0", 79 | "@types/history": "^4.7.2", 80 | "@types/jest": "^25.1.0", 81 | "@types/lodash": "^4.14.144", 82 | "@types/qs": "^6.5.3", 83 | "@types/react": "^16.8.19", 84 | "@types/react-dom": "^16.8.4", 85 | "@types/react-helmet": "^5.0.13", 86 | "@umijs/fabric": "^2.0.2", 87 | "chalk": "^3.0.0", 88 | "cross-env": "^7.0.0", 89 | "cross-port-killer": "^1.1.1", 90 | "enzyme": "^3.9.0", 91 | "express": "^4.17.1", 92 | "gh-pages": "^2.0.1", 93 | "husky": "^4.0.7", 94 | "jsdom-global": "^3.0.2", 95 | "lint-staged": "^10.0.0", 96 | "mockjs": "^1.0.1-beta3", 97 | "node-fetch": "^2.6.0", 98 | "prettier": "^1.19.1", 99 | "pro-download": "1.0.1", 100 | "stylelint": "^13.0.0", 101 | "umi-plugin-antd-icon-config": "^1.0.2", 102 | "umi-plugin-ga": "^1.1.3", 103 | "umi-plugin-pro": "^1.0.2", 104 | "umi-types": "^0.5.0" 105 | }, 106 | "engines": { 107 | "node": ">=10.0.0" 108 | }, 109 | "checkFiles": [ 110 | "src/**/*.js*", 111 | "src/**/*.ts*", 112 | "src/**/*.less", 113 | "config/**/*.js*", 114 | "scripts/**/*.js" 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/public/favicon.png -------------------------------------------------------------------------------- /public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/public/home_bg.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ix1024/antd-design-pro-tabs/c26f2b47219de3830fed139f1bc78bc03b842fb0/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/pro_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Group 28 Copy 5Created with Sketch. -------------------------------------------------------------------------------- /src/components/Authorized/Authorized.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | import check from './CheckPermissions'; 4 | 5 | const Authorized = ({ 6 | children, 7 | authority, 8 | noMatch = ( 9 | 14 | ), 15 | }) => { 16 | const childrenRender = typeof children === 'undefined' ? null : children; 17 | const dom = check(authority, childrenRender, noMatch); 18 | return <>{dom}; 19 | }; 20 | 21 | export default Authorized; 22 | -------------------------------------------------------------------------------- /src/components/Authorized/AuthorizedRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | import React from 'react'; 3 | import Authorized from './Authorized'; 4 | 5 | const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => ( 6 | ( 12 | 17 | )} 18 | /> 19 | } 20 | > 21 | (Component ? : render(props))} /> 22 | 23 | ); 24 | 25 | export default AuthorizedRoute; 26 | -------------------------------------------------------------------------------- /src/components/Authorized/CheckPermissions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; // eslint-disable-next-line import/no-cycle 3 | 4 | import PromiseRender from './PromiseRender'; 5 | 6 | /** 7 | * 通用权限检查方法 8 | * Common check permissions method 9 | * @param { 权限判定 | Permission judgment } authority 10 | * @param { 你的权限 | Your permission description } currentAuthority 11 | * @param { 通过的组件 | Passing components } target 12 | * @param { 未通过的组件 | no pass components } Exception 13 | */ 14 | const checkPermissions = (authority, currentAuthority, target, Exception) => { 15 | // 没有判定权限.默认查看所有 16 | // Retirement authority, return target; 17 | if (!authority) { 18 | return target; 19 | } // 数组处理 20 | 21 | if (Array.isArray(authority)) { 22 | if (Array.isArray(currentAuthority)) { 23 | if (currentAuthority.some(item => authority.includes(item))) { 24 | return target; 25 | } 26 | } else if (authority.includes(currentAuthority)) { 27 | return target; 28 | } 29 | 30 | return Exception; 31 | } // string 处理 32 | 33 | if (typeof authority === 'string') { 34 | if (Array.isArray(currentAuthority)) { 35 | if (currentAuthority.some(item => authority === item)) { 36 | return target; 37 | } 38 | } else if (authority === currentAuthority) { 39 | return target; 40 | } 41 | 42 | return Exception; 43 | } // Promise 处理 44 | 45 | if (authority instanceof Promise) { 46 | return ; 47 | } // Function 处理 48 | 49 | if (typeof authority === 'function') { 50 | try { 51 | const bool = authority(currentAuthority); // 函数执行后返回值是 Promise 52 | 53 | if (bool instanceof Promise) { 54 | return ; 55 | } 56 | 57 | if (bool) { 58 | return target; 59 | } 60 | 61 | return Exception; 62 | } catch (error) { 63 | throw error; 64 | } 65 | } 66 | 67 | throw new Error('unsupported parameters'); 68 | }; 69 | 70 | export { checkPermissions }; 71 | 72 | function check(authority, target, Exception) { 73 | return checkPermissions(authority, CURRENT, target, Exception); 74 | } 75 | 76 | export default check; 77 | -------------------------------------------------------------------------------- /src/components/Authorized/PromiseRender.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; // eslint-disable-next-line import/no-cycle 5 | 6 | export default class PromiseRender extends React.Component { 7 | state = { 8 | component: () => null, 9 | }; 10 | 11 | componentDidMount() { 12 | this.setRenderComponent(this.props); 13 | } 14 | 15 | shouldComponentUpdate = (nextProps, nextState) => { 16 | const { component } = this.state; 17 | 18 | if (!isEqual(nextProps, this.props)) { 19 | this.setRenderComponent(nextProps); 20 | } 21 | 22 | if (nextState.component !== component) return true; 23 | return false; 24 | }; // set render Component : ok or error 25 | 26 | setRenderComponent(props) { 27 | const ok = this.checkIsInstantiation(props.ok); 28 | const error = this.checkIsInstantiation(props.error); 29 | props.promise 30 | .then(() => { 31 | this.setState({ 32 | component: ok, 33 | }); 34 | return true; 35 | }) 36 | .catch(() => { 37 | this.setState({ 38 | component: error, 39 | }); 40 | }); 41 | } // Determine whether the incoming component has been instantiated 42 | // AuthorizedRoute is already instantiated 43 | // Authorized render is already instantiated, children is no instantiated 44 | // Secured is not instantiated 45 | 46 | checkIsInstantiation = target => { 47 | if (isComponentClass(target)) { 48 | const Target = target; 49 | return props => ; 50 | } 51 | 52 | if (React.isValidElement(target)) { 53 | return props => React.cloneElement(target, props); 54 | } 55 | 56 | return () => target; 57 | }; 58 | 59 | render() { 60 | const { component: Component } = this.state; 61 | const { ok, error, promise, ...rest } = this.props; 62 | return Component ? ( 63 | 64 | ) : ( 65 |
74 | 75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Authorized/Secured.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckPermissions from './CheckPermissions'; 3 | /** 4 | * 默认不能访问任何页面 5 | * default is "NULL" 6 | */ 7 | 8 | const Exception403 = () => 403; 9 | 10 | export const isComponentClass = component => { 11 | if (!component) return false; 12 | const proto = Object.getPrototypeOf(component); 13 | if (proto === React.Component || proto === Function.prototype) return true; 14 | return isComponentClass(proto); 15 | }; // Determine whether the incoming component has been instantiated 16 | // AuthorizedRoute is already instantiated 17 | // Authorized render is already instantiated, children is no instantiated 18 | // Secured is not instantiated 19 | 20 | const checkIsInstantiation = target => { 21 | if (isComponentClass(target)) { 22 | const Target = target; 23 | return props => ; 24 | } 25 | 26 | if (React.isValidElement(target)) { 27 | return props => React.cloneElement(target, props); 28 | } 29 | 30 | return () => target; 31 | }; 32 | /** 33 | * 用于判断是否拥有权限访问此 view 权限 34 | * authority 支持传入 string, () => boolean | Promise 35 | * e.g. 'user' 只有 user 用户能访问 36 | * e.g. 'user,admin' user 和 admin 都能访问 37 | * e.g. ()=>boolean 返回true能访问,返回false不能访问 38 | * e.g. Promise then 能访问 catch不能访问 39 | * e.g. authority support incoming string, () => boolean | Promise 40 | * e.g. 'user' only user user can access 41 | * e.g. 'user, admin' user and admin can access 42 | * e.g. () => boolean true to be able to visit, return false can not be accessed 43 | * e.g. Promise then can not access the visit to catch 44 | * @param {string | function | Promise} authority 45 | * @param {ReactNode} error 非必需参数 46 | */ 47 | 48 | const authorize = (authority, error) => { 49 | /** 50 | * conversion into a class 51 | * 防止传入字符串时找不到staticContext造成报错 52 | * String parameters can cause staticContext not found error 53 | */ 54 | let classError = false; 55 | 56 | if (error) { 57 | classError = () => error; 58 | } 59 | 60 | if (!authority) { 61 | throw new Error('authority is required'); 62 | } 63 | 64 | return function decideAuthority(target) { 65 | const component = CheckPermissions(authority, target, classError || Exception403); 66 | return checkIsInstantiation(component); 67 | }; 68 | }; 69 | 70 | export default authorize; 71 | -------------------------------------------------------------------------------- /src/components/Authorized/index.jsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import Secured from './Secured'; 3 | import check from './CheckPermissions'; 4 | import renderAuthorize from './renderAuthorize'; 5 | 6 | Authorized.Secured = Secured; 7 | Authorized.check = check; 8 | const RenderAuthorize = renderAuthorize(Authorized); 9 | export default RenderAuthorize; 10 | -------------------------------------------------------------------------------- /src/components/Authorized/renderAuthorize.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | 3 | /* eslint-disable import/no-mutable-exports */ 4 | let CURRENT = 'NULL'; 5 | 6 | /** 7 | * use authority or getAuthority 8 | * @param {string|()=>String} currentAuthority 9 | */ 10 | const renderAuthorize = Authorized => currentAuthority => { 11 | if (currentAuthority) { 12 | if (typeof currentAuthority === 'function') { 13 | CURRENT = currentAuthority(); 14 | } 15 | 16 | if ( 17 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 18 | Array.isArray(currentAuthority) 19 | ) { 20 | CURRENT = currentAuthority; 21 | } 22 | } else { 23 | CURRENT = 'NULL'; 24 | } 25 | 26 | return Authorized; 27 | }; 28 | 29 | export { CURRENT }; 30 | export default Authorized => renderAuthorize(Authorized); 31 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/AvatarDropdown.jsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Icon, Menu, Spin } from 'antd'; 2 | import { FormattedMessage } from 'umi-plugin-react/locale'; 3 | import React from 'react'; 4 | import { connect } from 'dva'; 5 | import { router } from 'umi'; 6 | import HeaderDropdown from '../HeaderDropdown'; 7 | import styles from './index.less'; 8 | 9 | class AvatarDropdown extends React.Component { 10 | onMenuClick = event => { 11 | const { key } = event; 12 | 13 | if (key === 'logout') { 14 | const { dispatch } = this.props; 15 | 16 | if (dispatch) { 17 | dispatch({ 18 | type: 'login/logout', 19 | }); 20 | } 21 | 22 | return; 23 | } 24 | 25 | router.push(`/account/${key}`); 26 | }; 27 | 28 | render() { 29 | const { 30 | currentUser = { 31 | avatar: '', 32 | name: '', 33 | }, 34 | menu, 35 | } = this.props; 36 | const menuHeaderDropdown = ( 37 | 38 | {menu && ( 39 | 40 | 41 | 42 | 43 | )} 44 | {menu && ( 45 | 46 | 47 | 48 | 49 | )} 50 | {menu && } 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | return currentUser && currentUser.name ? ( 59 | 60 | 61 | 62 | {currentUser.name} 63 | 64 | 65 | ) : ( 66 | 73 | ); 74 | } 75 | } 76 | 77 | export default connect(({ user }) => ({ 78 | currentUser: user.currentUser, 79 | }))(AvatarDropdown); 80 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/NoticeIconView.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Tag, message } from 'antd'; 3 | import { connect } from 'dva'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | import groupBy from 'lodash/groupBy'; 6 | import moment from 'moment'; 7 | import NoticeIcon from '../NoticeIcon'; 8 | import styles from './index.less'; 9 | 10 | class GlobalHeaderRight extends Component { 11 | componentDidMount() { 12 | const { dispatch } = this.props; 13 | 14 | if (dispatch) { 15 | dispatch({ 16 | type: 'global/fetchNotices', 17 | }); 18 | } 19 | } 20 | 21 | changeReadState = clickedItem => { 22 | const { id } = clickedItem; 23 | const { dispatch } = this.props; 24 | 25 | if (dispatch) { 26 | dispatch({ 27 | type: 'global/changeNoticeReadState', 28 | payload: id, 29 | }); 30 | } 31 | }; 32 | 33 | handleNoticeClear = (title, key) => { 34 | const { dispatch } = this.props; 35 | message.success( 36 | `${formatMessage({ 37 | id: 'component.noticeIcon.cleared', 38 | })} ${title}`, 39 | ); 40 | 41 | if (dispatch) { 42 | dispatch({ 43 | type: 'global/clearNotices', 44 | payload: key, 45 | }); 46 | } 47 | }; 48 | 49 | getNoticeData = () => { 50 | const { notices = [] } = this.props; 51 | 52 | if (notices.length === 0) { 53 | return {}; 54 | } 55 | 56 | const newNotices = notices.map(notice => { 57 | const newNotice = { ...notice }; 58 | 59 | if (newNotice.datetime) { 60 | newNotice.datetime = moment(notice.datetime).fromNow(); 61 | } 62 | 63 | if (newNotice.id) { 64 | newNotice.key = newNotice.id; 65 | } 66 | 67 | if (newNotice.extra && newNotice.status) { 68 | const color = { 69 | todo: '', 70 | processing: 'blue', 71 | urgent: 'red', 72 | doing: 'gold', 73 | }[newNotice.status]; 74 | newNotice.extra = ( 75 | 81 | {newNotice.extra} 82 | 83 | ); 84 | } 85 | 86 | return newNotice; 87 | }); 88 | return groupBy(newNotices, 'type'); 89 | }; 90 | 91 | getUnreadData = noticeData => { 92 | const unreadMsg = {}; 93 | Object.keys(noticeData).forEach(key => { 94 | const value = noticeData[key]; 95 | 96 | if (!unreadMsg[key]) { 97 | unreadMsg[key] = 0; 98 | } 99 | 100 | if (Array.isArray(value)) { 101 | unreadMsg[key] = value.filter(item => !item.read).length; 102 | } 103 | }); 104 | return unreadMsg; 105 | }; 106 | 107 | render() { 108 | const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; 109 | const noticeData = this.getNoticeData(); 110 | const unreadMsg = this.getUnreadData(noticeData); 111 | return ( 112 | { 116 | this.changeReadState(item); 117 | }} 118 | loading={fetchingNotices} 119 | clearText={formatMessage({ 120 | id: 'component.noticeIcon.clear', 121 | })} 122 | viewMoreText={formatMessage({ 123 | id: 'component.noticeIcon.view-more', 124 | })} 125 | onClear={this.handleNoticeClear} 126 | onPopupVisibleChange={onNoticeVisibleChange} 127 | onViewMore={() => message.info('Click on view more')} 128 | clearClose 129 | > 130 | 142 | 154 | 166 | 167 | ); 168 | } 169 | } 170 | 171 | export default connect(({ user, global, loading }) => ({ 172 | currentUser: user.currentUser, 173 | collapsed: global.collapsed, 174 | fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], 175 | fetchingNotices: loading.effects['global/fetchNotices'], 176 | notices: global.notices, 177 | }))(GlobalHeaderRight); 178 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.jsx: -------------------------------------------------------------------------------- 1 | import { Icon, Tooltip, Tag } from 'antd'; 2 | import React from 'react'; 3 | import { connect } from 'dva'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | import Avatar from './AvatarDropdown'; 6 | import HeaderSearch from '../HeaderSearch'; 7 | import SelectLang from '../SelectLang'; 8 | import styles from './index.less'; 9 | 10 | const ENVTagColor = { 11 | dev: 'orange', 12 | test: 'green', 13 | pre: '#87d068', 14 | }; 15 | 16 | const GlobalHeaderRight = props => { 17 | const { theme, layout } = props; 18 | let className = styles.right; 19 | 20 | if (theme === 'dark' && layout === 'topmenu') { 21 | className = `${styles.right} ${styles.dark}`; 22 | } 23 | 24 | return ( 25 |
26 | {}} 44 | onPressEnter={() => {}} 45 | /> 46 | 51 | 57 | 58 | 59 | 60 | 61 | {REACT_APP_ENV && {REACT_APP_ENV}} 62 | 63 |
64 | ); 65 | }; 66 | 67 | export default connect(({ settings }) => ({ 68 | theme: settings.navTheme, 69 | layout: settings.layout, 70 | }))(GlobalHeaderRight); 71 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | float: right; 16 | height: @layout-header-height; 17 | margin-left: auto; 18 | overflow: hidden; 19 | .action { 20 | display: inline-block; 21 | height: 100%; 22 | padding: 0 12px; 23 | cursor: pointer; 24 | transition: all 0.3s; 25 | > i { 26 | color: @text-color; 27 | vertical-align: middle; 28 | } 29 | &:hover { 30 | background: @pro-header-hover-bg; 31 | } 32 | &:global(.opened) { 33 | background: @pro-header-hover-bg; 34 | } 35 | } 36 | .search { 37 | padding: 0 12px; 38 | &:hover { 39 | background: transparent; 40 | } 41 | } 42 | .account { 43 | .avatar { 44 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 45 | margin-right: 8px; 46 | color: @primary-color; 47 | vertical-align: top; 48 | background: rgba(255, 255, 255, 0.85); 49 | } 50 | } 51 | } 52 | 53 | .dark { 54 | .action { 55 | color: rgba(255, 255, 255, 0.85); 56 | > i { 57 | color: rgba(255, 255, 255, 0.85); 58 | } 59 | &:hover, 60 | &:global(.opened) { 61 | background: @primary-color; 62 | } 63 | } 64 | } 65 | 66 | :global(.ant-pro-global-header) { 67 | .dark { 68 | .action { 69 | color: @text-color; 70 | > i { 71 | color: @text-color; 72 | } 73 | &:hover { 74 | color: rgba(255, 255, 255, 0.85); 75 | > i { 76 | color: rgba(255, 255, 255, 0.85); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | @media only screen and (max-width: @screen-md) { 84 | :global(.ant-divider-vertical) { 85 | vertical-align: unset; 86 | } 87 | .name { 88 | display: none; 89 | } 90 | .right { 91 | position: absolute; 92 | top: 0; 93 | right: 12px; 94 | .account { 95 | .avatar { 96 | margin-right: 0; 97 | } 98 | } 99 | .search { 100 | display: none; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.jsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'antd'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './index.less'; 5 | 6 | const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => ( 7 | 8 | ); 9 | 10 | export default HeaderDropdown; 11 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.jsx: -------------------------------------------------------------------------------- 1 | import { AutoComplete, Icon, Input } from 'antd'; 2 | import React, { Component } from 'react'; 3 | import classNames from 'classnames'; 4 | import debounce from 'lodash/debounce'; 5 | import styles from './index.less'; 6 | 7 | export default class HeaderSearch extends Component { 8 | inputRef = null; 9 | 10 | static defaultProps = { 11 | defaultActiveFirstOption: false, 12 | onPressEnter: () => {}, 13 | onSearch: () => {}, 14 | onChange: () => {}, 15 | className: '', 16 | placeholder: '', 17 | dataSource: [], 18 | defaultOpen: false, 19 | onVisibleChange: () => {}, 20 | }; 21 | 22 | static getDerivedStateFromProps(props) { 23 | if ('open' in props) { 24 | return { 25 | searchMode: props.open, 26 | }; 27 | } 28 | 29 | return null; 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | searchMode: props.defaultOpen, 36 | value: props.defaultValue, 37 | }; 38 | this.debouncePressEnter = debounce(this.debouncePressEnter, 500, { 39 | leading: true, 40 | trailing: false, 41 | }); 42 | } 43 | 44 | onKeyDown = e => { 45 | if (e.key === 'Enter') { 46 | this.debouncePressEnter(); 47 | } 48 | }; 49 | 50 | onChange = value => { 51 | if (typeof value === 'string') { 52 | const { onSearch, onChange } = this.props; 53 | this.setState({ 54 | value, 55 | }); 56 | 57 | if (onSearch) { 58 | onSearch(value); 59 | } 60 | 61 | if (onChange) { 62 | onChange(value); 63 | } 64 | } 65 | }; 66 | 67 | enterSearchMode = () => { 68 | const { onVisibleChange } = this.props; 69 | onVisibleChange(true); 70 | this.setState( 71 | { 72 | searchMode: true, 73 | }, 74 | () => { 75 | const { searchMode } = this.state; 76 | 77 | if (searchMode && this.inputRef) { 78 | this.inputRef.focus(); 79 | } 80 | }, 81 | ); 82 | }; 83 | 84 | leaveSearchMode = () => { 85 | this.setState({ 86 | searchMode: false, 87 | }); 88 | }; 89 | 90 | debouncePressEnter = () => { 91 | const { onPressEnter } = this.props; 92 | const { value } = this.state; 93 | onPressEnter(value || ''); 94 | }; 95 | 96 | render() { 97 | const { className, defaultValue, placeholder, open, ...restProps } = this.props; 98 | const { searchMode, value } = this.state; 99 | delete restProps.defaultOpen; // for rc-select not affected 100 | 101 | const inputClass = classNames(styles.input, { 102 | [styles.show]: searchMode, 103 | }); 104 | return ( 105 | { 109 | if (propertyName === 'width' && !searchMode) { 110 | const { onVisibleChange } = this.props; 111 | onVisibleChange(searchMode); 112 | } 113 | }} 114 | > 115 | 116 | 123 | { 125 | this.inputRef = node; 126 | }} 127 | defaultValue={defaultValue} 128 | aria-label={placeholder} 129 | placeholder={placeholder} 130 | onKeyDown={this.onKeyDown} 131 | onBlur={this.leaveSearchMode} 132 | /> 133 | 134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | :global(.anticon-search) { 5 | font-size: 16px; 6 | cursor: pointer; 7 | } 8 | .input { 9 | width: 0; 10 | background: transparent; 11 | border-radius: 0; 12 | transition: width 0.3s, margin-left 0.3s; 13 | :global(.ant-select-selection) { 14 | background: transparent; 15 | } 16 | input { 17 | padding-right: 0; 18 | padding-left: 0; 19 | border: 0; 20 | box-shadow: none !important; 21 | } 22 | &, 23 | &:hover, 24 | &:focus { 25 | border-bottom: 1px solid @border-color-base; 26 | } 27 | &.show { 28 | width: 210px; 29 | margin-left: 8px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/NoticeList.jsx: -------------------------------------------------------------------------------- 1 | import { Avatar, List } from 'antd'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './NoticeList.less'; 5 | 6 | const NoticeList = ({ 7 | data = [], 8 | onClick, 9 | onClear, 10 | title, 11 | onViewMore, 12 | emptyText, 13 | showClear = true, 14 | clearText, 15 | viewMoreText, 16 | showViewMore = false, 17 | }) => { 18 | if (data.length === 0) { 19 | return ( 20 |
21 | not found 25 |
{emptyText}
26 |
27 | ); 28 | } 29 | 30 | return ( 31 |
32 | { 36 | const itemCls = classNames(styles.item, { 37 | [styles.read]: item.read, 38 | }); // eslint-disable-next-line no-nested-ternary 39 | 40 | const leftIcon = item.avatar ? ( 41 | typeof item.avatar === 'string' ? ( 42 | 43 | ) : ( 44 | {item.avatar} 45 | ) 46 | ) : null; 47 | return ( 48 | onClick && onClick(item)} 52 | > 53 | 58 | {item.title} 59 |
{item.extra}
60 |
61 | } 62 | description={ 63 |
64 |
{item.description}
65 |
{item.datetime}
66 |
67 | } 68 | /> 69 | 70 | ); 71 | }} 72 | /> 73 |
74 | {showClear ? ( 75 |
76 | {clearText} {title} 77 |
78 | ) : null} 79 | {showViewMore ? ( 80 |
{ 82 | if (onViewMore) { 83 | onViewMore(e); 84 | } 85 | }} 86 | > 87 | {viewMoreText} 88 |
89 | ) : null} 90 |
91 | 92 | ); 93 | }; 94 | 95 | export default NoticeList; 96 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/NoticeList.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .list { 4 | max-height: 400px; 5 | overflow: auto; 6 | &::-webkit-scrollbar { 7 | display: none; 8 | } 9 | .item { 10 | padding-right: 24px; 11 | padding-left: 24px; 12 | overflow: hidden; 13 | cursor: pointer; 14 | transition: all 0.3s; 15 | 16 | .meta { 17 | width: 100%; 18 | } 19 | 20 | .avatar { 21 | margin-top: 4px; 22 | background: #fff; 23 | } 24 | .iconElement { 25 | font-size: 32px; 26 | } 27 | 28 | &.read { 29 | opacity: 0.4; 30 | } 31 | &:last-child { 32 | border-bottom: 0; 33 | } 34 | &:hover { 35 | background: @primary-1; 36 | } 37 | .title { 38 | margin-bottom: 8px; 39 | font-weight: normal; 40 | } 41 | .description { 42 | font-size: 12px; 43 | line-height: @line-height-base; 44 | } 45 | .datetime { 46 | margin-top: 4px; 47 | font-size: 12px; 48 | line-height: @line-height-base; 49 | } 50 | .extra { 51 | float: right; 52 | margin-top: -1.5px; 53 | margin-right: 0; 54 | color: @text-color-secondary; 55 | font-weight: normal; 56 | } 57 | } 58 | .loadMore { 59 | padding: 8px 0; 60 | color: @primary-6; 61 | text-align: center; 62 | cursor: pointer; 63 | &.loadedAll { 64 | color: rgba(0, 0, 0, 0.25); 65 | cursor: unset; 66 | } 67 | } 68 | } 69 | 70 | .notFound { 71 | padding: 73px 0 88px; 72 | color: @text-color-secondary; 73 | text-align: center; 74 | img { 75 | display: inline-block; 76 | height: 76px; 77 | margin-bottom: 16px; 78 | } 79 | } 80 | 81 | .bottomBar { 82 | height: 46px; 83 | color: @text-color; 84 | line-height: 46px; 85 | text-align: center; 86 | border-top: 1px solid @border-color-split; 87 | border-radius: 0 0 @border-radius-base @border-radius-base; 88 | transition: all 0.3s; 89 | div { 90 | display: inline-block; 91 | width: 50%; 92 | cursor: pointer; 93 | transition: all 0.3s; 94 | user-select: none; 95 | 96 | &:only-child { 97 | width: 100%; 98 | } 99 | &:not(:only-child):last-child { 100 | border-left: 1px solid @border-color-split; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import { Badge, Icon, Spin, Tabs } from 'antd'; 2 | import React, { Component } from 'react'; 3 | import classNames from 'classnames'; 4 | import NoticeList from './NoticeList'; 5 | import HeaderDropdown from '../HeaderDropdown'; 6 | import styles from './index.less'; 7 | 8 | const { TabPane } = Tabs; 9 | export default class NoticeIcon extends Component { 10 | static Tab = NoticeList; 11 | 12 | static defaultProps = { 13 | onItemClick: () => {}, 14 | onPopupVisibleChange: () => {}, 15 | onTabChange: () => {}, 16 | onClear: () => {}, 17 | onViewMore: () => {}, 18 | loading: false, 19 | clearClose: false, 20 | emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', 21 | }; 22 | 23 | state = { 24 | visible: false, 25 | }; 26 | 27 | onItemClick = (item, tabProps) => { 28 | const { onItemClick } = this.props; 29 | 30 | if (onItemClick) { 31 | onItemClick(item, tabProps); 32 | } 33 | }; 34 | 35 | onClear = (name, key) => { 36 | const { onClear } = this.props; 37 | 38 | if (onClear) { 39 | onClear(name, key); 40 | } 41 | }; 42 | 43 | onTabChange = tabType => { 44 | const { onTabChange } = this.props; 45 | 46 | if (onTabChange) { 47 | onTabChange(tabType); 48 | } 49 | }; 50 | 51 | onViewMore = (tabProps, event) => { 52 | const { onViewMore } = this.props; 53 | 54 | if (onViewMore) { 55 | onViewMore(tabProps, event); 56 | } 57 | }; 58 | 59 | getNotificationBox() { 60 | const { children, loading, clearText, viewMoreText } = this.props; 61 | 62 | if (!children) { 63 | return null; 64 | } 65 | 66 | const panes = []; 67 | React.Children.forEach(children, child => { 68 | if (!child) { 69 | return; 70 | } 71 | 72 | const { list, title, count, tabKey, showClear, showViewMore } = child.props; 73 | const len = list && list.length ? list.length : 0; 74 | const msgCount = count || count === 0 ? count : len; 75 | const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title; 76 | panes.push( 77 | 78 | this.onClear(title, tabKey)} 83 | onClick={item => this.onItemClick(item, child.props)} 84 | onViewMore={event => this.onViewMore(child.props, event)} 85 | showClear={showClear} 86 | showViewMore={showViewMore} 87 | title={title} 88 | {...child.props} 89 | /> 90 | , 91 | ); 92 | }); 93 | return ( 94 | <> 95 | 96 | 97 | {panes} 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | handleVisibleChange = visible => { 105 | const { onPopupVisibleChange } = this.props; 106 | this.setState({ 107 | visible, 108 | }); 109 | 110 | if (onPopupVisibleChange) { 111 | onPopupVisibleChange(visible); 112 | } 113 | }; 114 | 115 | render() { 116 | const { className, count, popupVisible, bell } = this.props; 117 | const { visible } = this.state; 118 | const noticeButtonClass = classNames(className, styles.noticeButton); 119 | const notificationBox = this.getNotificationBox(); 120 | const NoticeBellIcon = bell || ; 121 | const trigger = ( 122 | 127 | 134 | {NoticeBellIcon} 135 | 136 | 137 | ); 138 | 139 | if (!notificationBox) { 140 | return trigger; 141 | } 142 | 143 | const popoverProps = {}; 144 | 145 | if ('popupVisible' in this.props) { 146 | popoverProps.visible = popupVisible; 147 | } 148 | 149 | return ( 150 | 159 | {trigger} 160 | 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .popover { 4 | position: relative; 5 | width: 336px; 6 | } 7 | 8 | .noticeButton { 9 | display: inline-block; 10 | cursor: pointer; 11 | transition: all 0.3s; 12 | } 13 | .icon { 14 | padding: 4px; 15 | vertical-align: middle; 16 | } 17 | 18 | .badge { 19 | font-size: 16px; 20 | } 21 | 22 | .tabs { 23 | :global { 24 | .ant-tabs-nav-scroll { 25 | text-align: center; 26 | } 27 | .ant-tabs-bar { 28 | margin-bottom: 0; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.jsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '@ant-design/pro-layout'; // loading components from code split 2 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 3 | 4 | export default PageLoading; 5 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.jsx: -------------------------------------------------------------------------------- 1 | import { Icon, Menu } from 'antd'; 2 | import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import HeaderDropdown from '../HeaderDropdown'; 6 | import styles from './index.less'; 7 | 8 | const SelectLang = props => { 9 | const { className } = props; 10 | const selectedLang = getLocale(); 11 | 12 | const changeLang = ({ key }) => setLocale(key); 13 | 14 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; 15 | const languageLabels = { 16 | 'zh-CN': '简体中文', 17 | 'zh-TW': '繁体中文', 18 | 'en-US': 'English', 19 | 'pt-BR': 'Português', 20 | }; 21 | const languageIcons = { 22 | 'zh-CN': '🇨🇳', 23 | 'zh-TW': '🇭🇰', 24 | 'en-US': '🇺🇸', 25 | 'pt-BR': '🇧🇷', 26 | }; 27 | const langMenu = ( 28 | 29 | {locales.map(locale => ( 30 | 31 | 32 | {languageIcons[locale]} 33 | {' '} 34 | {languageLabels[locale]} 35 | 36 | ))} 37 | 38 | ); 39 | return ( 40 | 41 | 42 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default SelectLang; 54 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .menu { 4 | :global(.anticon) { 5 | margin-right: 8px; 6 | } 7 | :global(.ant-dropdown-menu-item) { 8 | min-width: 160px; 9 | } 10 | } 11 | 12 | .dropDown { 13 | line-height: @layout-header-height; 14 | vertical-align: top; 15 | cursor: pointer; 16 | > i { 17 | font-size: 16px !important; 18 | transform: none !important; 19 | svg { 20 | position: relative; 21 | top: -1px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('lodash'); 2 | const RouterConfig = require('../../config/config').default.routes; 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach(item => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter(item => !!item)); 20 | } 21 | 22 | beforeAll(async () => { 23 | await page.goto(`${BASE_URL}`); 24 | await page.evaluate(() => { 25 | localStorage.setItem('antd-pro-authority', '["admin"]'); 26 | }); 27 | }); 28 | 29 | describe('Ant Design Pro E2E test', () => { 30 | const testPage = path => async () => { 31 | await page.goto(`${BASE_URL}${path}`); 32 | await page.waitForSelector('footer', { 33 | timeout: 2000, 34 | }); 35 | const haveFooter = await page.evaluate( 36 | () => document.getElementsByTagName('footer').length > 0, 37 | ); 38 | expect(haveFooter).toBeTruthy(); 39 | }; 40 | 41 | const routers = formatter(RouterConfig); 42 | routers.forEach(route => { 43 | it(`test pages ${route}`, testPage(route)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/e2e/topMenu.e2e.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 2 | 3 | describe('Homepage', () => { 4 | it('topmenu should have footer', async () => { 5 | const params = '?navTheme=light&layout=topmenu'; 6 | await page.goto(`${BASE_URL}${params}`); 7 | await page.waitForSelector('footer', { 8 | timeout: 2000, 9 | }); 10 | const haveFooter = await page.evaluate( 11 | () => document.getElementsByTagName('footer').length > 0, 12 | ); 13 | expect(haveFooter).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/global.jsx: -------------------------------------------------------------------------------- 1 | import { Button, message, notification } from 'antd'; 2 | import React from 'react'; 3 | import { formatMessage } from 'umi-plugin-react/locale'; 4 | import defaultSettings from '../config/defaultSettings'; 5 | 6 | const { pwa } = defaultSettings; // if pwa is true 7 | 8 | if (pwa) { 9 | // Notify user if offline now 10 | window.addEventListener('sw.offline', () => { 11 | message.warning( 12 | formatMessage({ 13 | id: 'app.pwa.offline', 14 | }), 15 | ); 16 | }); // Pop up a prompt on the page asking the user if they want to use the latest version 17 | 18 | window.addEventListener('sw.updated', event => { 19 | const e = event; 20 | 21 | const reloadSW = async () => { 22 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 23 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 24 | const worker = e.detail && e.detail.waiting; 25 | 26 | if (!worker) { 27 | return true; 28 | } // Send skip-waiting event to waiting SW with MessageChannel 29 | 30 | await new Promise((resolve, reject) => { 31 | const channel = new MessageChannel(); 32 | 33 | channel.port1.onmessage = msgEvent => { 34 | if (msgEvent.data.error) { 35 | reject(msgEvent.data.error); 36 | } else { 37 | resolve(msgEvent.data); 38 | } 39 | }; 40 | 41 | worker.postMessage( 42 | { 43 | type: 'skip-waiting', 44 | }, 45 | [channel.port2], 46 | ); 47 | }); // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 48 | 49 | window.location.reload(true); 50 | return true; 51 | }; 52 | 53 | const key = `open${Date.now()}`; 54 | const btn = ( 55 | 66 | ); 67 | notification.open({ 68 | message: formatMessage({ 69 | id: 'app.pwa.serviceworker.updated', 70 | }), 71 | description: formatMessage({ 72 | id: 'app.pwa.serviceworker.updated.hint', 73 | }), 74 | btn, 75 | key, 76 | onClose: async () => {}, 77 | }); 78 | }); 79 | } else if ('serviceWorker' in navigator) { 80 | // unregister service worker 81 | const { serviceWorker } = navigator; 82 | 83 | if (serviceWorker.getRegistrations) { 84 | serviceWorker.getRegistrations().then(sws => { 85 | sws.forEach(sw => { 86 | sw.unregister(); 87 | }); 88 | }); 89 | } 90 | 91 | serviceWorker.getRegistration().then(sw => { 92 | if (sw) sw.unregister(); 93 | }); // remove all caches 94 | 95 | if (window.caches && window.caches.keys) { 96 | caches.keys().then(keys => { 97 | keys.forEach(key => { 98 | caches.delete(key); 99 | }); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/global.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | .colorWeak { 10 | filter: invert(80%); 11 | } 12 | 13 | .ant-layout { 14 | min-height: 100vh; 15 | } 16 | 17 | canvas { 18 | display: block; 19 | } 20 | 21 | body { 22 | text-rendering: optimizeLegibility; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | ul, 28 | ol { 29 | list-style: none; 30 | } 31 | 32 | @media (max-width: @screen-xs) { 33 | .ant-table { 34 | width: 100%; 35 | overflow-x: auto; 36 | &-thead > tr, 37 | &-tbody > tr { 38 | > th, 39 | > td { 40 | white-space: pre; 41 | > span { 42 | display: block; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | // 兼容IE11 50 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) { 51 | body .ant-design-pro > .ant-layout { 52 | min-height: 100vh; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout. 3 | * You can view component api by: 4 | * https://github.com/ant-design/ant-design-pro-layout 5 | */ 6 | import ProLayout, { DefaultFooter } from '@ant-design/pro-layout'; 7 | import React, { useEffect } from 'react'; 8 | import { Link } from 'umi'; 9 | import { connect } from 'dva'; 10 | import { Icon, Result, Button } from 'antd'; 11 | import { formatMessage } from 'umi-plugin-react/locale'; 12 | import Authorized from '@/utils/Authorized'; 13 | import RightContent from '@/components/GlobalHeader/RightContent'; 14 | import { isAntDesignPro, getAuthorityFromRouter } from '@/utils/utils'; 15 | import logo from '../assets/logo.svg'; 16 | import PageTab from './PageTab'; 17 | 18 | const noMatch = ( 19 | 25 | Go Login 26 | 27 | } 28 | /> 29 | ); 30 | 31 | /** 32 | * use Authorized check all menu item 33 | */ 34 | const menuDataRender = menuList => 35 | menuList.map(item => { 36 | const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] }; 37 | return Authorized.check(item.authority, localItem, null); 38 | }); 39 | 40 | const defaultFooterDom = ( 41 | , 53 | href: 'https://github.com/ant-design/ant-design-pro', 54 | blankTarget: true, 55 | }, 56 | { 57 | key: 'Ant Design', 58 | title: 'Ant Design', 59 | href: 'https://ant.design', 60 | blankTarget: true, 61 | }, 62 | ]} 63 | /> 64 | ); 65 | 66 | const footerRender = () => { 67 | if (1) { 68 | return null; 69 | } 70 | if (!isAntDesignPro()) { 71 | return defaultFooterDom; 72 | } 73 | 74 | return ( 75 | <> 76 | {defaultFooterDom} 77 |
83 | 84 | netlify logo 89 | 90 |
91 | 92 | ); 93 | }; 94 | 95 | const BasicLayout = props => { 96 | const { 97 | dispatch, 98 | children, 99 | settings, 100 | location = { 101 | pathname: '/', 102 | }, 103 | } = props; 104 | /** 105 | * constructor 106 | */ 107 | 108 | useEffect(() => { 109 | if (dispatch) { 110 | dispatch({ 111 | type: 'user/fetchCurrent', 112 | }); 113 | } 114 | }, []); 115 | /** 116 | * init variables 117 | */ 118 | 119 | const handleMenuCollapse = payload => { 120 | if (dispatch) { 121 | dispatch({ 122 | type: 'global/changeLayoutCollapsed', 123 | payload, 124 | }); 125 | } 126 | }; // get children authority 127 | 128 | const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || { 129 | authority: undefined, 130 | }; 131 | return ( 132 | ( 135 | 136 | {logoDom} 137 | {titleDom} 138 | 139 | )} 140 | onCollapse={handleMenuCollapse} 141 | menuItemRender={(menuItemProps, defaultDom) => { 142 | if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) { 143 | return defaultDom; 144 | } 145 | 146 | return {defaultDom}; 147 | }} 148 | breadcrumbRender={(routers = []) => [ 149 | { 150 | path: '/', 151 | breadcrumbName: formatMessage({ 152 | id: 'menu.home', 153 | defaultMessage: 'Home', 154 | }), 155 | }, 156 | ...routers, 157 | ]} 158 | itemRender={(route, params, routes, paths) => { 159 | const first = routes.indexOf(route) === 0; 160 | return first ? ( 161 | {route.breadcrumbName} 162 | ) : ( 163 | {route.breadcrumbName} 164 | ); 165 | }} 166 | footerRender={footerRender} 167 | menuDataRender={menuDataRender} 168 | formatMessage={formatMessage} 169 | rightContentRender={() => } 170 | {...props} 171 | {...settings} 172 | > 173 | 174 | {children} 175 | 176 | 177 | ); 178 | }; 179 | 180 | export default connect(({ global, settings }) => ({ 181 | collapsed: global.collapsed, 182 | settings, 183 | }))(BasicLayout); 184 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout = ({ children }) => <>{children}; 4 | 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /src/layouts/DraggableTabs.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Tabs } from 'antd'; 3 | import { DndProvider, DragSource, DropTarget } from 'react-dnd'; 4 | import { HTML5Backend } from 'react-dnd-html5-backend'; 5 | 6 | 7 | 8 | const TabNode = (props) => { 9 | const { connectDragSource, connectDropTarget, children } = props; 10 | return connectDragSource(connectDropTarget(children)); 11 | } 12 | 13 | 14 | const cardTarget = { 15 | drop(props, monitor) { 16 | const dragKey = monitor.getItem().index; 17 | const hoverKey = props.index; 18 | 19 | if (dragKey === hoverKey) { 20 | return; 21 | } 22 | 23 | props.moveTabNode(dragKey, hoverKey); 24 | // eslint-disable-next-line no-param-reassign 25 | monitor.getItem().index = hoverKey; 26 | }, 27 | }; 28 | 29 | const cardSource = { 30 | beginDrag(props) { 31 | return { 32 | id: props.id, 33 | index: props.index, 34 | }; 35 | }, 36 | }; 37 | 38 | const WrapTabNode = DropTarget('DND_NODE', cardTarget, connect => ({ 39 | connectDropTarget: connect.dropTarget(), 40 | }))( 41 | DragSource('DND_NODE', cardSource, (connect, monitor) => ({ 42 | connectDragSource: connect.dragSource(), 43 | isDragging: monitor.isDragging(), 44 | }))(TabNode), 45 | ); 46 | 47 | class DraggableTabs extends Component { 48 | state = { 49 | order: [], 50 | }; 51 | 52 | moveTabNode = (dragKey, hoverKey) => { 53 | const { order } = this.state; 54 | const newOrder = order.slice(); 55 | const { children } = this.props; 56 | 57 | React.Children.forEach(children, c => { 58 | if (newOrder.indexOf(c.key) === -1) { 59 | newOrder.push(c.key); 60 | } 61 | }); 62 | 63 | const dragIndex = newOrder.indexOf(dragKey); 64 | const hoverIndex = newOrder.indexOf(hoverKey); 65 | 66 | newOrder.splice(dragIndex, 1); 67 | newOrder.splice(hoverIndex, 0, dragKey); 68 | 69 | this.setState({ 70 | order: newOrder, 71 | }); 72 | }; 73 | 74 | renderTabBar = (props, DefaultTabBar) => ( 75 | 76 | {node => ( 77 | 78 | {node} 79 | 80 | )} 81 | 82 | ); 83 | 84 | render() { 85 | const { order } = this.state; 86 | const { children } = this.props; 87 | 88 | const tabs = []; 89 | React.Children.forEach(children, c => { 90 | tabs.push(c); 91 | }); 92 | 93 | const orderTabs = tabs.slice().sort((a, b) => { 94 | const orderA = order.indexOf(a.key); 95 | const orderB = order.indexOf(b.key); 96 | 97 | if (orderA !== -1 && orderB !== -1) { 98 | return orderA - orderB; 99 | } 100 | if (orderA !== -1) { 101 | return -1; 102 | } 103 | if (orderB !== -1) { 104 | return 1; 105 | } 106 | 107 | const ia = tabs.indexOf(a); 108 | const ib = tabs.indexOf(b); 109 | 110 | return ia - ib; 111 | }); 112 | 113 | return ( 114 | 115 | 116 | {orderTabs} 117 | 118 | 119 | ); 120 | } 121 | } 122 | 123 | export default DraggableTabs; 124 | -------------------------------------------------------------------------------- /src/layouts/PageTab.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 标签页组件 3 | * @Author hz16042180 4 | * @Date 2019-11-13 5 | * @param children {ReactElement} umi框架中page目录下的页面组件 6 | * @example 7 | * import PageTab from '@/layouts/PageTab'; 8 | * {children} 9 | */ 10 | 11 | import { connect } from 'dva'; 12 | import router from 'umi/router'; 13 | import React, { Component } from 'react'; 14 | import { message, Tabs, Menu, Dropdown, Tooltip, Icon } from 'antd'; 15 | import DraggableTabs from './DraggableTabs'; 16 | import pageTabStyle from './PageTab.less'; 17 | 18 | const { TabPane } = Tabs; 19 | const TABS_NOT_TIPS = 'TABS_NOT_TIPS'; 20 | 21 | 22 | const getTitle = (cb = () => { }) => { 23 | return localStorage.getItem(TABS_NOT_TIPS) ? ( 24 | undefined 25 | ) : ( 26 |
27 |
1、点击鼠标右键可以操作标签页面;
28 |
29 | 2、双击标签页标题可以刷新当前页; 我已知道, 30 | { 33 | message.success('操作成功,已不再提示'); 34 | 35 | localStorage.setItem(TABS_NOT_TIPS, true); 36 | cb(); 37 | }} 38 | > 39 | 不再提示 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | const menu = obj => { 47 | const { pane = {}, pages = [], reflash = () => { }, closeOhterTabs = () => { } } = obj; 48 | const { key, title } = pane; 49 | let leftDisabled = false; 50 | let rightDisabled = false; 51 | pages.forEach((item, index) => { 52 | if (item.key === key) { 53 | if (index > pages.length - 2) { 54 | rightDisabled = true; 55 | } 56 | if (index === 0) { 57 | leftDisabled = true; 58 | } 59 | } 60 | }); 61 | return ( 62 | 63 | { 66 | 67 | reflash(); 68 | }} 69 | > 70 | 71 | 72 | 刷新当前页面 73 | 74 | 75 | { 78 | 79 | window.location.reload(true); 80 | }} 81 | > 82 | 83 | 84 | 刷新浏览器 85 | 86 | 87 | 88 | { 92 | 93 | closeOhterTabs(key); 94 | }} 95 | > 96 | 97 | 关闭其他标签页 98 | 99 | { 103 | 104 | closeOhterTabs(key, 'right'); 105 | }} 106 | > 107 | 108 | 关闭右侧标签页 109 | 110 | { 114 | 115 | closeOhterTabs(key, 'left'); 116 | }} 117 | > 118 | 119 | 关闭左侧标签页 120 | 121 | 122 | ); 123 | }; 124 | @connect(({ global, tabs }) => ({ 125 | global, tabs, 126 | })) 127 | class App extends Component { 128 | state = { pages: [], keys: {} }; 129 | 130 | componentDidMount() { 131 | this.getData(); 132 | this.tipsTitle = getTitle(() => { 133 | this.tipsTitle = undefined; 134 | }); 135 | this.closeOhterTabs = this.closeOhterTabs.bind(this); 136 | } 137 | 138 | componentDidUpdate(preProps) { 139 | const { tabs } = this.props; 140 | const { pathname } = tabs; 141 | if (pathname !== preProps.tabs.pathname) { 142 | // 当路由发生改变时,显示相应tab页面 143 | this.getData(); 144 | } 145 | } 146 | 147 | reflash(key) { 148 | const { keys } = this.state; 149 | keys[key] = Date.now(); 150 | this.setState({ keys }, () => { 151 | message.success('页面已经刷新'); 152 | }); 153 | } 154 | 155 | getData = () => { 156 | const { tabs, children } = this.props; 157 | const { pathname, pageName } = tabs; 158 | const { pages } = this.state; 159 | const myPage = Object.assign([], pages); 160 | // 如果是新开标签页,push到tabs标签页数组中,并设置当前激活页面 161 | if (pathname !== '/' && !pages.some(page => page.key === pathname)) { 162 | myPage.push({ key: pathname, title: pageName, content: children }); 163 | } 164 | const keys = {}; 165 | myPage.forEach(item => { 166 | const { key } = item; 167 | keys[key] = key; 168 | }); 169 | this.setState({ 170 | pages: myPage, 171 | activeKey: pathname, 172 | keys, 173 | }); 174 | }; 175 | 176 | onEdit = targetKey => { 177 | /** 178 | * 参照chrome标签页操作,如果关闭当前页的话: 179 | * 1. 关闭中间某一标签页,选中该页后一页; 180 | * 2. 关闭最后一页标签页,选中该页前一页; 181 | * 3. 仅剩一页时不能删除 182 | */ 183 | const { pages = [] } = this.state; 184 | let { activeKey } = this.state; 185 | let index = null; 186 | index = pages.findIndex(page => page.key === targetKey); 187 | if (activeKey === targetKey) { 188 | const len = pages.length; 189 | if (index === len - 1) { 190 | activeKey = pages[len - 2].key; 191 | } else { 192 | activeKey = pages[index + 1].key; 193 | } 194 | } 195 | pages.splice(index, 1); 196 | this.setState({ pages }, () => { 197 | router.push(activeKey); 198 | }); 199 | }; 200 | 201 | closeOhterTabs(key, direction) { 202 | const { pages } = this.state; 203 | if (pages.length <= 1) { 204 | return; 205 | } 206 | let cIndex = 0; 207 | const newPages = pages 208 | .map((item, index) => { 209 | if (item.key === key) { 210 | cIndex = index; 211 | } 212 | return item; 213 | }) 214 | .map((item, index) => { 215 | if (direction === 'left') { 216 | if (index < cIndex) { 217 | return undefined; 218 | } 219 | } else if (direction === 'right') { 220 | if (index > cIndex) { 221 | return undefined; 222 | } 223 | } else if (item.key !== key) { 224 | return undefined; 225 | } 226 | return item; 227 | }) 228 | .filter(item => item); 229 | this.setState({ pages: newPages }); 230 | } 231 | 232 | render() { 233 | const { pages = [], activeKey, keys } = this.state; 234 | 235 | return ( 236 |
237 | { 244 | router.push(ev); 245 | }} 246 | > 247 | {pages.map(pane => { 248 | return ( 249 | { 259 | this.reflash(pane.key); 260 | }, 261 | })} 262 | > 263 | 264 | { 267 | 268 | this.reflash(pane.key); 269 | }} 270 | > 271 | {pane.title} 272 | 273 | 274 | 275 | } 276 | key={pane.key} 277 | closable={pages.length > 1} 278 | style={{ background: 'transparent', paddingLeft: 0, paddingRight: 0 }} 279 | > 280 |
{pane.content}
281 |
282 | ); 283 | })} 284 |
285 |
286 | ); 287 | } 288 | } 289 | 290 | export default App; 291 | -------------------------------------------------------------------------------- /src/layouts/PageTab.less: -------------------------------------------------------------------------------- 1 | .page { 2 | & > div[class*='ant-tabs-content'] { 3 | background-color: transparent; 4 | padding: 0; 5 | } 6 | & > div[class*="ant-tabs-bar"] { 7 | margin: 0; 8 | } 9 | } 10 | 11 | //main 12 | .tabPage { 13 | & > div { 14 | & > div[class*="ant-pro-page-header-wrap"] { 15 | margin: 0 !important; 16 | } 17 | & > div { 18 | & > div[class*="ant-pro-grid-content"] { 19 | & > div[class*="ant-pro-page-header-wrap-children-content"] { 20 | margin: 10px 0 !important; 21 | } 22 | } 23 | } 24 | & > div[class*='ant-spin-nested-loading'] { 25 | height: 100%; 26 | & > div[class*='ant-spin-container'] { 27 | height: 100%; 28 | & > iframe { 29 | width: 100% !important; 30 | height: 100% !important; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | :global { 38 | .ant-layout { 39 | display: flex; 40 | & > div { 41 | flex: 1; 42 | padding: 10px; 43 | & > main { 44 | height: 100%; 45 | padding: 0; 46 | margin: 0; 47 | & > div, 48 | & > div > div, 49 | & > div > div > div { 50 | height: 100%; 51 | & > div { 52 | display: flex; 53 | flex-direction: column; 54 | & > div[class*="ant-tabs-content"] { 55 | flex: 1; 56 | & > div { 57 | height: 100%; 58 | & > div { 59 | height: 100%; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | .ant-pro-basicLayout-content { 70 | margin: 10px; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { PageLoading } from '@ant-design/pro-layout'; 4 | import { Redirect } from 'umi'; 5 | import { stringify } from 'querystring'; 6 | 7 | class SecurityLayout extends React.Component { 8 | state = { 9 | isReady: false, 10 | }; 11 | 12 | componentDidMount() { 13 | this.setState({ 14 | isReady: true, 15 | }); 16 | const { dispatch } = this.props; 17 | 18 | if (dispatch) { 19 | dispatch({ 20 | type: 'user/fetchCurrent', 21 | }); 22 | } 23 | } 24 | 25 | render() { 26 | const { isReady } = this.state; 27 | const { children, loading, currentUser } = this.props; // You can replace it to your authentication rule (such as check token exists) 28 | // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) 29 | 30 | const isLogin = currentUser && currentUser.userid; 31 | const queryString = stringify({ 32 | redirect: window.location.href, 33 | }); 34 | 35 | if ((!isLogin && loading) || !isReady) { 36 | return ; 37 | } 38 | 39 | if (!isLogin && window.location.pathname !== '/user/login') { 40 | return ; 41 | } 42 | 43 | return children; 44 | } 45 | } 46 | 47 | export default connect(({ user, loading }) => ({ 48 | currentUser: user.currentUser, 49 | loading: loading.models.user, 50 | }))(SecurityLayout); 51 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.jsx: -------------------------------------------------------------------------------- 1 | import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout'; 2 | import { Helmet } from 'react-helmet'; 3 | import { Link } from 'umi'; 4 | import React from 'react'; 5 | import { connect } from 'dva'; 6 | import { formatMessage } from 'umi-plugin-react/locale'; 7 | import SelectLang from '@/components/SelectLang'; 8 | import logo from '../assets/logo.svg'; 9 | import styles from './UserLayout.less'; 10 | 11 | const UserLayout = props => { 12 | const { 13 | route = { 14 | routes: [], 15 | }, 16 | } = props; 17 | const { routes = [] } = route; 18 | const { 19 | children, 20 | location = { 21 | pathname: '', 22 | }, 23 | } = props; 24 | const { breadcrumb } = getMenuData(routes); 25 | const title = getPageTitle({ 26 | pathname: location.pathname, 27 | breadcrumb, 28 | formatMessage, 29 | ...props, 30 | }); 31 | return ( 32 | <> 33 | 34 | {title} 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 | logo 47 | Ant Design 48 | 49 |
50 |
Ant Design 是西湖区最具影响力的 Web 设计规范
51 |
52 | {children} 53 |
54 | 55 |
56 | 57 | ); 58 | }; 59 | 60 | export default connect(({ settings }) => ({ ...settings }))(UserLayout); 61 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .lang { 12 | width: 100%; 13 | height: 40px; 14 | line-height: 44px; 15 | text-align: right; 16 | :global(.ant-dropdown-trigger) { 17 | margin-right: 24px; 18 | } 19 | } 20 | 21 | .content { 22 | flex: 1; 23 | padding: 32px 0; 24 | } 25 | 26 | @media (min-width: @screen-md-min) { 27 | .container { 28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 29 | background-repeat: no-repeat; 30 | background-position: center 110px; 31 | background-size: 100%; 32 | } 33 | 34 | .content { 35 | padding: 32px 0 24px; 36 | } 37 | } 38 | 39 | .top { 40 | text-align: center; 41 | } 42 | 43 | .header { 44 | height: 44px; 45 | line-height: 44px; 46 | a { 47 | text-decoration: none; 48 | } 49 | } 50 | 51 | .logo { 52 | height: 44px; 53 | margin-right: 16px; 54 | vertical-align: top; 55 | } 56 | 57 | .title { 58 | position: relative; 59 | top: 2px; 60 | color: @heading-color; 61 | font-weight: 600; 62 | font-size: 33px; 63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 64 | } 65 | 66 | .desc { 67 | margin-top: 12px; 68 | margin-bottom: 40px; 69 | color: @text-color-secondary; 70 | font-size: @font-size-base; 71 | } 72 | -------------------------------------------------------------------------------- /src/locales/en-US.js: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Languages', 10 | 'layout.user.link.help': 'Help', 11 | 'layout.user.link.privacy': 'Privacy', 12 | 'layout.user.link.terms': 'Terms', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | 'app.welcome.link.fetch-blocks': 'Get all block', 15 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/en-US/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Page', 7 | 'menu.login': 'Login', 8 | 'menu.register': 'Register', 9 | 'menu.register.result': 'Register Result', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Analysis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Basic Form', 19 | 'menu.form.step-form': 'Step Form', 20 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 21 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 22 | 'menu.form.step-form.result': 'Step Form(finished)', 23 | 'menu.form.advanced-form': 'Advanced Form', 24 | 'menu.list': 'List', 25 | 'menu.list.table-list': 'Search Table', 26 | 'menu.list.basic-list': 'Basic List', 27 | 'menu.list.card-list': 'Card List', 28 | 'menu.list.search-list': 'Search List', 29 | 'menu.list.search-list.articles': 'Search List(articles)', 30 | 'menu.list.search-list.projects': 'Search List(projects)', 31 | 'menu.list.search-list.applications': 'Search List(applications)', 32 | 'menu.profile': 'Profile', 33 | 'menu.profile.basic': 'Basic Profile', 34 | 'menu.profile.advanced': 'Advanced Profile', 35 | 'menu.result': 'Result', 36 | 'menu.result.success': 'Success', 37 | 'menu.result.fail': 'Fail', 38 | 'menu.exception': 'Exception', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Trigger', 43 | 'menu.account': 'Account', 44 | 'menu.account.center': 'Account Center', 45 | 'menu.account.settings': 'Account Settings', 46 | 'menu.account.trigger': 'Trigger Error', 47 | 'menu.account.logout': 'Logout', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/en-US/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/en-US/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 38 | 'Unbound MFA device, after binding, can be confirmed twice', 39 | 'app.settings.security.modify': 'Modify', 40 | 'app.settings.security.set': 'Set', 41 | 'app.settings.security.bind': 'Bind', 42 | 'app.settings.binding.taobao': 'Binding Taobao', 43 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 44 | 'app.settings.binding.alipay': 'Binding Alipay', 45 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 46 | 'app.settings.binding.dingding': 'Binding DingTalk', 47 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 48 | 'app.settings.binding.bind': 'Bind', 49 | 'app.settings.notification.password': 'Account Password', 50 | 'app.settings.notification.password-description': 51 | 'Messages from other users will be notified in the form of a station letter', 52 | 'app.settings.notification.messages': 'System Messages', 53 | 'app.settings.notification.messages-description': 54 | 'System messages will be notified in the form of a station letter', 55 | 'app.settings.notification.todo': 'To-do Notification', 56 | 'app.settings.notification.todo-description': 57 | 'The to-do list will be notified in the form of a letter from the station', 58 | 'app.settings.open': 'Open', 59 | 'app.settings.close': 'Close', 60 | }; 61 | -------------------------------------------------------------------------------- /src/locales/pt-BR.js: -------------------------------------------------------------------------------- 1 | import component from './pt-BR/component'; 2 | import globalHeader from './pt-BR/globalHeader'; 3 | import menu from './pt-BR/menu'; 4 | import pwa from './pt-BR/pwa'; 5 | import settingDrawer from './pt-BR/settingDrawer'; 6 | import settings from './pt-BR/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Idiomas', 10 | 'layout.user.link.help': 'ajuda', 11 | 'layout.user.link.privacy': 'política de privacidade', 12 | 'layout.user.link.terms': 'termos de serviços', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/pt-BR/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/pt-BR/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Busca', 3 | 'component.globalHeader.search.example1': 'Exemplo de busca 1', 4 | 'component.globalHeader.search.example2': 'Exemplo de busca 2', 5 | 'component.globalHeader.search.example3': 'Exemplo de busca 3', 6 | 'component.globalHeader.help': 'Ajuda', 7 | 'component.globalHeader.notification': 'Notificação', 8 | 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', 9 | 'component.globalHeader.message': 'Mensagem', 10 | 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', 11 | 'component.globalHeader.event': 'Evento', 12 | 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', 13 | 'component.noticeIcon.clear': 'Limpar', 14 | 'component.noticeIcon.cleared': 'Limpo', 15 | 'component.noticeIcon.empty': 'Sem notificações', 16 | 'component.noticeIcon.loaded': 'Carregado', 17 | 'component.noticeIcon.view-more': 'Veja mais', 18 | }; 19 | -------------------------------------------------------------------------------- /src/locales/pt-BR/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Início', 5 | 'menu.login': 'Login', 6 | 'menu.admin': 'Admin', 7 | 'menu.admin.sub-page': 'Sub-Page', 8 | 'menu.register': 'Registro', 9 | 'menu.register.result': 'Resultado de registro', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Análise', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Formulário', 18 | 'menu.form.basic-form': 'Formulário Básico', 19 | 'menu.form.step-form': 'Formulário Assistido', 20 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 21 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 22 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 23 | 'menu.form.advanced-form': 'Formulário Avançado', 24 | 'menu.list': 'Lista', 25 | 'menu.list.table-list': 'Tabela de Busca', 26 | 'menu.list.basic-list': 'Lista Básica', 27 | 'menu.list.card-list': 'Lista de Card', 28 | 'menu.list.search-list': 'Lista de Busca', 29 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 30 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 31 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 32 | 'menu.profile': 'Perfil', 33 | 'menu.profile.basic': 'Perfil Básico', 34 | 'menu.profile.advanced': 'Perfil Avançado', 35 | 'menu.result': 'Resultado', 36 | 'menu.result.success': 'Sucesso', 37 | 'menu.result.fail': 'Falha', 38 | 'menu.exception': 'Exceção', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Disparar', 43 | 'menu.account': 'Conta', 44 | 'menu.account.center': 'Central da Conta', 45 | 'menu.account.settings': 'Configurar Conta', 46 | 'menu.account.trigger': 'Disparar Erro', 47 | 'menu.account.logout': 'Sair', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/pt-BR/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/pt-BR/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Configuração de estilo da página', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Largura do conteúdo', 6 | 'app.setting.content-width.fixed': 'Fixo', 7 | 'app.setting.content-width.fluid': 'Fluido', 8 | 'app.setting.themecolor': 'Cor do Tema', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Modo de Navegação', 18 | 'app.setting.sidemenu': 'Layout do Menu Lateral', 19 | 'app.setting.topmenu': 'Layout do Menu Superior', 20 | 'app.setting.fixedheader': 'Cabeçalho fixo', 21 | 'app.setting.fixedsidebar': 'Barra lateral fixa', 22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', 23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', 24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', 25 | 'app.setting.othersettings': 'Outras configurações', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copiar Configuração', 28 | 'app.setting.copyinfo': 29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/pt-BR/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Configurações Básicas', 3 | 'app.settings.menuMap.security': 'Configurações de Segurança', 4 | 'app.settings.menuMap.binding': 'Vinculação de Conta', 5 | 'app.settings.menuMap.notification': 'Mensagens de Notificação', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Alterar avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Por favor insira seu email!', 10 | 'app.settings.basic.nickname': 'Nome de usuário', 11 | 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!', 12 | 'app.settings.basic.profile': 'Perfil pessoal', 13 | 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!', 14 | 'app.settings.basic.profile-placeholder': 'Breve introdução sua', 15 | 'app.settings.basic.country': 'País/Região', 16 | 'app.settings.basic.country-message': 'Por favor insira país!', 17 | 'app.settings.basic.geographic': 'Província, estado ou cidade', 18 | 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!', 19 | 'app.settings.basic.address': 'Endereço', 20 | 'app.settings.basic.address-message': 'Por favor insira seu endereço!', 21 | 'app.settings.basic.phone': 'Número de telefone', 22 | 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!', 23 | 'app.settings.basic.update': 'Atualizar Informações', 24 | 'app.settings.security.strong': 'Forte', 25 | 'app.settings.security.medium': 'Média', 26 | 'app.settings.security.weak': 'Fraca', 27 | 'app.settings.security.password': 'Senha da Conta', 28 | 'app.settings.security.password-description': 'Força da senha', 29 | 'app.settings.security.phone': 'Telefone de Seguraça', 30 | 'app.settings.security.phone-description': 'Telefone vinculado', 31 | 'app.settings.security.question': 'Pergunta de Segurança', 32 | 'app.settings.security.question-description': 33 | 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta', 34 | 'app.settings.security.email': 'Email de Backup', 35 | 'app.settings.security.email-description': 'Email vinculado', 36 | 'app.settings.security.mfa': 'Dispositivo MFA', 37 | 'app.settings.security.mfa-description': 38 | 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes', 39 | 'app.settings.security.modify': 'Modificar', 40 | 'app.settings.security.set': 'Atribuir', 41 | 'app.settings.security.bind': 'Vincular', 42 | 'app.settings.binding.taobao': 'Vincular Taobao', 43 | 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao', 44 | 'app.settings.binding.alipay': 'Vincular Alipay', 45 | 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay', 46 | 'app.settings.binding.dingding': 'Vincular DingTalk', 47 | 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk', 48 | 'app.settings.binding.bind': 'Vincular', 49 | 'app.settings.notification.password': 'Senha da Conta', 50 | 'app.settings.notification.password-description': 51 | 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra', 52 | 'app.settings.notification.messages': 'Mensagens de Sistema', 53 | 'app.settings.notification.messages-description': 54 | 'Mensagens de sistema serão notificadas na forma de uma estação de letra', 55 | 'app.settings.notification.todo': 'Notificação de To-do', 56 | 'app.settings.notification.todo-description': 57 | 'A lista de to-do será notificada na forma de uma estação de letra', 58 | 'app.settings.open': 'Aberto', 59 | 'app.settings.close': 'Fechado', 60 | }; 61 | -------------------------------------------------------------------------------- /src/locales/zh-CN.js: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pwa from './zh-CN/pwa'; 5 | import settingDrawer from './zh-CN/settingDrawer'; 6 | import settings from './zh-CN/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '语言', 10 | 'layout.user.link.help': '帮助', 11 | 'layout.user.link.privacy': '隐私', 12 | 'layout.user.link.terms': '条款', 13 | 'app.preview.down.block': '下载此页面到本地项目', 14 | 'app.welcome.link.fetch-blocks': '获取全部区块', 15 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.test': '测试页面', 6 | 'menu.admin': '管理页', 7 | 'menu.admin.sub-page': '二级管理页', 8 | 'menu.login': '登录', 9 | 'menu.CustomPage': '自定义', 10 | 'menu.register': '注册', 11 | 'menu.register.result': '注册结果', 12 | 'menu.dashboard': 'Dashboard', 13 | 'menu.dashboard.analysis': '分析页', 14 | 'menu.dashboard.monitor': '监控页', 15 | 'menu.dashboard.workplace': '工作台', 16 | 'menu.exception.403': '403', 17 | 'menu.exception.404': '404', 18 | 'menu.exception.500': '500', 19 | 'menu.form': '表单页', 20 | 'menu.form.basic-form': '基础表单', 21 | 'menu.form.step-form': '分步表单', 22 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 23 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 24 | 'menu.form.step-form.result': '分步表单(完成)', 25 | 'menu.form.advanced-form': '高级表单', 26 | 'menu.list': '列表页', 27 | 'menu.list.table-list': '查询表格', 28 | 'menu.list.basic-list': '标准列表', 29 | 'menu.list.card-list': '卡片列表', 30 | 'menu.list.search-list': '搜索列表', 31 | 'menu.list.search-list.articles': '搜索列表(文章)', 32 | 'menu.list.search-list.projects': '搜索列表(项目)', 33 | 'menu.list.search-list.applications': '搜索列表(应用)', 34 | 'menu.profile': '详情页', 35 | 'menu.profile.basic': '基础详情页', 36 | 'menu.profile.advanced': '高级详情页', 37 | 'menu.result': '结果页', 38 | 'menu.result.success': '成功页', 39 | 'menu.result.fail': '失败页', 40 | 'menu.exception': '异常页', 41 | 'menu.exception.not-permission': '403', 42 | 'menu.exception.not-find': '404', 43 | 'menu.exception.server-error': '500', 44 | 'menu.exception.trigger': '触发错误', 45 | 'menu.account': '个人页', 46 | 'menu.account.center': '个人中心', 47 | 'menu.account.settings': '个人设置', 48 | 'menu.account.trigger': '触发报错', 49 | 'menu.account.logout': '退出登录', 50 | 'menu.editor': '图形编辑器', 51 | 'menu.editor.flow': '流程编辑器', 52 | 'menu.editor.mind': '脑图编辑器', 53 | 'menu.editor.koni': '拓扑编辑器', 54 | }; 55 | -------------------------------------------------------------------------------- /src/locales/zh-CN/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/zh-TW.js: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/zh-TW/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-TW/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站內搜索', 3 | 'component.globalHeader.search.example1': '搜索提示壹', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用手冊', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '妳已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已讀完所有消息', 11 | 'component.globalHeader.event': '待辦', 12 | 'component.globalHeader.event.empty': '妳已完成所有待辦', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暫無資料', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-TW/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '歡迎', 3 | 'menu.more-blocks': '更多區塊', 4 | 'menu.home': '首頁', 5 | 'menu.login': '登錄', 6 | 'menu.admin': '权限', 7 | 'menu.admin.sub-page': '二级管理页', 8 | 'menu.exception.403': '403', 9 | 'menu.exception.404': '404', 10 | 'menu.exception.500': '500', 11 | 'menu.register': '註冊', 12 | 'menu.register.result': '註冊結果', 13 | 'menu.dashboard': 'Dashboard', 14 | 'menu.dashboard.analysis': '分析頁', 15 | 'menu.dashboard.monitor': '監控頁', 16 | 'menu.dashboard.workplace': '工作臺', 17 | 'menu.form': '表單頁', 18 | 'menu.form.basic-form': '基礎表單', 19 | 'menu.form.step-form': '分步表單', 20 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 21 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 22 | 'menu.form.step-form.result': '分步表單(完成)', 23 | 'menu.form.advanced-form': '高級表單', 24 | 'menu.list': '列表頁', 25 | 'menu.list.table-list': '查詢表格', 26 | 'menu.list.basic-list': '標淮列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(項目)', 31 | 'menu.list.search-list.applications': '搜索列表(應用)', 32 | 'menu.profile': '詳情頁', 33 | 'menu.profile.basic': '基礎詳情頁', 34 | 'menu.profile.advanced': '高級詳情頁', 35 | 'menu.result': '結果頁', 36 | 'menu.result.success': '成功頁', 37 | 'menu.result.fail': '失敗頁', 38 | 'menu.account': '個人頁', 39 | 'menu.account.center': '個人中心', 40 | 'menu.account.settings': '個人設置', 41 | 'menu.account.trigger': '觸發報錯', 42 | 'menu.account.logout': '退出登錄', 43 | 'menu.exception': '异常页', 44 | 'menu.exception.not-permission': '403', 45 | 'menu.exception.not-find': '404', 46 | 'menu.exception.server-error': '500', 47 | 'menu.exception.trigger': '触发错误', 48 | 'menu.editor': '圖形編輯器', 49 | 'menu.editor.flow': '流程編輯器', 50 | 'menu.editor.mind': '腦圖編輯器', 51 | 'menu.editor.koni': '拓撲編輯器', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/zh-TW/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整體風格設置', 3 | 'app.setting.pagestyle.dark': '暗色菜單風格', 4 | 'app.setting.pagestyle.light': '亮色菜單風格', 5 | 'app.setting.content-width': '內容區域寬度', 6 | 'app.setting.content-width.fixed': '定寬', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主題色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '極光綠', 14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)', 15 | 'app.setting.themecolor.geekblue': '極客藍', 16 | 'app.setting.themecolor.purple': '醬紫', 17 | 'app.setting.navigationmode': '導航模式', 18 | 'app.setting.sidemenu': '側邊菜單布局', 19 | 'app.setting.topmenu': '頂部菜單布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定側邊菜單', 22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置', 23 | 'app.setting.hideheader': '下滑時隱藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 時可配置', 25 | 'app.setting.othersettings': '其他設置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷貝設置', 28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', 29 | 'app.setting.production.hint': 30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設置', 3 | 'app.settings.menuMap.security': '安全設置', 4 | 'app.settings.menuMap.binding': '賬號綁定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '頭像', 7 | 'app.settings.basic.change-avatar': '更換頭像', 8 | 'app.settings.basic.email': '郵箱', 9 | 'app.settings.basic.email-message': '請輸入您的郵箱!', 10 | 'app.settings.basic.nickname': '昵稱', 11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'app.settings.basic.profile': '個人簡介', 13 | 'app.settings.basic.profile-message': '請輸入個人簡介!', 14 | 'app.settings.basic.profile-placeholder': '個人簡介', 15 | 'app.settings.basic.country': '國家/地區', 16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '請輸入您的街道地址!', 21 | 'app.settings.basic.phone': '聯系電話', 22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '強', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '賬戶密碼', 28 | 'app.settings.security.password-description': '當前密碼強度', 29 | 'app.settings.security.phone': '密保手機', 30 | 'app.settings.security.phone-description': '已綁定手機', 31 | 'app.settings.security.question': '密保問題', 32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 33 | 'app.settings.security.email': '備用郵箱', 34 | 'app.settings.security.email-description': '已綁定郵箱', 35 | 'app.settings.security.mfa': 'MFA 設備', 36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '設置', 39 | 'app.settings.security.bind': '綁定', 40 | 'app.settings.binding.taobao': '綁定淘寶', 41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號', 42 | 'app.settings.binding.alipay': '綁定支付寶', 43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號', 44 | 'app.settings.binding.dingding': '綁定釘釘', 45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號', 46 | 'app.settings.binding.bind': '綁定', 47 | 'app.settings.notification.password': '賬戶密碼', 48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 49 | 'app.settings.notification.messages': '系統消息', 50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知', 51 | 'app.settings.notification.todo': '待辦任務', 52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知', 53 | 'app.settings.open': '開', 54 | 'app.settings.close': '關', 55 | }; 56 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ant Design Pro", 3 | "short_name": "Ant Design Pro", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/models/global.js: -------------------------------------------------------------------------------- 1 | import { queryNotices } from '@/services/user'; 2 | 3 | const GlobalModel = { 4 | namespace: 'global', 5 | state: { 6 | collapsed: false, 7 | notices: [], 8 | }, 9 | effects: { 10 | *fetchNotices(_, { call, put, select }) { 11 | const data = yield call(queryNotices); 12 | yield put({ 13 | type: 'saveNotices', 14 | payload: data, 15 | }); 16 | const unreadCount = yield select( 17 | state => state.global.notices.filter(item => !item.read).length, 18 | ); 19 | yield put({ 20 | type: 'user/changeNotifyCount', 21 | payload: { 22 | totalCount: data.length, 23 | unreadCount, 24 | }, 25 | }); 26 | }, 27 | 28 | *clearNotices({ payload }, { put, select }) { 29 | yield put({ 30 | type: 'saveClearedNotices', 31 | payload, 32 | }); 33 | const count = yield select(state => state.global.notices.length); 34 | const unreadCount = yield select( 35 | state => state.global.notices.filter(item => !item.read).length, 36 | ); 37 | yield put({ 38 | type: 'user/changeNotifyCount', 39 | payload: { 40 | totalCount: count, 41 | unreadCount, 42 | }, 43 | }); 44 | }, 45 | 46 | *changeNoticeReadState({ payload }, { put, select }) { 47 | const notices = yield select(state => 48 | state.global.notices.map(item => { 49 | const notice = { ...item }; 50 | 51 | if (notice.id === payload) { 52 | notice.read = true; 53 | } 54 | 55 | return notice; 56 | }), 57 | ); 58 | yield put({ 59 | type: 'saveNotices', 60 | payload: notices, 61 | }); 62 | yield put({ 63 | type: 'user/changeNotifyCount', 64 | payload: { 65 | totalCount: notices.length, 66 | unreadCount: notices.filter(item => !item.read).length, 67 | }, 68 | }); 69 | }, 70 | }, 71 | reducers: { 72 | changeLayoutCollapsed( 73 | state = { 74 | notices: [], 75 | collapsed: true, 76 | }, 77 | { payload }, 78 | ) { 79 | return { ...state, collapsed: payload }; 80 | }, 81 | 82 | saveNotices(state, { payload }) { 83 | return { 84 | collapsed: false, 85 | ...state, 86 | notices: payload, 87 | }; 88 | }, 89 | 90 | saveClearedNotices( 91 | state = { 92 | notices: [], 93 | collapsed: true, 94 | }, 95 | { payload }, 96 | ) { 97 | return { 98 | collapsed: false, 99 | ...state, 100 | notices: state.notices.filter(item => item.type !== payload), 101 | }; 102 | }, 103 | }, 104 | subscriptions: { 105 | setup({ history }) { 106 | // Subscribe history(url) change, trigger `load` action if pathname is `/` 107 | history.listen(({ pathname, search }) => { 108 | if (typeof window.ga !== 'undefined') { 109 | window.ga('send', 'pageview', pathname + search); 110 | } 111 | }); 112 | }, 113 | }, 114 | }; 115 | export default GlobalModel; 116 | -------------------------------------------------------------------------------- /src/models/login.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'querystring'; 2 | import { router } from 'umi'; 3 | import { fakeAccountLogin, getFakeCaptcha } from '@/services/login'; 4 | import { setAuthority } from '@/utils/authority'; 5 | import { getPageQuery } from '@/utils/utils'; 6 | 7 | const Model = { 8 | namespace: 'login', 9 | state: { 10 | status: undefined, 11 | }, 12 | effects: { 13 | *login({ payload }, { call, put }) { 14 | const response = yield call(fakeAccountLogin, payload); 15 | yield put({ 16 | type: 'changeLoginStatus', 17 | payload: response, 18 | }); // Login successfully 19 | 20 | if (response.status === 'ok') { 21 | const urlParams = new URL(window.location.href); 22 | const params = getPageQuery(); 23 | let { redirect } = params; 24 | 25 | if (redirect) { 26 | const redirectUrlParams = new URL(redirect); 27 | 28 | if (redirectUrlParams.origin === urlParams.origin) { 29 | redirect = redirect.substr(urlParams.origin.length); 30 | 31 | if (redirect.match(/^\/.*#/)) { 32 | redirect = redirect.substr(redirect.indexOf('#') + 1); 33 | } 34 | } else { 35 | window.location.href = '/'; 36 | return; 37 | } 38 | } 39 | 40 | router.replace(redirect || '/'); 41 | } 42 | }, 43 | 44 | *getCaptcha({ payload }, { call }) { 45 | yield call(getFakeCaptcha, payload); 46 | }, 47 | 48 | logout() { 49 | const { redirect } = getPageQuery(); // Note: There may be security issues, please note 50 | 51 | if (window.location.pathname !== '/user/login' && !redirect) { 52 | router.replace({ 53 | pathname: '/user/login', 54 | search: stringify({ 55 | redirect: window.location.href, 56 | }), 57 | }); 58 | } 59 | }, 60 | }, 61 | reducers: { 62 | changeLoginStatus(state, { payload }) { 63 | setAuthority(payload.currentAuthority); 64 | return { ...state, status: payload.status, type: payload.type }; 65 | }, 66 | }, 67 | }; 68 | export default Model; 69 | -------------------------------------------------------------------------------- /src/models/setting.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '../../config/defaultSettings'; 2 | 3 | const updateColorWeak = colorWeak => { 4 | const root = document.getElementById('root'); 5 | 6 | if (root) { 7 | root.className = colorWeak ? 'colorWeak' : ''; 8 | } 9 | }; 10 | 11 | const SettingModel = { 12 | namespace: 'settings', 13 | state: defaultSettings, 14 | reducers: { 15 | changeSetting(state = defaultSettings, { payload }) { 16 | const { colorWeak, contentWidth } = payload; 17 | 18 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 19 | window.dispatchEvent(new Event('resize')); 20 | } 21 | 22 | updateColorWeak(!!colorWeak); 23 | return { ...state, ...payload }; 24 | }, 25 | }, 26 | }; 27 | export default SettingModel; 28 | -------------------------------------------------------------------------------- /src/models/tabs.js: -------------------------------------------------------------------------------- 1 | 2 | import config from '../../config/config'; 3 | import menu from '../locales/zh-CN/menu'; 4 | import { store } from '@/utils/utils'; 5 | 6 | const { get } = store; 7 | const GlobalModel = { 8 | namespace: 'tabs', 9 | state: { 10 | collapsed: false, 11 | notices: [], 12 | pathname: '/', 13 | pageName: '新页面', 14 | paths: [], 15 | pages: [], 16 | }, 17 | effects: { 18 | 19 | }, 20 | reducers: { 21 | // 设置当前Path 22 | setCurrentPath(state, { payload }) { 23 | const { pathname, pageName } = payload; 24 | const { paths } = state; 25 | if (!paths.some(path => path === pathname)) { 26 | paths.push(pathname); 27 | } 28 | return { ...state, pathname, pageName, paths }; 29 | }, 30 | }, 31 | subscriptions: { 32 | setup({ dispatch, history }) { 33 | const getName = (routes = [], parentName, pathname) => { 34 | const list = []; 35 | routes.forEach(item => { 36 | // eslint-disable-next-line no-shadow 37 | const { routes, name } = item; 38 | const pName = parentName && name ? `${parentName}.${name}` : parentName || name; 39 | 40 | if (routes && routes.length) { 41 | list.push(...getName(routes, pName, pathname)); 42 | } else if (pName && name) { 43 | if (item.path === pathname) { 44 | list.push(pName); 45 | } 46 | } 47 | }); 48 | return list; 49 | }; 50 | // 监听路由变化 51 | return history.listen(({ pathname }) => { 52 | let id; 53 | if (pathname === '/') { 54 | return; 55 | } 56 | try { 57 | id = pathname.split('/').slice(-1)[0]; 58 | } catch (error) { } 59 | const { title } = get(id, 'sessionstorage') || {}; 60 | let name = ''; 61 | name = pathname.substr(pathname.lastIndexOf('/') + 1); 62 | 63 | const pageName = 64 | menu[getName(config.routes, 'menu', pathname)[0]] || title || name || '新标签页'; 65 | 66 | setTimeout(() => { 67 | dispatch({ type: 'setCurrentPath', payload: { pathname, pageName: title || pageName } }); 68 | // dispatch({ type: 'addPath', payload: { pathname, pageName } }); 69 | }, 0); 70 | }); 71 | }, 72 | }, 73 | }; 74 | export default GlobalModel; 75 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | import { queryCurrent, query as queryUsers } from '@/services/user'; 2 | 3 | const UserModel = { 4 | namespace: 'user', 5 | state: { 6 | currentUser: {}, 7 | }, 8 | effects: { 9 | *fetch(_, { call, put }) { 10 | const response = yield call(queryUsers); 11 | yield put({ 12 | type: 'save', 13 | payload: response, 14 | }); 15 | }, 16 | 17 | *fetchCurrent(_, { call, put }) { 18 | const response = yield call(queryCurrent); 19 | yield put({ 20 | type: 'saveCurrentUser', 21 | payload: response, 22 | }); 23 | }, 24 | }, 25 | reducers: { 26 | saveCurrentUser(state, action) { 27 | return { ...state, currentUser: action.payload || {} }; 28 | }, 29 | 30 | changeNotifyCount( 31 | state = { 32 | currentUser: {}, 33 | }, 34 | action, 35 | ) { 36 | return { 37 | ...state, 38 | currentUser: { 39 | ...state.currentUser, 40 | notifyCount: action.payload.totalCount, 41 | unreadCount: action.payload.unreadCount, 42 | }, 43 | }; 44 | }, 45 | }, 46 | }; 47 | export default UserModel; 48 | -------------------------------------------------------------------------------- /src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { router } from 'umi'; // 这里应该使用 antd 的 404 result 组件, 4 | // 但是还没发布,先来个简单的。 5 | 6 | const NoFoundPage = () => ( 7 | router.push('/')}> 13 | Back Home 14 | 15 | } 16 | /> 17 | ); 18 | 19 | export default NoFoundPage; 20 | -------------------------------------------------------------------------------- /src/pages/Admin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, Typography, Alert, Icon } from 'antd'; 3 | import { PageHeaderWrapper } from '@ant-design/pro-layout'; 4 | 5 | export default () => ( 6 | 7 | 8 | 18 | 24 | Ant Design Pro{' '} 25 | You 26 | 27 | 28 |

34 | Want to add more pages? Please refer to{' '} 35 | 36 | use block 37 | 38 | 。 39 |

40 |
41 | ); 42 | -------------------------------------------------------------------------------- /src/pages/Authorized.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'umi'; 3 | import { connect } from 'dva'; 4 | import Authorized from '@/utils/Authorized'; 5 | import { getRouteAuthority } from '@/utils/utils'; 6 | 7 | const AuthComponent = ({ 8 | children, 9 | route = { 10 | routes: [], 11 | }, 12 | location = { 13 | pathname: '', 14 | }, 15 | user, 16 | }) => { 17 | const { currentUser } = user; 18 | const { routes = [] } = route; 19 | const isLogin = currentUser && currentUser.name; 20 | return ( 21 | : } 24 | > 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default connect(({ user }) => ({ 31 | user, 32 | }))(AuthComponent); 33 | -------------------------------------------------------------------------------- /src/pages/CustomPage/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 寄送管理详情-内嵌页面 3 | * @Author hz19114716 4 | * @Date 2019-12-04 5 | */ 6 | import { stringify } from 'qs'; 7 | import { store } from '@/utils/utils'; 8 | import withRouter from 'umi/withRouter'; 9 | import React, { Component } from 'react'; 10 | import PageLoading from '@/components/PageLoading'; 11 | import style from './index.less'; 12 | 13 | const { get } = store; 14 | 15 | class CustomPageApp extends Component { 16 | state = { id: undefined }; 17 | 18 | componentDidMount() { 19 | const { match } = this.props; 20 | const { params } = match; 21 | const { id } = params; 22 | const { title, url, data } = get(id, 'sessionstorage') || {}; 23 | this.setState({ id, title, url, data }); 24 | } 25 | 26 | render() { 27 | const { url, title, data } = this.state; 28 | if (!url) { 29 | return ; 30 | } 31 | const src = `${url}${url.indexOf('?') !== -1 ? '&' : '?'}${stringify(data)}`; 32 | return