├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ └── rebase.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── README.md ├── config ├── config.js ├── defaultSettings.js ├── plugin.config.js ├── route.config.js └── themePluginConfig.js ├── jest-puppeteer.config.js ├── jest.config.js ├── jsconfig.json ├── package.json ├── public ├── favicon.png └── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png ├── screenshot_qq.jpeg ├── src ├── api.jsx ├── 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 │ ├── SecurityLayout.jsx │ ├── UserLayout.jsx │ └── UserLayout.less ├── locales │ ├── en-US.js │ ├── en-US │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pwa.js │ │ ├── role.js │ │ ├── settingDrawer.js │ │ ├── settings.js │ │ └── user.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 │ │ ├── role.js │ │ ├── settingDrawer.js │ │ ├── settings.js │ │ └── user.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 │ ├── menu.js │ ├── role.js │ ├── setting.js │ └── user.js ├── pages │ ├── 404.jsx │ ├── Authorized.jsx │ ├── Welcome.jsx │ ├── Welcome.less │ ├── document.ejs │ ├── system │ │ ├── menu │ │ │ ├── components │ │ │ │ ├── MenuAction │ │ │ │ │ ├── AddDialog.jsx │ │ │ │ │ ├── EditableCell.jsx │ │ │ │ │ └── index.jsx │ │ │ │ ├── MenuEditor.jsx │ │ │ │ ├── MenuResource │ │ │ │ │ ├── AddDialog.jsx │ │ │ │ │ ├── EditableCell.jsx │ │ │ │ │ └── index.jsx │ │ │ │ └── StandardTable │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── index.less │ │ │ ├── data.d.ts │ │ │ ├── index.jsx │ │ │ ├── style.less │ │ │ └── utils │ │ │ │ └── utils.less │ │ └── role │ │ │ ├── components │ │ │ ├── RoleEditor.jsx │ │ │ ├── RoleMenu │ │ │ │ ├── EditableCell.jsx │ │ │ │ └── index.jsx │ │ │ └── StandardTable │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── data.d.ts │ │ │ ├── index.jsx │ │ │ ├── style.less │ │ │ └── utils │ │ │ └── utils.less │ └── user │ │ ├── components │ │ └── UpdatePasswordDialog.jsx │ │ ├── list │ │ ├── components │ │ │ ├── RoleSelect.jsx │ │ │ ├── StandardTable │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ └── UserEditor.jsx │ │ ├── data.d.ts │ │ ├── index.jsx │ │ ├── style.less │ │ └── utils │ │ │ └── utils.less │ │ └── 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 │ ├── menu.js │ ├── role.js │ └── user.js └── utils │ ├── Authorized.js │ ├── authority.js │ ├── authority.test.js │ ├── request.js │ ├── store.js │ ├── utils.js │ ├── utils.less │ └── utils.test.js └── tests └── run-tests.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 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 Ant Design Pro 的 bug 4 | title: '🐛[BUG]' 5 | labels: '🐛bug' 6 | assignees: '' 7 | --- 8 | 9 | ### 🐛 bug 描述 10 | 11 | 14 | 15 | ### 📷 复现步骤 16 | 17 | 20 | 21 | ### 🏞 期望结果 22 | 23 | 26 | 27 | ### 💻 复现代码 28 | 29 | 32 | 33 | ### © 版本信息 34 | 35 | - Ant Design Pro 版本: [e.g. 4.0.0] 36 | - umi 版本 37 | - 浏览器环境 38 | - 开发环境 [e.g. mac OS] 39 | 40 | ### 🚑 其他信息 41 | 42 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 Ant Design Pro 的需求或建议 4 | title: '👑 [需求]' 5 | labels: '👑Feature Request' 6 | assignees: '' 7 | --- 8 | 9 | ### 🥰 需求描述 10 | 11 | 14 | 15 | ### 🧐 解决方案 16 | 17 | 20 | 21 | ### 🚑 其他信息 22 | 23 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '疑问或需要帮助 ❓' 3 | about: 对 Ant Design Pro 使用的疑问或需要帮助 4 | title: '🧐[问题]' 5 | labels: '🧐question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 问题描述 10 | 11 | 14 | 15 | ### 💻 示例代码 16 | 17 | 20 | 21 | ### 🚑 其他信息 22 | 23 | 26 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Automatic Rebase 5 | jobs: 6 | rebase: 7 | name: Rebase 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Automatic Rebase 12 | uses: cirrus-actions/rebase@master 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | # Gin Admin React - [gin-admin](https://github.com/LyricTian/gin-admin) 2 |
3 | 基于 Ant Design Pro v4 实现的RBAC权限管理脚手架,目的是提供一套轻量的中后台开发框架,方便、快速的完成业务需求的开发。 4 |
5 | 6 | - [在线演示地址](http://demo.tiannianshou.com) (用户名:root,密码:abc-123) 7 | (`温馨提醒:为了达到更好的演示效果,这里给出了拥有最高权限的用户,请手下留情,只操作自己新增的数据,不要动平台本身的数据!谢谢!`) 8 | 9 | ## 获取并运行 10 | ``` 11 | git clone https://github.com/gin-admin/gin-admin-react.git 12 | cd gin-admin-react 13 | yarn 14 | yarn start 15 | ``` 16 | 17 | ## 打包编译 18 | ``` 19 | umi build 20 | ``` 21 | 22 | ## 讨论组 23 | > QQ群: 1409099 24 | > 25 | 26 | ## MIT License 27 | Copyright (c) 2019 gin-admin 28 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from './defaultSettings'; // https://umijs.org/config/ 2 | 3 | import slash from 'slash2'; 4 | import themePluginConfig from './themePluginConfig'; 5 | import pageRoutes from './route.config'; 6 | const { pwa } = defaultSettings; // preview.pro.ant.design only do not use in your production ; 7 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 8 | 9 | const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env; 10 | const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site'; 11 | const plugins = [ 12 | [ 13 | 'umi-plugin-react', 14 | { 15 | antd: true, 16 | dva: { 17 | hmr: true, 18 | }, 19 | locale: { 20 | // default false 21 | enable: true, 22 | // default zh-CN 23 | default: 'zh-CN', 24 | // default true, when it is true, will use `navigator.language` overwrite default 25 | baseNavigator: true, 26 | }, 27 | // dynamicImport: { 28 | // loadingComponent: './components/PageLoading/index', 29 | // webpackChunkName: true, 30 | // level: 3, 31 | // }, 32 | pwa: pwa 33 | ? { 34 | workboxPluginMode: 'InjectManifest', 35 | workboxOptions: { 36 | importWorkboxFrom: 'local', 37 | }, 38 | } 39 | : false, // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665 40 | // dll features https://webpack.js.org/plugins/dll-plugin/ 41 | // dll: { 42 | // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'], 43 | // exclude: ['@babel/runtime', 'netlify-lambda'], 44 | // }, 45 | }, 46 | ], 47 | [ 48 | 'umi-plugin-pro-block', 49 | { 50 | moveMock: false, 51 | moveService: false, 52 | modifyRequest: true, 53 | autoAddMenu: true, 54 | }, 55 | ], 56 | ]; 57 | 58 | if (isAntDesignProPreview) { 59 | // 针对 preview.pro.ant.design 的 GA 统计代码 60 | plugins.push([ 61 | 'umi-plugin-ga', 62 | { 63 | code: 'UA-72788897-6', 64 | }, 65 | ]); 66 | plugins.push(['umi-plugin-antd-theme', themePluginConfig]); 67 | } 68 | 69 | export default { 70 | plugins, 71 | hash: true, 72 | targets: { 73 | ie: 11, 74 | }, 75 | // umi routes: https://umijs.org/zh/guide/router.html 76 | routes: pageRoutes, 77 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn 78 | theme: { 79 | // ...darkTheme, 80 | }, 81 | define: { 82 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 83 | 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 专用环境变量,请不要在你的项目中使用它。 84 | }, 85 | ignoreMomentLocale: true, 86 | lessLoaderOptions: { 87 | javascriptEnabled: true, 88 | }, 89 | disableRedirectHoist: true, 90 | cssLoaderOptions: { 91 | modules: true, 92 | getLocalIdent: (context, _, localName) => { 93 | if ( 94 | context.resourcePath.includes('node_modules') || 95 | context.resourcePath.includes('ant.design.pro.less') || 96 | context.resourcePath.includes('global.less') 97 | ) { 98 | return localName; 99 | } 100 | 101 | const match = context.resourcePath.match(/src(.*)/); 102 | 103 | if (match && match[1]) { 104 | const antdProPath = match[1].replace('.less', ''); 105 | const arr = slash(antdProPath) 106 | .split('/') 107 | .map(a => a.replace(/([A-Z])/g, '-$1')) 108 | .map(a => a.toLowerCase()); 109 | return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-'); 110 | } 111 | 112 | return localName; 113 | }, 114 | }, 115 | manifest: { 116 | basePath: '/', 117 | }, // chainWebpack: webpackPlugin, 118 | proxy: { 119 | '/api/': { 120 | target: 'http://127.0.0.1:10088', 121 | changeOrigin: true, 122 | }, 123 | }, 124 | }; 125 | -------------------------------------------------------------------------------- /config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | navTheme: "light", 3 | primaryColor: "#1890FF", 4 | layout: "topmenu", 5 | contentWidth: "Fluid", 6 | fixedHeader: true, 7 | autoHideHeader: true, 8 | fixSiderbar: false, 9 | menu: { 10 | "locale": true 11 | }, 12 | title: "权限管理脚手架", 13 | pwa: false, 14 | iconfontUrl: "", 15 | collapse: true, 16 | language: "zh-CN" 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 | export 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 | 'gg-editor-core', 44 | 'bizcharts-plugin-slider', 45 | ].includes(packageName); 46 | } 47 | 48 | return false; 49 | }, 50 | 51 | name(module) { 52 | const packageName = getModulePackageName(module); 53 | 54 | if (packageName) { 55 | if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) { 56 | return 'viz'; // visualization package 57 | } 58 | } 59 | 60 | return 'misc'; 61 | }, 62 | }, 63 | }, 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /config/route.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/login', 4 | component: '../layouts/UserLayout', 5 | routes: [ 6 | { 7 | name: 'login', 8 | path: '/login', 9 | component: './user/login', 10 | }, 11 | ], 12 | }, 13 | { 14 | path: '/', 15 | component: '../layouts/SecurityLayout', 16 | routes: [ 17 | { 18 | path: '/', 19 | component: '../layouts/BasicLayout', 20 | // authority: ['admin', 'user'], 21 | routes: [ 22 | { 23 | path: '/', 24 | redirect: '/welcome', 25 | }, 26 | { 27 | path: '/welcome', 28 | name: 'welcome', 29 | icon: 'smile', 30 | component: './Welcome', 31 | }, 32 | { 33 | path: '/demo', 34 | name: 'demo', 35 | icon: 'database', // component: './demo', 36 | }, 37 | { 38 | path: '/user', 39 | name: 'user', 40 | icon: 'user', 41 | component: './user/list', 42 | }, 43 | { 44 | path: '/system', 45 | name: 'system', 46 | icon: 'setting', 47 | routes: [ 48 | { 49 | path: '/system/menu', 50 | name: 'menu', 51 | icon: 'menu', 52 | component: './system/menu', 53 | }, 54 | { 55 | path: '/system/role', 56 | name: 'role', 57 | icon: 'user', 58 | component: './system/role', 59 | }, 60 | ], 61 | }, 62 | { 63 | component: './404', 64 | }, 65 | ], 66 | }, 67 | { 68 | component: './404', 69 | }, 70 | ], 71 | }, 72 | { 73 | component: './404', 74 | }, 75 | ]; 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | // ps https://github.com/GoogleChrome/puppeteer/issues/3120 2 | module.exports = { 3 | launch: { 4 | args: [ 5 | '--disable-gpu', 6 | '--disable-dev-shm-usage', 7 | '--no-first-run', 8 | '--no-zygote', 9 | '--no-sandbox', 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | preset: 'jest-puppeteer', 4 | globals: { 5 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 6 | localStorage: null, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ant-design-pro", 3 | "version": "1.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 | "fetch:blocks": "pro fetch-blocks && npm run prettier", 11 | "format-imports": "cross-env import-sort --write '**/*.{js,jsx,ts,tsx}'", 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": "check-prettier lint", 20 | "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", 21 | "prettier": "prettier -c --write \"**/*\"", 22 | "start": "umi dev", 23 | "start:no-mock": "cross-env MOCK=none umi dev", 24 | "start:no-ui": "cross-env UMI_UI=none umi dev", 25 | "test": "umi test", 26 | "test:all": "node ./tests/run-tests.js", 27 | "test:component": "umi test ./src/components", 28 | "ui": "umi ui" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "npm run lint-staged" 33 | } 34 | }, 35 | "lint-staged": { 36 | "**/*.less": "stylelint --syntax less", 37 | "**/*.{js,jsx,tsx,ts,less,md,json}": [ 38 | "prettier --write", 39 | "git add" 40 | ], 41 | "**/*.{js,jsx}": "npm run lint-staged:js", 42 | "**/*.{js,ts,tsx}": "npm run lint-staged:js" 43 | }, 44 | "browserslist": [ 45 | "> 1%", 46 | "last 2 versions", 47 | "not ie <= 10" 48 | ], 49 | "dependencies": { 50 | "@ant-design/pro-layout": "^4.8.3", 51 | "@antv/data-set": "^0.10.2", 52 | "antd": "^3.23.6", 53 | "classnames": "^2.2.6", 54 | "dva": "^2.4.1", 55 | "lodash": "^4.17.11", 56 | "md5": "^2.2.1", 57 | "moment": "^2.24.0", 58 | "omit.js": "^1.0.2", 59 | "path-to-regexp": "2.4.0", 60 | "qs": "^6.9.0", 61 | "react": "^16.8.6", 62 | "react-copy-to-clipboard": "^5.0.1", 63 | "react-dom": "^16.8.6", 64 | "react-helmet": "^5.2.1", 65 | "redux": "^4.0.1", 66 | "umi": "^2.8.7", 67 | "umi-plugin-antd-theme": "^1.0.1", 68 | "umi-plugin-pro-block": "^1.3.2", 69 | "umi-plugin-react": "^1.9.5", 70 | "umi-request": "^1.0.8", 71 | "uuid": "^3.3.3" 72 | }, 73 | "devDependencies": { 74 | "@ant-design/pro-cli": "^1.0.14", 75 | "@types/classnames": "^2.2.7", 76 | "@types/express": "^4.17.0", 77 | "@types/history": "^4.7.2", 78 | "@types/jest": "^24.0.13", 79 | "@types/lodash": "^4.14.144", 80 | "@types/qs": "^6.5.3", 81 | "@types/react": "^16.8.19", 82 | "@types/react-dom": "^16.8.4", 83 | "@types/react-helmet": "^5.0.13", 84 | "@umijs/fabric": "^2.0.0-beta.3", 85 | "chalk": "^3.0.0", 86 | "check-prettier": "^1.0.3", 87 | "cross-env": "^6.0.0", 88 | "cross-port-killer": "^1.1.1", 89 | "enzyme": "^3.9.0", 90 | "express": "^4.17.1", 91 | "gh-pages": "^2.0.1", 92 | "husky": "^3.0.0", 93 | "import-sort-cli": "^6.0.0", 94 | "import-sort-parser-babylon": "^6.0.0", 95 | "import-sort-parser-typescript": "^6.0.0", 96 | "import-sort-style-module": "^6.0.0", 97 | "jest-puppeteer": "^4.2.0", 98 | "lint-staged": "^9.0.0", 99 | "mockjs": "^1.0.1-beta3", 100 | "node-fetch": "^2.6.0", 101 | "prettier": "^1.19.1", 102 | "pro-download": "1.0.1", 103 | "stylelint": "^12.0.0", 104 | "umi-plugin-ga": "^1.1.3", 105 | "umi-plugin-pro": "^1.0.2", 106 | "umi-types": "^0.5.0" 107 | }, 108 | "optionalDependencies": { 109 | "puppeteer": "^1.17.0" 110 | }, 111 | "engines": { 112 | "node": ">=10.0.0" 113 | }, 114 | "checkFiles": [ 115 | "src/**/*.js*", 116 | "src/**/*.ts*", 117 | "src/**/*.less", 118 | "config/**/*.js*", 119 | "scripts/**/*.js" 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-react-pro4/d78023bb82df05a9763a11a98c3bd4f93cc45512/public/favicon.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-react-pro4/d78023bb82df05a9763a11a98c3bd4f93cc45512/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-react-pro4/d78023bb82df05a9763a11a98c3bd4f93cc45512/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-react-pro4/d78023bb82df05a9763a11a98c3bd4f93cc45512/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /screenshot_qq.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-react-pro4/d78023bb82df05a9763a11a98c3bd4f93cc45512/screenshot_qq.jpeg -------------------------------------------------------------------------------- /src/api.jsx: -------------------------------------------------------------------------------- 1 | const Domain = ''; /* 使用当前域名留空 */ 2 | const BaseUrl = `${Domain}/api`; 3 | 4 | export const Public = { 5 | Login: { 6 | Base: `${BaseUrl}/v1/pub/login`, 7 | Exit: `${BaseUrl}/v1/pub/login/exit`, 8 | GetCaptcha: `${BaseUrl}/v1/pub/login/captchaid`, 9 | ResCaptcha: `${BaseUrl}/v1/pub/login/captcha`, 10 | }, 11 | RefreshToken: `${BaseUrl}/v1/pub/refresh-token`, 12 | Current: { 13 | UpdatePassword: `${BaseUrl}/v1/pub/current/password`, 14 | GetUserInfo: `${BaseUrl}/v1/pub/current/user`, 15 | QueryUserMenuTree: `${BaseUrl}/v1/pub/current/menutree`, 16 | } 17 | }; 18 | 19 | export const Demo = { 20 | Base: `${BaseUrl}/v1/demos`, 21 | }; 22 | 23 | export const Menu = { 24 | Base: `${BaseUrl}/v1/menus`, 25 | Tree: `${BaseUrl}/v1/menus.tree`, 26 | }; 27 | 28 | export const Role = { 29 | Base: `${BaseUrl}/v1/roles`, 30 | Select: `${BaseUrl}/v1/roles.select`, 31 | }; 32 | 33 | export const User = { 34 | Base: `${BaseUrl}/v1/users`, 35 | }; 36 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | Authorized.Secured = Secured; 6 | Authorized.check = check; 7 | const RenderAuthorize = renderAuthorize(Authorized); 8 | export default RenderAuthorize; 9 | -------------------------------------------------------------------------------- /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 React from 'react'; 2 | import { Avatar, Icon, Menu, Spin } from 'antd'; 3 | import { FormattedMessage } from 'umi-plugin-react/locale'; 4 | import { connect } from 'dva'; 5 | import router from 'umi/router'; 6 | import HeaderDropdown from '../HeaderDropdown'; 7 | import styles from './index.less'; 8 | import UpdatePasswordDialog from '@/pages/user/components/UpdatePasswordDialog'; 9 | 10 | class AvatarDropdown extends React.Component { 11 | state = { 12 | updatePasswordVisible: false, 13 | }; 14 | 15 | onMenuClick = event => { 16 | const { key } = event; 17 | 18 | if (key === 'logout') { 19 | const { dispatch } = this.props; 20 | dispatch({ 21 | type: 'login/logout', 22 | }); 23 | return; 24 | } else if (key === 'password') { 25 | this.setState({ updatePasswordVisible: true }); 26 | return; 27 | } 28 | 29 | router.push(`/account/${key}`); 30 | }; 31 | 32 | handleCancel = () => { 33 | this.setState({ updatePasswordVisible: false }); 34 | }; 35 | 36 | render() { 37 | const { 38 | currentUser = { 39 | avatar: '', 40 | name: '', 41 | }, 42 | menu, 43 | } = this.props; 44 | const { updatePasswordVisible } = this.state; 45 | const menuHeaderDropdown = ( 46 | 47 | {menu && ( 48 | 49 | 50 | 51 | 52 | )} 53 | {menu && ( 54 | 55 | 56 | 57 | 58 | )} 59 | {menu && } 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | return currentUser && currentUser.real_name ? ( 72 | 73 | 74 | 75 | 78 | {currentUser.real_name} 79 | 80 | 81 | 83 | 84 | ) : ( 85 | 92 | ); 93 | } 94 | } 95 | 96 | export default connect(({ global }) => ({ 97 | currentUser: global.currentUser, 98 | }))(AvatarDropdown); 99 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.jsx: -------------------------------------------------------------------------------- 1 | import { Icon, Tooltip } 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 GlobalHeaderRight = props => { 11 | const { theme, layout } = props; 12 | let className = styles.right; 13 | 14 | if (theme === 'dark' && layout === 'topmenu') { 15 | className = `${styles.right} ${styles.dark}`; 16 | } 17 | 18 | return ( 19 |
20 | { 38 | console.log('input', value); 39 | }} 40 | onPressEnter={value => { 41 | console.log('enter', value); 42 | }} 43 | /> 44 | 49 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | export default connect(({ settings }) => ({ 65 | theme: settings.navTheme, 66 | layout: settings.layout, 67 | }))(GlobalHeaderRight); 68 | -------------------------------------------------------------------------------- /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: 100%; 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 | color: #fff; 49 | background-color: rgb(24, 144, 255); 50 | } 51 | } 52 | } 53 | 54 | .dark { 55 | height: @layout-header-height; 56 | .action { 57 | color: rgba(255, 255, 255, 0.85); 58 | > i { 59 | color: rgba(255, 255, 255, 0.85); 60 | } 61 | &:hover, 62 | &:global(.opened) { 63 | background: @primary-color; 64 | } 65 | } 66 | } 67 | 68 | :global(.ant-pro-global-header) { 69 | .dark { 70 | .action { 71 | color: @text-color; 72 | > i { 73 | color: @text-color; 74 | } 75 | &:hover { 76 | color: rgba(255, 255, 255, 0.85); 77 | > i { 78 | color: rgba(255, 255, 255, 0.85); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | @media only screen and (max-width: @screen-md) { 86 | :global(.ant-divider-vertical) { 87 | vertical-align: unset; 88 | } 89 | .name { 90 | display: none; 91 | } 92 | .right { 93 | position: absolute; 94 | top: 0; 95 | right: 12px; 96 | .account { 97 | .avatar { 98 | margin-right: 0; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /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 | export default class HeaderSearch extends Component { 7 | static defaultProps = { 8 | defaultActiveFirstOption: false, 9 | onPressEnter: () => {}, 10 | onSearch: () => {}, 11 | onChange: () => {}, 12 | className: '', 13 | placeholder: '', 14 | dataSource: [], 15 | defaultOpen: false, 16 | onVisibleChange: () => {}, 17 | }; 18 | 19 | static getDerivedStateFromProps(props) { 20 | if ('open' in props) { 21 | return { 22 | searchMode: props.open, 23 | }; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | inputRef = null; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | searchMode: props.defaultOpen, 35 | value: props.defaultValue, 36 | }; 37 | this.debouncePressEnter = debounce(this.debouncePressEnter, 500, { 38 | leading: true, 39 | trailing: false, 40 | }); 41 | } 42 | 43 | onKeyDown = e => { 44 | if (e.key === 'Enter') { 45 | this.debouncePressEnter(); 46 | } 47 | }; 48 | onChange = value => { 49 | if (typeof value === 'string') { 50 | const { onSearch, onChange } = this.props; 51 | this.setState({ 52 | value, 53 | }); 54 | 55 | if (onSearch) { 56 | onSearch(value); 57 | } 58 | 59 | if (onChange) { 60 | onChange(value); 61 | } 62 | } 63 | }; 64 | enterSearchMode = () => { 65 | const { onVisibleChange } = this.props; 66 | onVisibleChange(true); 67 | this.setState( 68 | { 69 | searchMode: true, 70 | }, 71 | () => { 72 | const { searchMode } = this.state; 73 | 74 | if (searchMode && this.inputRef) { 75 | this.inputRef.focus(); 76 | } 77 | }, 78 | ); 79 | }; 80 | leaveSearchMode = () => { 81 | this.setState({ 82 | searchMode: false, 83 | }); 84 | }; 85 | debouncePressEnter = () => { 86 | const { onPressEnter } = this.props; 87 | const { value } = this.state; 88 | onPressEnter(value || ''); 89 | }; 90 | 91 | render() { 92 | const { className, defaultValue, placeholder, open, ...restProps } = this.props; 93 | const { searchMode, value } = this.state; 94 | delete restProps.defaultOpen; // for rc-select not affected 95 | 96 | const inputClass = classNames(styles.input, { 97 | [styles.show]: searchMode, 98 | }); 99 | return ( 100 | { 104 | if (propertyName === 'width' && !searchMode) { 105 | const { onVisibleChange } = this.props; 106 | onVisibleChange(searchMode); 107 | } 108 | }} 109 | > 110 | 111 | 118 | { 120 | this.inputRef = node; 121 | }} 122 | defaultValue={defaultValue} 123 | aria-label={placeholder} 124 | placeholder={placeholder} 125 | onKeyDown={this.onKeyDown} 126 | onBlur={this.leaveSearchMode} 127 | /> 128 | 129 | 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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 | const { TabPane } = Tabs; 8 | export default class NoticeIcon extends Component { 9 | static Tab = NoticeList; 10 | static defaultProps = { 11 | onItemClick: () => {}, 12 | onPopupVisibleChange: () => {}, 13 | onTabChange: () => {}, 14 | onClear: () => {}, 15 | onViewMore: () => {}, 16 | loading: false, 17 | clearClose: false, 18 | emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', 19 | }; 20 | state = { 21 | visible: false, 22 | }; 23 | onItemClick = (item, tabProps) => { 24 | const { onItemClick } = this.props; 25 | 26 | if (onItemClick) { 27 | onItemClick(item, tabProps); 28 | } 29 | }; 30 | onClear = (name, key) => { 31 | const { onClear } = this.props; 32 | 33 | if (onClear) { 34 | onClear(name, key); 35 | } 36 | }; 37 | onTabChange = tabType => { 38 | const { onTabChange } = this.props; 39 | 40 | if (onTabChange) { 41 | onTabChange(tabType); 42 | } 43 | }; 44 | onViewMore = (tabProps, event) => { 45 | const { onViewMore } = this.props; 46 | 47 | if (onViewMore) { 48 | onViewMore(tabProps, event); 49 | } 50 | }; 51 | 52 | getNotificationBox() { 53 | const { children, loading, clearText, viewMoreText } = this.props; 54 | 55 | if (!children) { 56 | return null; 57 | } 58 | 59 | const panes = React.Children.map(children, child => { 60 | if (!child) { 61 | return null; 62 | } 63 | 64 | const { list, title, count, tabKey, showClear, showViewMore } = child.props; 65 | const len = list && list.length ? list.length : 0; 66 | const msgCount = count || count === 0 ? count : len; 67 | const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title; 68 | return ( 69 | 70 | this.onClear(title, tabKey)} 75 | onClick={item => this.onItemClick(item, child.props)} 76 | onViewMore={event => this.onViewMore(child.props, event)} 77 | showClear={showClear} 78 | showViewMore={showViewMore} 79 | title={title} 80 | {...child.props} 81 | /> 82 | 83 | ); 84 | }); 85 | return ( 86 | <> 87 | 88 | 89 | {panes} 90 | 91 | 92 | 93 | ); 94 | } 95 | 96 | handleVisibleChange = visible => { 97 | const { onPopupVisibleChange } = this.props; 98 | this.setState({ 99 | visible, 100 | }); 101 | 102 | if (onPopupVisibleChange) { 103 | onPopupVisibleChange(visible); 104 | } 105 | }; 106 | 107 | render() { 108 | const { className, count, popupVisible, bell } = this.props; 109 | const { visible } = this.state; 110 | const noticeButtonClass = classNames(className, styles.noticeButton); 111 | const notificationBox = this.getNotificationBox(); 112 | const NoticeBellIcon = bell || ; 113 | const trigger = ( 114 | 119 | 126 | {NoticeBellIcon} 127 | 128 | 129 | ); 130 | 131 | if (!notificationBox) { 132 | return trigger; 133 | } 134 | 135 | const popoverProps = {}; 136 | 137 | if ('popupVisible' in this.props) { 138 | popoverProps.visible = popupVisible; 139 | } 140 | 141 | return ( 142 | 151 | {trigger} 152 | 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /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 React from 'react'; 2 | import { Spin } from 'antd'; // loading components from code split 3 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 4 | 5 | const PageLoading = () => ( 6 |
12 | 13 |
14 | ); 15 | 16 | export default PageLoading; 17 | -------------------------------------------------------------------------------- /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 | const { pwa } = defaultSettings; // if pwa is true 6 | 7 | if (pwa) { 8 | // Notify user if offline now 9 | window.addEventListener('sw.offline', () => { 10 | message.warning( 11 | formatMessage({ 12 | id: 'app.pwa.offline', 13 | }), 14 | ); 15 | }); // Pop up a prompt on the page asking the user if they want to use the latest version 16 | 17 | window.addEventListener('sw.updated', event => { 18 | const e = event; 19 | 20 | const reloadSW = async () => { 21 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 22 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 23 | const worker = e.detail && e.detail.waiting; 24 | 25 | if (!worker) { 26 | return true; 27 | } // Send skip-waiting event to waiting SW with MessageChannel 28 | 29 | await new Promise((resolve, reject) => { 30 | const channel = new MessageChannel(); 31 | 32 | channel.port1.onmessage = msgEvent => { 33 | if (msgEvent.data.error) { 34 | reject(msgEvent.data.error); 35 | } else { 36 | resolve(msgEvent.data); 37 | } 38 | }; 39 | 40 | worker.postMessage( 41 | { 42 | type: 'skip-waiting', 43 | }, 44 | [channel.port2], 45 | ); 46 | }); // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 47 | 48 | window.location.reload(true); 49 | return true; 50 | }; 51 | 52 | const key = `open${Date.now()}`; 53 | const btn = ( 54 | 65 | ); 66 | notification.open({ 67 | message: formatMessage({ 68 | id: 'app.pwa.serviceworker.updated', 69 | }), 70 | description: formatMessage({ 71 | id: 'app.pwa.serviceworker.updated.hint', 72 | }), 73 | btn, 74 | key, 75 | onClose: async () => {}, 76 | }); 77 | }); 78 | } else if ('serviceWorker' in navigator) { 79 | // unregister service worker 80 | const { serviceWorker } = navigator; 81 | 82 | if (serviceWorker.getRegistrations) { 83 | serviceWorker.getRegistrations().then(sws => { 84 | sws.forEach(sw => { 85 | sw.unregister(); 86 | }); 87 | }); 88 | } 89 | 90 | serviceWorker.getRegistration().then(sw => { 91 | if (sw) sw.unregister(); 92 | }); // remove all caches 93 | 94 | if (window.caches && window.caches.keys) { 95 | caches.keys().then(keys => { 96 | keys.forEach(key => { 97 | caches.delete(key); 98 | }); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/link'; 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 | const noMatch = ( 17 | 23 | Go Login 24 | 25 | } 26 | /> 27 | ); 28 | 29 | /** 30 | * use Authorized check all menu item 31 | */ 32 | const menuDataRender = menuList => 33 | menuList.map(item => { 34 | const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] }; 35 | return Authorized.check(item.authority, localItem, null); 36 | }); 37 | 38 | const defaultFooterDom = ( 39 | 43 | ); 44 | 45 | const footerRender = () => { 46 | if (!isAntDesignPro()) { 47 | return defaultFooterDom; 48 | } 49 | 50 | return ( 51 | <> 52 | {defaultFooterDom} 53 |
59 | 60 | netlify logo 65 | 66 |
67 | 68 | ); 69 | }; 70 | 71 | const BasicLayout = props => { 72 | const { 73 | dispatch, 74 | children, 75 | settings, 76 | location = { 77 | pathname: '/', 78 | }, 79 | } = props; 80 | /** 81 | * constructor 82 | */ 83 | 84 | useEffect(() => { 85 | if (dispatch) { 86 | dispatch({ 87 | type: 'global/fetchCurrent', 88 | }); 89 | } 90 | }, []); 91 | /** 92 | * init variables 93 | */ 94 | 95 | const handleMenuCollapse = payload => { 96 | if (dispatch) { 97 | dispatch({ 98 | type: 'global/changeLayoutCollapsed', 99 | payload, 100 | }); 101 | } 102 | }; // get children authority 103 | 104 | const { global: { routes } } = props; 105 | if (routes.length > 0) { 106 | props.route.routes = routes; 107 | } 108 | 109 | const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || { 110 | authority: undefined, 111 | }; 112 | return ( 113 | ( 116 | 117 | {logoDom} 118 | {titleDom} 119 | 120 | )} 121 | onCollapse={handleMenuCollapse} 122 | menuItemRender={(menuItemProps, defaultDom) => { 123 | if (menuItemProps.isUrl || menuItemProps.children) { 124 | return defaultDom; 125 | } 126 | 127 | return {defaultDom}; 128 | }} 129 | breadcrumbRender={(routers = []) => [ 130 | { 131 | path: '/', 132 | breadcrumbName: formatMessage({ 133 | id: 'menu.home', 134 | defaultMessage: 'Home', 135 | }), 136 | }, 137 | ...routers, 138 | ]} 139 | itemRender={(route, params, routes, paths) => { 140 | const first = routes.indexOf(route) === 0; 141 | return first ? ( 142 | {route.breadcrumbName} 143 | ) : ( 144 | {route.breadcrumbName} 145 | ); 146 | }} 147 | footerRender={footerRender} 148 | menuDataRender={menuDataRender} 149 | formatMessage={formatMessage} 150 | rightContentRender={rightProps => } 151 | {...props} 152 | {...settings} 153 | > 154 | 155 | {children} 156 | 157 | 158 | ); 159 | }; 160 | 161 | export default connect(({ global, settings }) => ({ 162 | collapsed: global.collapsed, 163 | global, 164 | settings, 165 | }))(BasicLayout); 166 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout = ({ children }) =>
{children}
; 4 | 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Redirect } from 'umi'; 4 | import { stringify } from 'querystring'; 5 | import PageLoading from '@/components/PageLoading'; 6 | import store from '@/utils/store'; 7 | 8 | class SecurityLayout extends React.Component { 9 | state = { 10 | isReady: false, 11 | }; 12 | 13 | componentDidMount() { 14 | this.setState({ 15 | isReady: true, 16 | }); 17 | const { dispatch } = this.props; 18 | 19 | dispatch({ 20 | type: 'global/fetchCurrent', 21 | success: () => { 22 | dispatch({ 23 | type: 'global/fetchMenuTree', 24 | payload: { 25 | routes : this.props.route.routes[0].routes, 26 | }, 27 | }); 28 | }, 29 | }); 30 | } 31 | 32 | render() { 33 | const { isReady } = this.state; 34 | const { children, loading } = this.props; // You can replace it to your authentication rule (such as check token exists) 35 | // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) 36 | 37 | const isLogin = store.getAccessToken(); 38 | const queryString = stringify({ 39 | redirect: window.location.href, 40 | }); 41 | 42 | if ((!isLogin && loading) || !isReady) { 43 | return ; 44 | } 45 | 46 | if (!isLogin) { 47 | return ; 48 | } 49 | 50 | return children; 51 | } 52 | } 53 | 54 | export default connect(({ loading }) => ({ 55 | loading: loading.models.global, 56 | }))(SecurityLayout); 57 | -------------------------------------------------------------------------------- /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/link'; 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 | {formatMessage({ 48 | id: 'app.name', 49 | })} 50 | 51 |
52 |
{formatMessage({ 53 | id: 'app.desc', 54 | })}
55 |
56 | {children} 57 |
58 | 59 |
60 | 61 | ); 62 | }; 63 | 64 | export default connect(({ settings }) => ({ ...settings }))(UserLayout); 65 | -------------------------------------------------------------------------------- /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 user from './en-US/user'; 5 | import role from './en-US/role'; 6 | import pwa from './en-US/pwa'; 7 | import settingDrawer from './en-US/settingDrawer'; 8 | import settings from './en-US/settings'; 9 | export default { 10 | 'navBar.lang': 'Languages', 11 | 'layout.user.link.help': 'Help', 12 | 'layout.user.link.privacy': 'Privacy', 13 | 'layout.user.link.terms': 'Terms', 14 | 'app.name': 'gin-admin-react', 15 | 'app.desc': 'gin-admin-react Based on Ant Design Pro v4', 16 | 'app.welcome.title': 'gin-admin v5.2.0 released, start the experience with gin-admin-cli.', 17 | 'app.welcome.link.gin-admin-react': 'In case of problems, please keep gin-admin up to date. Still have issues, please report it', 18 | 'app.welcome.project.gin-admin': 'Use gin-admin-cli to quickly build gin-admin', 19 | ...globalHeader, 20 | ...menu, 21 | ...user, 22 | ...role, 23 | ...settingDrawer, 24 | ...settings, 25 | ...pwa, 26 | ...component, 27 | }; 28 | -------------------------------------------------------------------------------- /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 | 'component.name:': 'Name:', 6 | 'component.name:.content': 'Name: {content}', 7 | 'component.button.confirm': 'Confirm', 8 | 'component.button.cancel': 'Cancel', 9 | 'component.template': 'Template', 10 | 'component.template.basic': 'Basic', 11 | 'component.template.advanced': 'Advanced', 12 | 'component.option.all': 'All', 13 | 'component.option.normal': 'Normal', 14 | 'component.option.ban': 'Ban', 15 | 'component.option.show': 'Show', 16 | 'component.option.hidden': 'Hidden', 17 | 'component.operation': 'Operation', 18 | 'component.operation.edit': 'Edit', 19 | 'component.operation.create': 'Create', 20 | 'component.operation.delete': 'Delete', 21 | 'component.operation.update': 'Edit', 22 | 'component.operation.query': 'Query', 23 | 'component.operation.enable': 'Enable', 24 | 'component.operation.disable': 'Disable', 25 | 'component.operation.create.content': 'Create {content}', 26 | 'component.operation.update.content': 'Edit {content}', 27 | 'component.operation.delete.content': 'Delete {content}', 28 | 'component.operation.enable.content': 'Enable {content}', 29 | 'component.operation.disable.content': 'Disable {content}', 30 | 'component.operation.delete.confirm': 'Confirm deletion?', 31 | 'component.operation.reset': 'Reset', 32 | 'component.searchList.search': 'Search', 33 | 'component.searchList.reset': 'Reset', 34 | 'component.searchList.status': 'Status', 35 | 'component.searchList.result.total': 'Total {total} items', 36 | 'component.placeholder': 'Please type', 37 | 'component.placeholder.select': 'Please select', 38 | 'component.placeholder.content': 'Please type {content}', 39 | 'component.placeholder.select.content': 'Please select {content}', 40 | 'component.notification.request.success': 'Successful', 41 | 'component.notification.request.fail': 'Failed', 42 | 'component.notification.request.success.content': '{content} successful', 43 | 'component.notification.request.fail.content': '{content} failed', 44 | 'component.icon.only.support.official': 'Only official Icon is supported', 45 | 'component.order.description.desc': 'Descending order', 46 | 'component.order.description.asc': 'Ascending order', 47 | }; 48 | -------------------------------------------------------------------------------- /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.login': 'Login', 7 | 'menu.register': 'Register', 8 | 'menu.register.result': 'Register Result', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': 'Analysis', 11 | 'menu.dashboard.monitor': 'Monitor', 12 | 'menu.dashboard.workplace': 'Workplace', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': 'Form', 17 | 'menu.form.basic-form': 'Basic Form', 18 | 'menu.form.step-form': 'Step Form', 19 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 20 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 21 | 'menu.form.step-form.result': 'Step Form(finished)', 22 | 'menu.form.advanced-form': 'Advanced Form', 23 | 'menu.list': 'List', 24 | 'menu.list.table-list': 'Search Table', 25 | 'menu.list.basic-list': 'Basic List', 26 | 'menu.list.card-list': 'Card List', 27 | 'menu.list.search-list': 'Search List', 28 | 'menu.list.search-list.articles': 'Search List(articles)', 29 | 'menu.list.search-list.projects': 'Search List(projects)', 30 | 'menu.list.search-list.applications': 'Search List(applications)', 31 | 'menu.profile': 'Profile', 32 | 'menu.profile.basic': 'Basic Profile', 33 | 'menu.profile.advanced': 'Advanced Profile', 34 | 'menu.result': 'Result', 35 | 'menu.result.success': 'Success', 36 | 'menu.result.fail': 'Fail', 37 | 'menu.exception': 'Exception', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': 'Trigger', 42 | 'menu.account': 'Account', 43 | 'menu.account.center': 'Account Center', 44 | 'menu.account.settings': 'Account Settings', 45 | 'menu.account.trigger': 'Trigger Error', 46 | 'menu.account.logout': 'Logout', 47 | 'menu.account.password': 'Security', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | 'menu.demo': 'Demo Data', 53 | 'menu.user': 'Users', 54 | 'menu.system': 'System', 55 | 'menu.system.menu': 'Menus', 56 | 'menu.system.role': 'Roles', 57 | 58 | 'menu.field.name': 'Name', 59 | 'menu.field.sequence': 'Sequence', 60 | 'menu.field.icon': 'Icon', 61 | 'menu.field.router': 'Route', 62 | 'menu.field.hidden': 'Visible', 63 | 'menu.field.parentID': 'Parent ID', 64 | 'menu.field.parentPath': 'Path', 65 | 'menu.field.creator': 'Creator', 66 | 67 | 'menu.field.parentID.none': 'None', 68 | 69 | 'menu.operation.delete.refuse': 'Refusing to Delete', 70 | 'menu.operation.delete.refuse.preset': 'Refusing to Delete Preset', 71 | 'menu.operation.delete.refuse.submenu': 'Please delete the submenu under the menu first', 72 | 'menu.operation.delete.refuse.confirm': 'Confirm delete this menu?', 73 | 'menu.operation.delete.refuse.confirm.name': 'Confirm deletion please enter menu name', 74 | 75 | 'menu(s)': 'Menu(s)', 76 | 'menu.name': 'Menus', 77 | 'menu.perm': 'Menu permissions', 78 | 79 | 'menu.action.title': 'Actions', 80 | 'menu.action.perm': 'Actions', 81 | 'menu.action.code': 'Code', 82 | 'menu.action.name': 'Name', 83 | 'menu.action.template': 'Action Template', 84 | 85 | 'menu.resource.title': 'Resources', 86 | 'menu.resource.perm': 'Resources', 87 | 'menu.resource.code': 'Code', 88 | 'menu.resource.name': 'Name', 89 | 'menu.resource.name.example': 'e.g.: User Data', 90 | 'menu.resource.method': 'Method', 91 | 'menu.resource.uri': 'URI', 92 | 'menu.resource.uri.example': 'e.g.: /api/v1/users', 93 | 'menu.resource.template': 'Resource Template', 94 | 95 | 'menu.resource.query': '{content} Query', 96 | 'menu.resource.get': '{content} Get', 97 | 'menu.resource.create': '{content} Create', 98 | 'menu.resource.update': '{content} Update', 99 | 'menu.resource.delete': '{content} Delete', 100 | 'menu.resource.enable': '{content} Enable', 101 | 'menu.resource.disable': '{content} Disable', 102 | }; 103 | -------------------------------------------------------------------------------- /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/role.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'role(s)': 'Role(s)', 3 | 'role.title': 'Role', 4 | 'role.field.name': 'Name', 5 | 'role.field.sequence': 'Sequence', 6 | 'role.field.memo': 'Memo', 7 | 'role.field.creator': 'Creator', 8 | 9 | 'role.operation.delete.refuse.confirm': 'Confirm delete this menu?', 10 | }; 11 | -------------------------------------------------------------------------------- /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/en-US/user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user.title': 'User', 3 | 'user(s)': 'User(s)', 4 | 'user.field.userName': 'UserName', 5 | 'user.field.realName': 'RealName', 6 | 'user.field.roles': 'Roles', 7 | 'user.field.password': 'Password', 8 | 'user.field.email': 'E-mail', 9 | 'user.field.phone': 'PhoneNumber', 10 | 'user.field.status': 'Status', 11 | 'user.field.creator': 'Creator', 12 | 'user.field.createdAt': 'Created At', 13 | 'user.search.roles': 'Roles', 14 | 15 | 'user.password.change': 'Change password', 16 | 'user.password.update': 'Update password', 17 | 'user.password.oldPassword': 'Old password', 18 | 'user.password.newPassword': 'New password', 19 | 'user.password.confirmPassword': 'Confirm new password', 20 | 'user.password.desc': 'Make sure it\'s at least 15 characters OR at least 8 characters including a number and a lowercase letter.', 21 | 'user.password.notMatch': 'Password confirmation doesn\'t match the password', 22 | }; 23 | -------------------------------------------------------------------------------- /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 | export default { 8 | 'navBar.lang': 'Idiomas', 9 | 'layout.user.link.help': 'ajuda', 10 | 'layout.user.link.privacy': 'política de privacidade', 11 | 'layout.user.link.terms': 'termos de serviços', 12 | 'app.preview.down.block': 'Download this page to your local project', 13 | ...globalHeader, 14 | ...menu, 15 | ...settingDrawer, 16 | ...settings, 17 | ...pwa, 18 | ...component, 19 | }; 20 | -------------------------------------------------------------------------------- /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.register': 'Registro', 8 | 'menu.register.result': 'Resultado de registro', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': 'Análise', 11 | 'menu.dashboard.monitor': 'Monitor', 12 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': 'Formulário', 17 | 'menu.form.basic-form': 'Formulário Básico', 18 | 'menu.form.step-form': 'Formulário Assistido', 19 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 20 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 21 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 22 | 'menu.form.advanced-form': 'Formulário Avançado', 23 | 'menu.list': 'Lista', 24 | 'menu.list.table-list': 'Tabela de Busca', 25 | 'menu.list.basic-list': 'Lista Básica', 26 | 'menu.list.card-list': 'Lista de Card', 27 | 'menu.list.search-list': 'Lista de Busca', 28 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 29 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 30 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 31 | 'menu.profile': 'Perfil', 32 | 'menu.profile.basic': 'Perfil Básico', 33 | 'menu.profile.advanced': 'Perfil Avançado', 34 | 'menu.result': 'Resultado', 35 | 'menu.result.success': 'Sucesso', 36 | 'menu.result.fail': 'Falha', 37 | 'menu.exception': 'Exceção', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': 'Disparar', 42 | 'menu.account': 'Conta', 43 | 'menu.account.center': 'Central da Conta', 44 | 'menu.account.settings': 'Configurar Conta', 45 | 'menu.account.trigger': 'Disparar Erro', 46 | 'menu.account.logout': 'Sair', 47 | 'menu.editor': 'Graphic Editor', 48 | 'menu.editor.flow': 'Flow Editor', 49 | 'menu.editor.mind': 'Mind Editor', 50 | 'menu.editor.koni': 'Koni Editor', 51 | }; 52 | -------------------------------------------------------------------------------- /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 user from './zh-CN/user'; 5 | import role from './zh-CN/role'; 6 | import pwa from './zh-CN/pwa'; 7 | import settingDrawer from './zh-CN/settingDrawer'; 8 | import settings from './zh-CN/settings'; 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.name': '权限管理脚手架', 15 | 'app.desc': 'gin-admin-react 基于 Ant Design Pro v4', 16 | 'app.preview.down.block': '下载此页面到本地项目', 17 | 'app.welcome.title': 'gin-admin v5.2.0 现已发布,欢迎使用 gin-admin-cli 启动体验。', 18 | 'app.welcome.link.gin-admin-react': '测试中遇到问题,请先保证 gin-admin 版本为最新版。仍有问题,请提 issue', 19 | 'app.welcome.project.gin-admin': '使用 gin-admin-cli 快速构建 gin-admin', 20 | ...globalHeader, 21 | ...menu, 22 | ...user, 23 | ...role, 24 | ...settingDrawer, 25 | ...settings, 26 | ...pwa, 27 | ...component, 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | 'component.name:': '名称:', 6 | 'component.name:.content': '名称:{content}', 7 | 'component.button.confirm': '确认', 8 | 'component.button.cancel': '取消', 9 | 'component.template': '模板', 10 | 'component.template.basic': '简单', 11 | 'component.template.advanced': '高级', 12 | 'component.option.all': '全部', 13 | 'component.option.normal': '正常', 14 | 'component.option.ban': '停用', 15 | 'component.option.show': '显示', 16 | 'component.option.hidden': '隐藏', 17 | 'component.operation': '操作', 18 | 'component.operation.edit': '编辑', 19 | 'component.operation.create': '新增', 20 | 'component.operation.delete': '删除', 21 | 'component.operation.update': '更新', 22 | 'component.operation.query': '查询', 23 | 'component.operation.enable': '启用', 24 | 'component.operation.disable': '禁用', 25 | 'component.operation.create.content': '新增{content}', 26 | 'component.operation.delete.content': '删除{content}', 27 | 'component.operation.update.content': '编辑{content}', 28 | 'component.operation.enable.content': '启用{content}', 29 | 'component.operation.disable.content': '禁用{content}', 30 | 'component.operation.delete.confirm': '确认删除?', 31 | 'component.operation.reset': '重置', 32 | 'component.searchList.search': '查询', 33 | 'component.searchList.reset': '重置', 34 | 'component.searchList.status': '状态', 35 | 'component.searchList.result.total': '共 {total} 条', 36 | 'component.placeholder': '请输入', 37 | 'component.placeholder.select': '请选择', 38 | 'component.placeholder.content': '请输入{content}', 39 | 'component.placeholder.select.content': '请选择{content}', 40 | 'component.notification.request.success': '成功', 41 | 'component.notification.request.fail': '错误', 42 | 'component.notification.request.success.content': '{content}成功', 43 | 'component.notification.request.fail.content': '{content}错误', 44 | 'component.icon.only.support.official': '图标仅支持官方Icon图标', 45 | 'component.order.description.desc': '降序排列,越大越靠前', 46 | 'component.order.description.asc': '升序排列,越小越靠前', 47 | }; 48 | -------------------------------------------------------------------------------- /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.admin': '管理页', 6 | 'menu.login': '登录', 7 | 'menu.register': '注册', 8 | 'menu.register.result': '注册结果', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': '分析页', 11 | 'menu.dashboard.monitor': '监控页', 12 | 'menu.dashboard.workplace': '工作台', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': '表单页', 17 | 'menu.form.basic-form': '基础表单', 18 | 'menu.form.step-form': '分步表单', 19 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 20 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 21 | 'menu.form.step-form.result': '分步表单(完成)', 22 | 'menu.form.advanced-form': '高级表单', 23 | 'menu.list': '列表页', 24 | 'menu.list.table-list': '查询表格', 25 | 'menu.list.basic-list': '标准列表', 26 | 'menu.list.card-list': '卡片列表', 27 | 'menu.list.search-list': '搜索列表', 28 | 'menu.list.search-list.articles': '搜索列表(文章)', 29 | 'menu.list.search-list.projects': '搜索列表(项目)', 30 | 'menu.list.search-list.applications': '搜索列表(应用)', 31 | 'menu.profile': '详情页', 32 | 'menu.profile.basic': '基础详情页', 33 | 'menu.profile.advanced': '高级详情页', 34 | 'menu.result': '结果页', 35 | 'menu.result.success': '成功页', 36 | 'menu.result.fail': '失败页', 37 | 'menu.exception': '异常页', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': '触发错误', 42 | 'menu.account': '个人页', 43 | 'menu.account.center': '个人中心', 44 | 'menu.account.settings': '个人设置', 45 | 'menu.account.trigger': '触发报错', 46 | 'menu.account.logout': '退出登录', 47 | 'menu.account.password': '修改密码', 48 | 'menu.editor': '图形编辑器', 49 | 'menu.editor.flow': '流程编辑器', 50 | 'menu.editor.mind': '脑图编辑器', 51 | 'menu.editor.koni': '拓扑编辑器', 52 | 'menu.demo': '演示数据', 53 | 'menu.user': '用户', 54 | 'menu.system': '系统配置', 55 | 'menu.system.menu': '菜单管理', 56 | 'menu.system.role': '角色管理', 57 | 58 | 'menu.field.name': '菜单名称', 59 | 'menu.field.sequence': '排序值', 60 | 'menu.field.icon': '菜单图标', 61 | 'menu.field.router': '访问路由', 62 | 'menu.field.hidden': '可见状态', 63 | 'menu.field.parentID': '父级内码', 64 | 'menu.field.parentPath': '父级路径', 65 | 'menu.field.creator': '创建人', 66 | 67 | 'menu.field.parentID.none': '无上级目录', 68 | 69 | 'menu.operation.delete.refuse': '禁止删除菜单', 70 | 'menu.operation.delete.refuse.preset': '禁止删除预设菜单', 71 | 'menu.operation.delete.refuse.submenu': '请先删除菜单下的子菜单', 72 | 'menu.operation.delete.refuse.confirm': '确认删除这个菜单?', 73 | 'menu.operation.delete.refuse.confirm.name': '确认删除请输入菜单名称', 74 | 75 | 'menu(s)': '菜单', 76 | 'menu.name': '菜单名称', 77 | 'menu.perm': '菜单权限', 78 | 79 | 'menu.action.title': '动作管理', 80 | 'menu.action.perm': '动作权限', 81 | 'menu.action.code': '动作编号', 82 | 'menu.action.name': '动作名称', 83 | 'menu.action.template': '资源模板', 84 | 85 | 'menu.resource.title': '资源管理', 86 | 'menu.resource.perm': '资源权限', 87 | 'menu.resource.code': '资源编号', 88 | 'menu.resource.name': '资源名称', 89 | 'menu.resource.name.example': '例:用户数据', 90 | 'menu.resource.method': '请求方式', 91 | 'menu.resource.uri': 'URI', 92 | 'menu.resource.uri.example': '例:/api/v1/users', 93 | 'menu.resource.template': '资源模板', 94 | 95 | 'menu.resource.query': '查询{content}', 96 | 'menu.resource.get': '精确查询{content}', 97 | 'menu.resource.create': '创建{content}', 98 | 'menu.resource.update': '更新{content}', 99 | 'menu.resource.delete': '删除{content}', 100 | 'menu.resource.enable': '启用{content}', 101 | 'menu.resource.disable': '禁用{content}', 102 | }; 103 | -------------------------------------------------------------------------------- /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/role.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'role(s)': '角色', 3 | 'role.title': '角色', 4 | 'role.field.name': '角色名称', 5 | 'role.field.sequence': '排列顺序', 6 | 'role.field.memo': '备注', 7 | 'role.field.creator': '创建者', 8 | 9 | 'role.operation.delete.refuse.confirm': '确认删除这个菜单?', 10 | }; 11 | -------------------------------------------------------------------------------- /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-CN/user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user.title': '用户', 3 | 'user(s)': '用户', 4 | 'user.field.userName': '用户名', 5 | 'user.field.realName': '真实姓名', 6 | 'user.field.roles': '角色名称', 7 | 'user.field.password': '密码', 8 | 'user.field.email': '邮箱', 9 | 'user.field.phone': '手机号', 10 | 'user.field.status': '状态', 11 | 'user.field.creator': '创建者', 12 | 'user.field.createdAt': '创建时间', 13 | 'user.search.roles': '所属角色', 14 | 15 | 'user.password.change': '修改密码', 16 | 'user.password.update': '更新用户密码', 17 | 'user.password.oldPassword': '用户原密码', 18 | 'user.password.newPassword': '用户新密码', 19 | 'user.password.confirmPassword': '确认新密码', 20 | 'user.password.desc': 'Make sure it\'s at least 15 characters OR at least 8 characters including a number and a lowercase letter.', 21 | 'user.password.notMatch': '新密码与确认密码不一致', 22 | }; 23 | -------------------------------------------------------------------------------- /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 | export default { 8 | 'navBar.lang': '語言', 9 | 'layout.user.link.help': '幫助', 10 | 'layout.user.link.privacy': '隱私', 11 | 'layout.user.link.terms': '條款', 12 | 'app.preview.down.block': '下載此頁面到本地項目', 13 | 'app.welcome.title': 'gin-admin v5.2.0 現已發布,歡迎使用 gin-admin-cli 啟動體驗。', 14 | 'app.welcome.link.gin-admin-react': '測試中遇到問題,請先保證 gin-admin 版本為最新版。仍有問題,請提 issue', 15 | 'app.welcome.project.gin-admin': '使用 gin-admin-cli 快速構建 gin-admin', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /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.exception.403': '403', 8 | 'menu.exception.404': '404', 9 | 'menu.exception.500': '500', 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.form': '表單頁', 17 | 'menu.form.basic-form': '基礎表單', 18 | 'menu.form.step-form': '分步表單', 19 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 20 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 21 | 'menu.form.step-form.result': '分步表單(完成)', 22 | 'menu.form.advanced-form': '高級表單', 23 | 'menu.list': '列表頁', 24 | 'menu.list.table-list': '查詢表格', 25 | 'menu.list.basic-list': '標淮列表', 26 | 'menu.list.card-list': '卡片列表', 27 | 'menu.list.search-list': '搜索列表', 28 | 'menu.list.search-list.articles': '搜索列表(文章)', 29 | 'menu.list.search-list.projects': '搜索列表(項目)', 30 | 'menu.list.search-list.applications': '搜索列表(應用)', 31 | 'menu.profile': '詳情頁', 32 | 'menu.profile.basic': '基礎詳情頁', 33 | 'menu.profile.advanced': '高級詳情頁', 34 | 'menu.result': '結果頁', 35 | 'menu.result.success': '成功頁', 36 | 'menu.result.fail': '失敗頁', 37 | 'menu.account': '個人頁', 38 | 'menu.account.center': '個人中心', 39 | 'menu.account.settings': '個人設置', 40 | 'menu.account.trigger': '觸發報錯', 41 | 'menu.account.logout': '退出登錄', 42 | 'menu.exception': '异常页', 43 | 'menu.exception.not-permission': '403', 44 | 'menu.exception.not-find': '404', 45 | 'menu.exception.server-error': '500', 46 | 'menu.exception.trigger': '触发错误', 47 | 'menu.editor': '圖形編輯器', 48 | 'menu.editor.flow': '流程編輯器', 49 | 'menu.editor.mind': '腦圖編輯器', 50 | 'menu.editor.koni': '拓撲編輯器', 51 | 'menu.demo': '演示數據', 52 | 'menu.user': '用戶', 53 | 'menu.system': '系統配置', 54 | 'menu.system.menu': '菜單管理', 55 | 'menu.system.role': '角色管理', 56 | }; 57 | -------------------------------------------------------------------------------- /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": "权限管理脚手架", 3 | "short_name": "gin-admin-react", 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/login.js: -------------------------------------------------------------------------------- 1 | import { routerRedux } from 'dva/router'; 2 | import { stringify } from 'querystring'; 3 | import { 4 | accountLogin, 5 | accountLogout, 6 | getCaptcha, 7 | refreshCaptcha, 8 | updatePassword, 9 | } from '@/services/login'; 10 | import { getPageQuery } from '@/utils/utils'; 11 | import store from '@/utils/store'; 12 | import md5 from 'md5'; 13 | // import { setAuthority } from '@/utils/authority'; 14 | 15 | const Model = { 16 | namespace: 'login', 17 | state: { 18 | status: undefined, 19 | captchaID: '', 20 | }, 21 | effects: { 22 | *login({ payload, error }, { call, put }) { 23 | payload.password = md5(payload.password); 24 | const { response, data } = yield call(accountLogin, payload); 25 | yield put({ 26 | type: 'changeLoginStatus', 27 | payload: data, 28 | }); // Login successfully 29 | 30 | if (response.status === 200 && data.expires_at && data.expires_at > 0) { 31 | store.setAccessToken(data); 32 | 33 | const urlParams = new URL(window.location.href); 34 | const params = getPageQuery(); 35 | let { redirect } = params; 36 | 37 | if (redirect) { 38 | const redirectUrlParams = new URL(redirect); 39 | 40 | if (redirectUrlParams.origin === urlParams.origin) { 41 | redirect = redirect.substr(urlParams.origin.length); 42 | 43 | if (redirect.match(/^\/.*#/)) { 44 | redirect = redirect.substr(redirect.indexOf('#') + 1); 45 | } 46 | } else { 47 | window.location.href = '/'; 48 | return; 49 | } 50 | } 51 | 52 | yield put(routerRedux.replace(redirect || '/')); 53 | } else { 54 | if (error) error(); 55 | yield put({ type: 'getCaptcha' }); 56 | } 57 | }, 58 | 59 | *getCaptcha(_, { call, put }) { 60 | const { response, data } = yield call(getCaptcha); 61 | if (response.status === 200) { 62 | yield put({ 63 | type: 'saveCaptchaID', 64 | payload: data 65 | }) 66 | } 67 | }, 68 | 69 | *refreshCaptcha({ payload }, { call }) { 70 | yield call(refreshCaptcha, payload); 71 | }, 72 | 73 | *updatePassword({ payload, success }, { call, put }) { 74 | const { response } = yield call(updatePassword, payload); 75 | if (response.status === 200) { 76 | yield put({ type: 'global/requestSuccess', payload: { 77 | id: 'user.field.password', action: 'update', 78 | }}); 79 | if (success) success(); 80 | } 81 | }, 82 | 83 | *logout(_, { call, put }) { 84 | const { response } = yield call(accountLogout); 85 | if (response.status === 200) { 86 | store.clearAccessToken(); 87 | 88 | const { redirect } = getPageQuery(); // redirect 89 | if (window.location.pathname !== '/login' && !redirect) { 90 | yield put( 91 | routerRedux.replace({ 92 | pathname: '/login', 93 | search: stringify({ 94 | redirect: window.location.href, 95 | }), 96 | }), 97 | ); 98 | } 99 | } 100 | }, 101 | }, 102 | reducers: { 103 | changeLoginStatus(state, { payload }) { 104 | // setAuthority(payload.currentAuthority); 105 | return { ...state, status: true }; 106 | }, 107 | saveCaptchaID(state, { payload }) { 108 | return { ...state, captchaID: payload.captcha_id }; 109 | }, 110 | }, 111 | }; 112 | export default Model; 113 | -------------------------------------------------------------------------------- /src/models/menu.js: -------------------------------------------------------------------------------- 1 | import { create, query, queryTree, get, remove, update } from '@/services/menu'; 2 | 3 | const Model = { 4 | namespace: 'menu', 5 | state: { 6 | data: { 7 | list: [], 8 | pagination: {}, 9 | }, 10 | treeData: [], 11 | }, 12 | effects: { 13 | *fetch({ payload, success }, { call, put }) { 14 | const { response, data } = yield call(query, payload); 15 | if (response.status === 200) { 16 | yield put({ 17 | type: 'save', 18 | payload: data, 19 | }); 20 | yield put({ type: 'fetchTree' }); 21 | if (success) success(); 22 | } 23 | }, 24 | 25 | *get({ payload, success }, { call }) { 26 | const { response, data } = yield call(get, payload); 27 | if (response.status === 200) { 28 | if (success) success(data); 29 | } 30 | }, 31 | 32 | *fetchTree({ payload, success }, { call, put }) { 33 | const { response, data } = yield call(queryTree, payload); 34 | if (response.status === 200) { 35 | yield put({ 36 | type: 'saveTree', 37 | payload: data.list || [], 38 | }); 39 | /* 在角色管理页面可以输出 菜单树 JSON */ 40 | // yield put({ 41 | // type: 'outputMenus', 42 | // }); 43 | if (success) success(data.list); 44 | } 45 | }, 46 | 47 | *add({ payload, success }, { call, put }) { 48 | const { response } = yield call(create, payload); 49 | if (response.status === 200) { 50 | yield put({ type: 'global/requestSuccess', payload: { 51 | id: 'menu(s)', action: 'create', 52 | }}); 53 | if (success) success(); 54 | } 55 | }, 56 | 57 | *remove({ payload, success }, { call, put }) { 58 | const { response } = yield call(remove, payload); 59 | if (response.status === 200) { 60 | yield put({ type: 'global/requestSuccess', payload: { 61 | id: 'menu(s)', action: 'delete', 62 | }}); 63 | if (success) success(); 64 | } 65 | }, 66 | 67 | *update({ payload, success }, { call, put }) { 68 | const { response } = yield call(update, payload); 69 | if (response.status === 200) { 70 | yield put({ type: 'global/requestSuccess', payload: { 71 | id: 'menu(s)', action: 'update', 72 | }}); 73 | if (success) success(); 74 | } 75 | }, 76 | }, 77 | reducers: { 78 | updateData(state, { payload }) { 79 | const { data } = state; 80 | data.list = data.list.map(item => { 81 | if (item.record_id === payload.record_id) { 82 | return payload; 83 | } 84 | return item; 85 | }); 86 | return { 87 | ...state, 88 | data 89 | }; 90 | }, 91 | save(state, { payload }) { 92 | return { 93 | ...state, 94 | data: payload, 95 | }; 96 | }, 97 | saveTree(state, { payload }) { 98 | return { 99 | ...state, 100 | treeData: payload, 101 | }; 102 | }, 103 | /* 在角色管理页面可以输出 菜单树 JSON */ 104 | outputMenus(state) { 105 | let menus = JSON.parse(JSON.stringify(state.treeData)); 106 | menus.map(menu => { 107 | delete menu['parent_id']; 108 | delete menu['parent_path']; 109 | delete menu['hidden']; 110 | if (menu.children && menu.children.length > 0) { 111 | delete menu['router']; 112 | delete menu['actions']; 113 | delete menu['resources']; 114 | menu.children.map(menu => { 115 | delete menu['parent_id']; 116 | delete menu['parent_path']; 117 | delete menu['children']; 118 | delete menu['record_id']; 119 | delete menu['hidden']; 120 | }); 121 | Object.keys(menu.children).sort(); 122 | } else { 123 | delete menu['children']; 124 | } 125 | 126 | delete menu['record_id']; 127 | Object.keys(menu).sort(); 128 | }); 129 | console.log('menus', JSON.stringify(menus)); 130 | } 131 | }, 132 | }; 133 | export default Model; 134 | -------------------------------------------------------------------------------- /src/models/role.js: -------------------------------------------------------------------------------- 1 | import { create, get, query, remove, update } from '@/services/role'; 2 | 3 | const Model = { 4 | namespace: 'role', 5 | state: { 6 | data: { 7 | list: [], 8 | pagination: {}, 9 | }, 10 | treeData: [], 11 | }, 12 | effects: { 13 | *fetch({ payload, success }, { call, put }) { 14 | const { response, data } = yield call(query, payload); 15 | if (response.status === 200) { 16 | yield put({ 17 | type: 'save', 18 | payload: data, 19 | }); 20 | if (success) success(data.list); 21 | } 22 | }, 23 | 24 | *get({ payload, success }, { call }) { 25 | const { response, data } = yield call(get, payload); 26 | if (response.status === 200) { 27 | if (success) success(data); 28 | } 29 | }, 30 | 31 | *add({ payload, success }, { call, put }) { 32 | const { response } = yield call(create, payload); 33 | if (response.status === 200) { 34 | yield put({ type: 'global/requestSuccess', payload: { 35 | id: 'role(s)', action: 'create', 36 | }}); 37 | if (success) success(); 38 | } 39 | }, 40 | 41 | *remove({ payload, success }, { call, put }) { 42 | const { response } = yield call(remove, payload); 43 | if (response.status === 200) { 44 | yield put({ type: 'global/requestSuccess', payload: { 45 | id: 'role(s)', action: 'delete', 46 | }}); 47 | if (success) success(); 48 | } 49 | }, 50 | 51 | *update({ payload, success }, { call, put }) { 52 | const { response } = yield call(update, payload); 53 | if (response.status === 200) { 54 | yield put({ type: 'global/requestSuccess', payload: { 55 | id: 'role(s)', action: 'update', 56 | }}); 57 | if (success) success(); 58 | } 59 | }, 60 | }, 61 | reducers: { 62 | updateData(state, { payload }) { 63 | const { data } = state; 64 | data.list = data.list.map(item => { 65 | if (item.record_id === payload.record_id) { 66 | return payload; 67 | } 68 | return item; 69 | }); 70 | return { 71 | ...state, 72 | data 73 | }; 74 | }, 75 | save(state, { payload }) { 76 | return { 77 | ...state, 78 | data: payload, 79 | }; 80 | }, 81 | }, 82 | }; 83 | export default Model; 84 | -------------------------------------------------------------------------------- /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/user.js: -------------------------------------------------------------------------------- 1 | import { query, disable, enable, create, remove, update, get } from '@/services/user'; 2 | import md5 from 'md5'; 3 | 4 | const Model = { 5 | namespace: 'user', 6 | state: { 7 | data: { 8 | list: [], 9 | pagination: {}, 10 | }, 11 | }, 12 | effects: { 13 | *fetch({ payload, success }, { call, put }) { 14 | const { response, data } = yield call(query, payload); 15 | if (response.status === 200) { 16 | yield put({ 17 | type: 'save', 18 | payload: data, 19 | }); 20 | if (success) success(); 21 | } 22 | }, 23 | 24 | *get({ payload, success }, { call }) { 25 | const { response, data } = yield call(get, payload); 26 | if (response.status === 200) { 27 | if (success) success(data); 28 | } 29 | }, 30 | 31 | *add({ payload, success }, { call, put }) { 32 | payload.password = md5(payload.password); 33 | const { response } = yield call(create, payload); 34 | if (response.status === 200) { 35 | yield put({ type: 'global/requestSuccess', payload: { 36 | id: 'user(s)', action: 'create', 37 | }}); 38 | if (success) success(); 39 | } 40 | }, 41 | 42 | *remove({ payload, success }, { call, put }) { 43 | const { response } = yield call(remove, payload); 44 | if (response.status === 200) { 45 | yield put({ type: 'global/requestSuccess', payload: { 46 | id: 'user(s)', action: 'delete', 47 | }}); 48 | if (success) success(); 49 | } 50 | }, 51 | 52 | *update({ payload, success }, { call, put }) { 53 | if (payload.password) { 54 | payload.password = md5(payload.password); 55 | } 56 | const { response } = yield call(update, payload); 57 | if (response.status === 200) { 58 | yield put({ type: 'global/requestSuccess', payload: { 59 | id: 'user(s)', action: 'update', 60 | }}); 61 | if (success) success(); 62 | } 63 | }, 64 | 65 | *enable({ payload, callback }, { call, put }) { 66 | const { response } = yield call(enable, payload); 67 | if (response.status === 200) { 68 | yield put({ type: 'global/requestSuccess', payload: { 69 | id: 'user(s)', action: 'enable', 70 | }}); 71 | yield put({ 72 | type: 'updateData', 73 | payload: payload, 74 | }); 75 | } 76 | if (callback) callback(); 77 | }, 78 | 79 | *disable({ payload, callback }, { call, put }) { 80 | const { response } = yield call(disable, payload); 81 | if (response.status === 200) { 82 | yield put({ type: 'global/requestSuccess', payload: { 83 | id: 'user(s)', action: 'disable', 84 | }}); 85 | yield put({ 86 | type: 'updateData', 87 | payload: payload, 88 | }); 89 | } 90 | if (callback) callback(); 91 | }, 92 | }, 93 | reducers: { 94 | updateData(state, { payload }) { 95 | state.data.list = state.data.list.map(item => { 96 | if (item.record_id === payload.record_id) { 97 | return payload; 98 | } 99 | return item; 100 | }); 101 | return state; 102 | }, 103 | 104 | save(state, { payload }) { 105 | return { ...state, data: payload }; 106 | }, 107 | 108 | changeNotifyCount(state, { payload }) { 109 | return { ...state }; 110 | }, 111 | }, 112 | }; 113 | export default Model; 114 | -------------------------------------------------------------------------------- /src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import router from 'umi/router'; // 这里应该使用 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/Authorized.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Redirect from 'umi/redirect'; 3 | import { connect } from 'dva'; 4 | import pathToRegexp from 'path-to-regexp'; 5 | import Authorized from '@/utils/Authorized'; 6 | 7 | const getRouteAuthority = (path, routeData) => { 8 | let authorities; 9 | routeData.forEach(route => { 10 | if (route.authority) { 11 | authorities = route.authority; 12 | } // match prefix 13 | 14 | if (pathToRegexp(`${route.path}(.*)`).test(path)) { 15 | // exact match 16 | if (route.path === path) { 17 | authorities = route.authority || authorities; 18 | } // get children authority recursively 19 | 20 | if (route.routes) { 21 | authorities = getRouteAuthority(path, route.routes) || authorities; 22 | } 23 | } 24 | }); 25 | return authorities; 26 | }; 27 | 28 | const AuthComponent = ({ 29 | children, 30 | route = { 31 | routes: [], 32 | }, 33 | location = { 34 | pathname: '', 35 | }, 36 | global, 37 | }) => { 38 | const { currentUser } = global; 39 | const { routes = [] } = route; 40 | const isLogin = currentUser && currentUser.user_name; 41 | return ( 42 | : } 45 | > 46 | {children} 47 | 48 | ); 49 | }; 50 | 51 | export default connect(({ global }) => ({ 52 | global, 53 | }))(AuthComponent); 54 | -------------------------------------------------------------------------------- /src/pages/Welcome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageHeaderWrapper } from '@ant-design/pro-layout'; 3 | import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale'; 4 | import { Card, Typography, Alert } from 'antd'; 5 | import styles from './Welcome.less'; 6 | 7 | const CodePreview = ({ children }) => ( 8 |
 9 |     
10 |       {children}
11 |     
12 |   
13 | ); 14 | 15 | export default () => ( 16 | 17 | 18 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | https://github.com/gin-admin/gin-admin 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | go get -v github.com/LyricTian/gin-admin-cli && gin-admin-cli new -m -d ~/go/src/gin-admin -p gin-admin 51 | 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /src/pages/Welcome.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .pre { 4 | margin: 12px 0; 5 | padding: 12px 20px; 6 | background: @input-bg; 7 | box-shadow: @card-shadow; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/system/menu/components/MenuAction/AddDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Modal, Form, Row, Col, Checkbox, Radio, Button } from 'antd'; 3 | import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale'; 4 | 5 | const basicTpl = ['query', 'create', 'update', 'delete']; 6 | const advancedTpl = ['query', 'create', 'update', 'delete', 'enable', 'disable']; 7 | 8 | @Form.create() 9 | class AddDialog extends PureComponent { 10 | handleCancel = () => { 11 | const { onCancel } = this.props; 12 | if (onCancel) onCancel(); 13 | }; 14 | 15 | handleOK = () => { 16 | const { form, onSubmit } = this.props; 17 | form.validateFieldsAndScroll((err, values) => { 18 | if (!err) { 19 | const formData = { ...values }; 20 | onSubmit(formData); 21 | } 22 | }); 23 | }; 24 | 25 | render() { 26 | const { 27 | visible, 28 | form: { getFieldDecorator, setFieldsValue }, 29 | } = this.props; 30 | const formItemLayout = { 31 | labelCol: { 32 | xs: { span: 24 }, 33 | sm: { span: 6 }, 34 | }, 35 | wrapperCol: { 36 | xs: { span: 24 }, 37 | sm: { span: 16 }, 38 | }, 39 | }; 40 | 41 | return ( 42 | 58 |
59 | 62 | 63 | 64 | 65 | 66 | {getFieldDecorator('type', { 67 | initialValue: 1, 68 | rules: [{ required: true }] 69 | })( 70 | setFieldsValue({ actions: e.target.value === 1 ? basicTpl : advancedTpl }) 71 | }> 72 | 73 | 74 | 75 | 76 | 77 | 78 | )} 79 | 80 | 81 | 86 | 87 | 88 | 89 | 90 | {getFieldDecorator('actions', { 91 | initialValue: basicTpl, 92 | rules: [{ required: true }] 93 | })( 94 | 95 | Query 96 | Create 97 | Update 98 | Delete 99 | Enable 100 | Disable 101 | 102 | )} 103 | 104 | 105 | 106 |
107 |
108 | ); 109 | } 110 | } 111 | 112 | export default AddDialog; 113 | -------------------------------------------------------------------------------- /src/pages/system/menu/components/MenuAction/EditableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Form } from 'antd'; 3 | import { PureComponent } from 'react'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | 7 | const EditableContext = React.createContext(); 8 | 9 | const EditableRow = ({ form, index, ...props }) => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export const EditableFormRow = Form.create()(EditableRow); 16 | 17 | export class EditableCell extends PureComponent { 18 | save = e => { 19 | const { record, handleSave } = this.props; 20 | this.form.validateFields((error, values) => { 21 | if (error && error[e.currentTarget.id]) { 22 | return; 23 | } 24 | handleSave({ ...record, ...values }); 25 | }); 26 | }; 27 | 28 | renderCell = form => { 29 | this.form = form; 30 | const { dataIndex, record, title } = this.props; 31 | return 32 | {form.getFieldDecorator(dataIndex, { 33 | rules: [ 34 | { 35 | required: true, 36 | message: formatMessage({ id: 'component.placeholder.content' }, { 37 | content: title, 38 | }), 39 | }, 40 | ], 41 | initialValue: record[dataIndex], 42 | })( (this.input = node)} onPressEnter={this.save} onBlur={this.save} />)} 43 | 44 | }; 45 | 46 | render() { 47 | const { 48 | editable, 49 | dataIndex, 50 | title, 51 | record, 52 | index, 53 | handleSave, 54 | children, 55 | ...restProps 56 | } = this.props; 57 | return ( 58 | 59 | {editable ? ( 60 | {this.renderCell} 61 | ) : ( 62 | children 63 | )} 64 | 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/system/menu/components/MenuResource/EditableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Form, Select } from 'antd'; 3 | import { PureComponent } from 'react'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | const EditableContext = React.createContext(); 7 | 8 | const EditableRow = ({ form, index, ...props }) => ( 9 | 10 | 11 | 12 | ); 13 | 14 | export const EditableFormRow = Form.create()(EditableRow); 15 | 16 | export class EditableCell extends PureComponent { 17 | save = e => { 18 | const { record, handleSave } = this.props; 19 | this.form.validateFields((error, values) => { 20 | if (error && error[e.currentTarget.id]) { 21 | return; 22 | } 23 | handleSave({ ...record, ...values }); 24 | }); 25 | }; 26 | 27 | renderCell = form => { 28 | this.form = form; 29 | const { dataIndex, record, title } = this.props; 30 | 31 | if (dataIndex === 'method') { 32 | return ( 33 | 34 | {this.form.getFieldDecorator(dataIndex, { 35 | rules: [ 36 | { 37 | required: true, 38 | message: formatMessage({ id: 'component.placeholder.select.content' }, { 39 | content: title, 40 | }), 41 | }, 42 | ], 43 | initialValue: record[dataIndex], 44 | })( 45 | 59 | )} 60 | 61 | ); 62 | } 63 | return 64 | {form.getFieldDecorator(dataIndex, { 65 | rules: [ 66 | { 67 | required: true, 68 | message: formatMessage({ id: 'component.placeholder.content' }, { 69 | content: title, 70 | }), 71 | }, 72 | ], 73 | initialValue: record[dataIndex], 74 | })( (this.input = node)} onPressEnter={this.save} onBlur={this.save} />)} 75 | 76 | }; 77 | 78 | render() { 79 | const { 80 | editable, 81 | dataIndex, 82 | title, 83 | record, 84 | index, 85 | handleSave, 86 | children, 87 | ...restProps 88 | } = this.props; 89 | return ( 90 | 91 | {editable ? ( 92 | {this.renderCell} 93 | ) : ( 94 | children 95 | )} 96 | 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/pages/system/menu/components/StandardTable/index.jsx: -------------------------------------------------------------------------------- 1 | import { Table } from 'antd'; 2 | import React, { Component } from 'react'; 3 | import styles from './index.less'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | class StandardTable extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | selectedRowKeys: [], 11 | }; 12 | } 13 | 14 | handleRowSelectChange = (selectedRowKeys, selectedRows) => { 15 | const currySelectedRowKeys = selectedRowKeys; 16 | const { onSelectRow } = this.props; 17 | if (onSelectRow) { 18 | onSelectRow(selectedRows); 19 | } 20 | 21 | this.setState({ 22 | selectedRowKeys: currySelectedRowKeys, 23 | }); 24 | }; 25 | 26 | handleTableChange = (pagination, filters, sorter, ...rest) => { 27 | const { onChange } = this.props; 28 | if (onChange) { 29 | onChange(pagination, filters, sorter, ...rest); 30 | } 31 | }; 32 | 33 | render() { 34 | const { selectedRowKeys } = this.state; 35 | const { selectedRows, data, rowKey, ...rest } = this.props; 36 | const { list = [], pagination = false } = data || {}; 37 | const paginationProps = pagination ? { 38 | showSizeChanger: true, 39 | showQuickJumper: true, 40 | ...pagination, 41 | showTotal: total => formatMessage({ 42 | id: 'component.searchList.result.total' 43 | }, { total }), 44 | } : false; 45 | const rowSelection = { 46 | selectedRowKeys: selectedRows && selectedRows.length === 0 ? [] : selectedRowKeys, 47 | onChange: this.handleRowSelectChange, 48 | getCheckboxProps: record => ({ 49 | disabled: record.disabled, 50 | }), 51 | }; 52 | return ( 53 |
54 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | export default StandardTable; 68 | -------------------------------------------------------------------------------- /src/pages/system/menu/components/StandardTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardTable { 4 | :global { 5 | .ant-table-pagination { 6 | margin-top: 24px; 7 | } 8 | } 9 | 10 | .tableAlert { 11 | margin-bottom: 16px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/system/menu/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | user_name?: string; 3 | real_name: string; 4 | email: string; 5 | phone: string; 6 | status: number; 7 | created_at: Date; 8 | } 9 | 10 | export interface TableListPagination { 11 | total: number; 12 | pageSize: number; 13 | page: number; 14 | } 15 | 16 | export interface TableListData { 17 | list: TableListItem[]; 18 | pagination: Partial; 19 | } 20 | 21 | export interface TableListParams { 22 | sorter: string; 23 | status: string; 24 | name: string; 25 | pageSize: number; 26 | page: number; 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/system/menu/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/system/menu/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/system/role/components/RoleMenu/EditableCell.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Checkbox, Row, Col } from 'antd'; 3 | 4 | export class EditableCell extends PureComponent { 5 | findItem = () => { 6 | const { data, record } = this.props; 7 | for (let i = 0; i < data.length; i += 1) { 8 | if (data[i].menu_id === record.record_id) { 9 | return data[i]; 10 | } 11 | } 12 | return null; 13 | }; 14 | 15 | handleChange = values => { 16 | const { record, dataIndex, handleSave } = this.props; 17 | handleSave(record, dataIndex, values); 18 | }; 19 | 20 | renderAction = () => { 21 | const { record } = this.props; 22 | if (!record.actions || record.actions.length === 0) { 23 | return null; 24 | } 25 | 26 | const item = this.findItem(); 27 | return ( 28 | 33 | 34 | {record.actions.map(v => ( 35 | 36 | {v.name} 37 | 38 | ))} 39 | 40 | 41 | ); 42 | }; 43 | 44 | renderResource = () => { 45 | const { record } = this.props; 46 | if (!record.resources || record.resources.length === 0) { 47 | return null; 48 | } 49 | 50 | const item = this.findItem(); 51 | return ( 52 | 57 | 58 | {record.resources.map(v => ( 59 | 60 | {v.name} 61 | 62 | ))} 63 | 64 | 65 | ); 66 | }; 67 | 68 | render() { 69 | const { dataIndex, record, menuData, handleSave, ...restProps } = this.props; 70 | return ( 71 | 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/pages/system/role/components/StandardTable/index.jsx: -------------------------------------------------------------------------------- 1 | import { Table } from 'antd'; 2 | import React, { Component } from 'react'; 3 | import styles from './index.less'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | class StandardTable extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | selectedRowKeys: [], 11 | }; 12 | } 13 | 14 | handleRowSelectChange = (selectedRowKeys, selectedRows) => { 15 | const currySelectedRowKeys = selectedRowKeys; 16 | const { onSelectRow } = this.props; 17 | if (onSelectRow) { 18 | onSelectRow(selectedRows); 19 | } 20 | this.setState({ 21 | selectedRowKeys: currySelectedRowKeys, 22 | }); 23 | }; 24 | 25 | handleTableChange = (pagination, filters, sorter, ...rest) => { 26 | const { onChange } = this.props; 27 | 28 | if (onChange) { 29 | onChange(pagination, filters, sorter, ...rest); 30 | } 31 | }; 32 | 33 | render() { 34 | const { selectedRowKeys } = this.state; 35 | const { selectedRows, data, rowKey, ...rest } = this.props; 36 | const { list = [], pagination = false } = data || {}; 37 | const paginationProps = pagination ? { 38 | showSizeChanger: true, 39 | showQuickJumper: true, 40 | ...pagination, 41 | showTotal: total => formatMessage({ 42 | id: 'component.searchList.result.total' 43 | }, { total }), 44 | } : false; 45 | const rowSelection = { 46 | selectedRowKeys: selectedRows && selectedRows.length === 0 ? [] : selectedRowKeys, 47 | onChange: this.handleRowSelectChange, 48 | getCheckboxProps: record => ({ 49 | disabled: record.disabled, 50 | }), 51 | }; 52 | return ( 53 |
54 |
72 | {dataIndex === 'actions' && this.renderAction()} 73 | {dataIndex === 'resources' && this.renderResource()} 74 | {!(dataIndex === 'actions' || dataIndex === 'resources') && restProps.children} 75 |
62 | 63 | ); 64 | } 65 | } 66 | 67 | export default StandardTable; 68 | -------------------------------------------------------------------------------- /src/pages/system/role/components/StandardTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardTable { 4 | :global { 5 | .ant-table-pagination { 6 | margin-top: 24px; 7 | } 8 | } 9 | 10 | .tableAlert { 11 | margin-bottom: 16px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/system/role/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | user_name?: string; 3 | real_name: string; 4 | email: string; 5 | phone: string; 6 | status: number; 7 | created_at: Date; 8 | } 9 | 10 | export interface TableListPagination { 11 | total: number; 12 | pageSize: number; 13 | page: number; 14 | } 15 | 16 | export interface TableListData { 17 | list: TableListItem[]; 18 | pagination: Partial; 19 | } 20 | 21 | export interface TableListParams { 22 | sorter: string; 23 | status: string; 24 | name: string; 25 | pageSize: number; 26 | page: number; 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/system/role/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/system/role/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/user/list/components/RoleSelect.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Select } from 'antd'; 3 | import { connect } from 'dva'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | function parseValue(value) { 7 | if (!value) { 8 | return []; 9 | } 10 | return value.map(v => v.role_id); 11 | } 12 | 13 | @connect(({ role, loading }) => ({ 14 | role, 15 | loading: loading.models.role, 16 | })) 17 | export default class RoleSelect extends PureComponent { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | value: parseValue(props.value), 23 | data: [], 24 | }; 25 | } 26 | 27 | componentDidMount() { 28 | const { dispatch } = this.props; 29 | dispatch({ 30 | type: 'role/fetch', 31 | payload: { 32 | q: 'select', 33 | }, 34 | success: data => { 35 | this.setState({ data }); 36 | } 37 | }); 38 | } 39 | 40 | static getDerivedStateFromProps(nextProps, state) { 41 | if ('value' in nextProps) { 42 | return { ...state, value: parseValue(nextProps.value) }; 43 | } 44 | return state; 45 | } 46 | 47 | handleChange = value => { 48 | this.setState({ value }); 49 | this.triggerChange(value); 50 | }; 51 | 52 | triggerChange = data => { 53 | const { onChange } = this.props; 54 | if (onChange) { 55 | const newData = data.map(v => ({ role_id: v })); 56 | onChange(newData); 57 | } 58 | }; 59 | 60 | render() { 61 | const { value, data } = this.state; 62 | 63 | return ( 64 | 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/pages/user/list/components/StandardTable/index.jsx: -------------------------------------------------------------------------------- 1 | import { Table } from 'antd'; 2 | import React, { Component } from 'react'; 3 | import styles from './index.less'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | 6 | class StandardTable extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | selectedRowKeys: [], 11 | }; 12 | } 13 | 14 | handleRowSelectChange = (selectedRowKeys, selectedRows) => { 15 | const currySelectedRowKeys = selectedRowKeys; 16 | const { onSelectRow } = this.props; 17 | if (onSelectRow) { 18 | onSelectRow(selectedRows); 19 | } 20 | this.setState({ 21 | selectedRowKeys: currySelectedRowKeys, 22 | }); 23 | }; 24 | 25 | handleTableChange = (pagination, filters, sorter, ...rest) => { 26 | const { onChange } = this.props; 27 | 28 | if (onChange) { 29 | onChange(pagination, filters, sorter, ...rest); 30 | } 31 | }; 32 | 33 | render() { 34 | const { selectedRowKeys } = this.state; 35 | const { selectedRows, data, rowKey, ...rest } = this.props; 36 | const { list = [], pagination = false } = data || {}; 37 | const paginationProps = pagination ? { 38 | showSizeChanger: true, 39 | showQuickJumper: true, 40 | ...pagination, 41 | showTotal: total => formatMessage({ 42 | id: 'component.searchList.result.total' 43 | }, { total }), 44 | } : false; 45 | const rowSelection = { 46 | selectedRowKeys: selectedRows && selectedRows.length === 0 ? [] : selectedRowKeys, 47 | onChange: this.handleRowSelectChange, 48 | getCheckboxProps: record => ({ 49 | disabled: record.disabled, 50 | }), 51 | }; 52 | return ( 53 |
54 |
62 | 63 | ); 64 | } 65 | } 66 | 67 | export default StandardTable; 68 | -------------------------------------------------------------------------------- /src/pages/user/list/components/StandardTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardTable { 4 | :global { 5 | .ant-table-pagination { 6 | margin-top: 24px; 7 | } 8 | } 9 | 10 | .tableAlert { 11 | margin-bottom: 16px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/user/list/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | user_name?: string; 3 | real_name: string; 4 | email: string; 5 | phone: string; 6 | status: number; 7 | creator: string; 8 | created_at: Date; 9 | } 10 | 11 | export interface TableListPagination { 12 | total: number; 13 | pageSize: number; 14 | page: number; 15 | } 16 | 17 | export interface TableListData { 18 | list: TableListItem[]; 19 | pagination: Partial; 20 | } 21 | 22 | export interface TableListParams { 23 | sorter: string; 24 | status: string; 25 | name: string; 26 | pageSize: number; 27 | page: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/user/list/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | min-width: 80px; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/user/list/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/user/login/components/Login/LoginContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | const LoginContext = createContext({}); 3 | export default LoginContext; 4 | -------------------------------------------------------------------------------- /src/pages/user/login/components/Login/LoginSubmit.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from 'antd'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './index.less'; 5 | const FormItem = Form.Item; 6 | 7 | const LoginSubmit = ({ className, ...rest }) => { 8 | const clsString = classNames(styles.submit, className); 9 | return ( 10 | 11 |