├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .github └── issue_template.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc.json ├── .travis.yml ├── .umirc.js ├── LICENSE ├── README-zh_CN.md ├── README.md ├── assets └── standard.md ├── docs ├── .nojekyll ├── API-configuration.md ├── README.md ├── _media │ ├── favicon.ico │ ├── logo.svg │ ├── term_build.svg │ ├── term_i18n.svg │ ├── term_js_build.svg │ └── term_js_i18n.svg ├── _sidebar.md ├── change-log.md ├── configuration.md ├── deploy.md ├── faq.md ├── getting-started.md ├── i18n.md ├── index.html ├── layout.md ├── request.md ├── sw.js └── zh-cn │ ├── API-configuration.md │ ├── README.md │ ├── _sidebar.md │ ├── change-log.md │ ├── configuration.md │ ├── deploy.md │ ├── faq.md │ ├── getting-started.md │ ├── i18n.md │ ├── layout.md │ ├── request.md │ └── router.md ├── jest.config.js ├── manifest.json ├── mock ├── _utils.js ├── dashboard.js ├── post.js ├── route.js └── user.js ├── package.json ├── public ├── america.svg ├── china.svg ├── favicon.ico ├── logo.svg ├── logo │ ├── logo@128.png │ ├── logo@144.png │ ├── logo@152.png │ ├── logo@192.png │ ├── logo@384.png │ ├── logo@512.png │ ├── logo@72.png │ └── logo@96.png └── portugal.svg ├── scripts └── translate.js └── src ├── components ├── DropOption │ ├── DropOption.js │ └── package.json ├── Editor │ ├── Editor.js │ ├── Editor.less │ └── package.json ├── Ellipsis │ ├── index.d.ts │ ├── index.js │ ├── index.less │ ├── index.md │ └── index.test.js ├── FilterItem │ ├── FilterItem.js │ ├── FilterItem.less │ └── package.json ├── GlobalFooter │ ├── index.d.ts │ ├── index.js │ ├── index.less │ └── index.md ├── Layout │ ├── Bread.js │ ├── Bread.less │ ├── Header.js │ ├── Header.less │ ├── Menu.js │ ├── Sider.js │ ├── Sider.less │ └── index.js ├── Loader │ ├── Loader.js │ ├── Loader.less │ └── package.json ├── Page │ ├── Page.js │ ├── Page.less │ └── package.json ├── ScrollBar │ ├── index.js │ └── index.less └── index.js ├── e2e └── login.e2e.js ├── layouts ├── BaseLayout.js ├── BaseLayout.less ├── PrimaryLayout.js ├── PrimaryLayout.less ├── PublicLayout.js └── index.js ├── locales ├── en │ └── messages.json ├── pt-br │ └── messages.json └── zh │ └── messages.json ├── models └── app.js ├── pages ├── 404.less ├── 404.tsx ├── chart │ ├── ECharts │ │ ├── AirportCoordComponent.js │ │ ├── BubbleGradientComponent.js │ │ ├── CalendarComponent.js │ │ ├── ChartAPIComponent.js │ │ ├── ChartShowLoadingComponent.js │ │ ├── ChartWithEventComponent.js │ │ ├── DynamicChartComponent.js │ │ ├── EchartsComponent.js │ │ ├── GCalendarComponent.js │ │ ├── GaugeComponent.js │ │ ├── GraphComponent.js │ │ ├── LiquidfillComponent.js │ │ ├── LunarCalendarComponent.js │ │ ├── MainPageComponent.js │ │ ├── MapChartComponent.js │ │ ├── ModuleLoadChartComponent.js │ │ ├── SimpleChartComponent.js │ │ ├── ThemeChartComponent.js │ │ ├── TransparentBar3DComPonent.js │ │ ├── TreemapComponent.js │ │ ├── index.js │ │ ├── index.less │ │ ├── map │ │ │ └── js │ │ │ │ └── china.js │ │ └── theme │ │ │ ├── macarons.js │ │ │ └── shine.js │ ├── Recharts │ │ ├── AreaChartComponent.js │ │ ├── BarChartComponent.js │ │ ├── Container.js │ │ ├── Container.less │ │ ├── LineChartComponent.js │ │ ├── ReChartsComponent.js │ │ ├── index.js │ │ └── index.less │ └── highCharts │ │ ├── HighChartsComponent.js │ │ ├── HighMoreComponent.js │ │ ├── HighmapsComponent.js │ │ ├── HighstockComponent.js │ │ ├── index.js │ │ ├── index.less │ │ └── mapdata │ │ └── europe.js ├── dashboard │ ├── components │ │ ├── browser.js │ │ ├── browser.less │ │ ├── comments.js │ │ ├── comments.less │ │ ├── completed.js │ │ ├── completed.less │ │ ├── cpu.js │ │ ├── cpu.less │ │ ├── index.js │ │ ├── numberCard.js │ │ ├── numberCard.less │ │ ├── quote.js │ │ ├── quote.less │ │ ├── recentSales.js │ │ ├── recentSales.less │ │ ├── sales.js │ │ ├── sales.less │ │ ├── user-background.png │ │ ├── user.js │ │ ├── user.less │ │ ├── weather.js │ │ └── weather.less │ ├── index.js │ ├── index.less │ ├── model.js │ └── services │ │ ├── dashboard.js │ │ └── weather.js ├── editor │ └── index.js ├── index.js ├── login │ ├── index.js │ ├── index.less │ └── model.js ├── post │ ├── components │ │ ├── List.js │ │ └── List.less │ ├── index.js │ └── model.js ├── request │ ├── index.js │ └── index.less └── user │ ├── [id] │ ├── index.js │ ├── index.less │ └── models │ │ └── detail.js │ ├── components │ ├── Filter.js │ ├── List.js │ ├── List.less │ └── Modal.js │ ├── index.js │ └── model.js ├── plugins └── onError.js ├── services ├── api.js └── index.js ├── themes ├── default.less ├── index.less ├── mixin.less └── vars.less └── utils ├── city.js ├── config.js ├── constant.js ├── iconMap.jsx ├── index.js ├── index.test.js ├── model.js ├── request.js └── theme.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 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | HOST=0.0.0.0 3 | PORT=7000 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*-test.js 2 | src/public 3 | src/routes/chart/ECharts/theme 4 | src/routes/chart/highCharts/mapdata 5 | src/locales/_build/ 6 | src/locales/**/*.js 7 | docs/**/*.js 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "jsx-a11y/href-no-hash": "off", 5 | "no-console": "warn", 6 | "valid-jsdoc": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | > 请酌情提供细节,并保持issue精简便于查阅 2 | > 1. 功能需求 => 详细说明 3 | > 2. Bug反馈 4 | > 2.1 简单描述下报错 5 | > 2.2 你期望什么结果 6 | > 2.3 如何操作导致的 7 | > 2.4 可提供运行环境信息 8 | > 3. 代码求助 => 可以提,未必及时答复 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | yarn.lock 7 | package-lock.json 8 | 9 | # ide 10 | .idea 11 | 12 | # Mac General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | # umi 18 | .umi 19 | .umi-production 20 | 21 | # jslingui 22 | src/locales/_build 23 | src/locales/**/*.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | *.ejs 3 | .DS_Store 4 | .umi 5 | .umi-production 6 | src/locales/**/*.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"], 3 | "rules": { 4 | "declaration-empty-line-before": null, 5 | "no-descending-specificity": null, 6 | "selector-pseudo-class-no-unknown": null, 7 | "selector-pseudo-element-colon-notation": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | script: 5 | - npm run build 6 | before_install: 7 | - yarn global add now 8 | after_script: 9 | - | 10 | if [[ $TRAVIS_BRANCH == 'master' ]]; then 11 | echo $PROD_SITE_NOW_CONFIG >> dist/vercel.json && echo $PROD_DOC_NOW_CONFIG >> docs/vercel.json; 12 | else 13 | echo $DEV_SITE_NOW_CONFIG >> dist/vercel.json && echo $DEV_DOC_NOW_CONFIG >> docs/vercel.json; 14 | fi 15 | - cd ./dist 16 | - now -A vercel.json -t $NOW_TOKEN --prod -c 17 | - cd ../docs 18 | - now -A vercel.json -t $NOW_TOKEN --prod -c 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2016 zuiidea (zuiiidea@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /assets/standard.md: -------------------------------------------------------------------------------- 1 | ### 开发环境 2 | 3 | - OS:Windows 4 | - 编辑器:Atom 5 | - cmd:Cmder 6 | 7 | ### Atom 插件 8 | 9 | - [atom-beautify](https://atom.io/packages/atom-beautify) 10 | 11 | #### Less 12 | 13 | - [x] Beautify On Save 14 | 15 | #### Javascript 16 | 17 | - [ ] Disable Beautifying Language 18 | 19 | #### Markdown 20 | 21 | - [x] Beautify On Save 22 | - [x] Default Beautifier:Remark 23 | 24 | #### HTML 25 | 26 | - [x] Beautify On Save 27 | 28 | - [linter](https://atom.io/packages/linter) 29 | - [ ] Lint on Change 30 | - [linter-eslint](https://atom.io/packages/linter-eslint) 31 | - [x] Fix errors on save 32 | 33 | ### Cmder 主题 34 | 35 | - [Panda-Theme-Cmder](https://github.com/HamidFaraji/Panda-Theme-Cmder) 36 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/docs/.nojekyll -------------------------------------------------------------------------------- /docs/API-configuration.md: -------------------------------------------------------------------------------- 1 | # API configuration 2 | 3 | ## Why 4 | 5 | In the use of `redux` or `dva` projects, we often write functions like the following `service` layer to make the code structure clearer, but it is easy to see that we will write a lot of similar code in `antd -admin@5.0`, using the more concise configuration method to achieve the same function. 6 | 7 | ```javascript 8 | export async function login(data) { 9 | return request({ 10 | url: '/api/v1/user/login', 11 | method: 'post', 12 | data, 13 | }) 14 | } 15 | ``` 16 | 17 | ## Configuration and use 18 | 19 | In the `src/services/api.js` file, you will see the following configuration object, the object's key is used to call the function name, the object's value is the requested `url`, the default request method is `GET`, The format of the value of the other request mode object is `'method url'`. 20 | 21 | ```javascript 22 | export default { 23 | ... 24 | queryUser: '/user/:id', 25 | queryUserList: '/users', 26 | updateUser: 'Patch /user/:id', 27 | createUser: 'POST /user/:id', 28 | removeUser: 'DELETE /user/:id', 29 | removeUserList: 'POST /users/delete', 30 | ... 31 | } 32 | ``` 33 | 34 | Used in other files 35 | 36 | ```javascript 37 | import { queryUser } from 'api' 38 | 39 | // in the general file 40 | ... 41 | queryUser(option).then(data => console.log(data)) 42 | ... 43 | 44 | / / Model file 45 | ... 46 | yield call(queryUser, option) 47 | ... 48 | ``` 49 | 50 | ## Method to realize 51 | 52 | Refer to the `src/services/index.js` file to traverse the api configuration. Each property returns the corresponding encapsulated request function. 53 | 54 | ```javascript 55 | import request from 'utils/request' 56 | import { apiPrefix } from 'utils/config' 57 | 58 | import api from './api' 59 | 60 | const gen = params => { 61 | let url = apiPrefix + params 62 | let method = 'GET' 63 | 64 | const paramsArray = params.split(' ') 65 | if (paramsArray.length === 2) { 66 | method = paramsArray[0] 67 | url = apiPrefix + paramsArray[1] 68 | } 69 | 70 | return function(data) { 71 | return request({ 72 | url, 73 | data, 74 | method, 75 | }) 76 | } 77 | } 78 | 79 | const APIFunction = {} 80 | for (const key in api) { 81 | APIFunction[key] = gen(api[key]) 82 | } 83 | 84 | module.exports = APIFunction 85 | 86 | ``` -------------------------------------------------------------------------------- /docs/_media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/docs/_media/favicon.ico -------------------------------------------------------------------------------- /docs/_media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | <svg width="169px" height="141px" viewBox="0 0 169 141" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch --> 4 | <desc>Created with Sketch.</desc> 5 | <defs> 6 | <linearGradient x1="54.0428975%" y1="4.39752391%" x2="54.0428975%" y2="108.456714%" id="linearGradient-1"> 7 | <stop stop-color="#29CDFF" offset="0%"></stop> 8 | <stop stop-color="#148EFF" offset="62.3089445%"></stop> 9 | <stop stop-color="#0A60FF" offset="100%"></stop> 10 | </linearGradient> 11 | <linearGradient x1="50%" y1="14.2201464%" x2="50%" y2="113.263844%" id="linearGradient-2"> 12 | <stop stop-color="#FA816E" offset="0%"></stop> 13 | <stop stop-color="#F74A5C" offset="65.9092442%"></stop> 14 | <stop stop-color="#F51D2C" offset="100%"></stop> 15 | </linearGradient> 16 | </defs> 17 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 18 | <g id="Group" transform="translate(0.000000, -5.000000)"> 19 | <rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(83.718923, 75.312358) rotate(-24.000000) translate(-83.718923, -75.312358) " x="68.7189234" y="0.312357954" width="30" height="150" rx="15"></rect> 20 | <rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(129.009910, 75.580213) rotate(-24.000000) translate(-129.009910, -75.580213) " x="114.00991" y="0.580212739" width="30" height="150" rx="15"></rect> 21 | <circle id="Oval" fill="url(#linearGradient-2)" cx="25" cy="120" r="25"></circle> 22 | </g> 23 | </g> 24 | </svg> -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting started 2 | - [Quick Start](getting-started.md) 3 | - Customization 4 | - [Configuration](configuration.md) 5 | - [API Configuration](API-configuration.md) 6 | - [I18n](i18n.md) 7 | - [Layout](layout.md) 8 | - [Request](request.md) 9 | - Guide 10 | - [Deploy](deploy.md) 11 | - [Change Log](change-log.md) 12 | - [FAQ](faq.md) 13 | -------------------------------------------------------------------------------- /docs/change-log.md: -------------------------------------------------------------------------------- 1 | ## 5.0.0 2 | 3 | #### Optimization 4 | 5 | - Try to use decorators to simplify code writing and improve code readability. 6 | 7 | - API configurization to simplify the way data is obtained. 8 | 9 | - The files in `utils` are split and each has its own role. 10 | 11 | - Simplify the `utils/request` file without special handling. 12 | 13 | #### Specification 14 | 15 | - Functions add comments, parameters, return values, etc., ambiguous code adds comments, canonical reference [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html#appendices-jsdoc-tag-reference). 16 | 17 | - Semantic version number, specification participation [semantic version 2.0.0](https://semver.org/lang/zh-CN/). 18 | 19 | - Static code checking, unified code style, will use `prettier`, `stylelint`, `eslint` specification code before code submission. 20 | 21 | - Git submits information normalization, [git-commit-emoji-cn](https://github.com/liuchengxu/git-commit-emoji-cn). 22 | 23 | - Based on the pre-defined routing of `Umi`, there is no need to write a routing configuration file. 24 | 25 | - Use `React 16` new features such as `Fragment`, `Context`, `PureComponent`, etc. 26 | 27 | #### Features 28 | 29 | - Support internationalization, extract source fields from source code, load language packs on demand, and automatically translate online. 30 | 31 | - Support for the introduction `lodash` functions on demand. 32 | 33 | - Support multiple layouts, which rules can be used according to the rules. 34 | 35 | - Support Antd Admin to automatically compile and deploy on Travis. 36 | 37 | - Generate a documentation website using `Docsify`. 38 | 39 | 40 | #### Style 41 | 42 | - Added Antd Admin standalone Logo. 43 | 44 | - Rewrite the overall layout component, optimize the menu, automatic breadcrumb navigation, menu auto-expansion and other logic. 45 | 46 | - The mobile menu is changed to drawer. 47 | 48 | #### Other 49 | 50 | - Discard components such as `IconFont`, `Search`, `DataTable` because they are well supported and replaceable in `Antd`. -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | You can do some custom configuration in `/src/utils/config.js`: 4 | 5 | ## siteName 6 | 7 | - Type `String` 8 | 9 | Configure the site name, apply it to the login box, and display the title text at the top of the sidebar. 10 | 11 | ## copyright 12 | 13 | - Type: `String` 14 | 15 | Configure the copyright notice to apply to the login page, at the bottom of the `Primary` layout. 16 | 17 | ## logoPath 18 | 19 | - Type: `String` 20 | 21 | Configure the site logo to apply to the login box and the Logo display at the top of the sidebar. 22 | 23 | ## apiPrefix 24 | 25 | - Type: `String` 26 | 27 | Configure the prefix of the interface in the project. The interface related documents can be viewed [API configuration](API-configuration.md) 28 | 29 | ## fixedHeader 30 | 31 | - Type: `String` 32 | 33 | Under the `Primary` layout, whether the top of the page is fixed when scrolling。 34 | 35 | ## layouts 36 | 37 | - Type: `Array` 38 | 39 | Configuration? Which routes use which layout, unspecified route uses the default layout `Public`, the project currently has `Primary` and `Public` layouts, 40 | The default configuration is as follows: 41 | 42 | ```javascript 43 | layouts: [ 44 | { 45 | name: 'primary', 46 | include: [/.*/], 47 | exclude: [/(\/(en|zh))*\/login/], 48 | }, 49 | ], 50 | ``` 51 | 52 | The object properties for each layout are as follows: 53 | 54 | - `name` - The name of the layout; 55 | 56 | - `include` - Specifies a list of routing rules that use this layout, which can be a regular expression or a string; 57 | 58 | - `exclude` - Specifies a list of routing rules that do not use this layout, which can be a regular expression or a string. 59 | 60 | > Note: `exclude` takes precedence over `include`, and the layout previous it has a higher priority than the behind layout. The development process may need to be combined with the layout in the `src/layouts` directory. For details, see [Using Layout](./layout.md). 61 | 62 | ## i18n 63 | 64 | - Type: `Object` 65 | 66 | Configure internationalization, the default configuration is as follows: 67 | 68 | ```javascript 69 | i18n: { 70 | languages: [ 71 | { 72 | key: 'en', 73 | title: 'English', 74 | flag: '/america.svg', 75 | }, 76 | { 77 | key: 'zh', 78 | title: '中文', 79 | flag: '/china.svg', 80 | }, 81 | ], 82 | defaultLanguage: 'en', 83 | } 84 | ``` 85 | 86 | ### i18n.languages 87 | 88 | - Type: `Array` 89 | 90 | Specify which languages the app supports, and the object properties for each language are as follows: 91 | 92 | - `key` - The `key` of the language is applied to the page url to distinguish the language, and also corresponds to the language package folder name in the `src/locales` directory; 93 | 94 | - `title` - The name of the language, at the bottom of the login page, at the top of the `Primary` layout, the language switch is displayed; 95 | 96 | - `flag` - The path of the flag icon of the language, the language switching display at the top of the `Primary` layout. 97 | 98 | ### i18n.defaultLanguage 99 | 100 | - Type: `String` 101 | 102 | Configure the default language. 103 | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy 2 | 3 | After the development is completed and verified in the development environment, it needs to be deployed to our users. 4 | 5 |  6 | 7 | ## Build 8 | 9 | First execute the following command, 10 | 11 | ```bash 12 | npm run build 13 | ``` 14 | 15 | After a few seconds, the output should look like this: 16 | 17 | ```bash 18 | > antd-admin@5.0.0-beta build /Users/zuiidea/web/antd-admin 19 | > umi build 20 | 21 | [21:13:17] webpack compiled in 43s 868ms 22 | DONE Compiled successfully in 43877ms 21:13:17 23 | 24 | File sizes after gzip: 25 | 26 | 1.3 MB dist/vendors.async.js 27 | 308.21 KB dist/umi.js 28 | 45.49 KB dist/vendors.chunk.css 29 | 36.08 KB dist/p__chart__highCharts__index.async.js 30 | 33.53 KB dist/p__user__index.async.js 31 | 22.36 KB dist/p__chart__ECharts__index.async.js 32 | 4.21 KB dist/p__dashboard__index.async.js 33 | 4.06 KB dist/umi.css 34 | ... 35 | ``` 36 | 37 | The `build` command will package all resources, including JavaScript, CSS, web fonts, images, html, and more. You can find these files in the `dist/` directory. 38 | 39 | > If you have requirements for using HashHistory , deploying html to non-root directories, statics, etc., check out [Umi Deployment] (https://umijs.org/en/guide/deploy.html). 40 | 41 | ## Local verification 42 | 43 | 44 | Local verification can be done via `serve` before publishing. 45 | 46 | ``` 47 | $ yarn global add serve 48 | $ serve ./dist 49 | 50 | Serving! 51 | 52 | - Local: http://localhost:5000 53 | - On Your Network: http://{Your IP}:5000 54 | 55 | Copied local address to clipboard! 56 | 57 | ``` 58 | 59 | Access [http://localhost:5000](http://localhost:5000), under normal circumstances, it should be consistent with `npm start` (The API may not get the correct data). 60 | 61 | 62 | ## Deploy 63 | 64 | Next, we can upload the static file to the server. If you use Nginx as the Web server, you can configure it in `ngnix.conf`: 65 | ``` 66 | server 67 | { 68 | listen 80; 69 | # Specify an accessible domain name 70 | server_name antd-admin.zuiidea.com; 71 | # The directory where the compiled files are stored 72 | root /home/www/antd-admin/dist; 73 | 74 | # Proxy server interface to avoid cross-domain 75 | location /api { 76 | proxy_pass http://localhost:7000/api; 77 | } 78 | 79 | Because the front end uses BrowserHistory, it will route backback to index.html 80 | location / { 81 | index index.html; 82 | try_files $uri $uri/ /index.html; 83 | } 84 | } 85 | ``` 86 | 87 | Restart the web server and access [http://antd-admin.zuiidea.com](http://antd-admin.zuiidea.com) , You will see the correct page. 88 | 89 | ```bash 90 | nginx -s reload 91 | ``` 92 | 93 | Similarly, if you use Caddy as a web server, you can do this in `Caddyfile`: 94 | 95 | ``` 96 | antd-admin.zuiidea.com { 97 | gzip 98 | root /home/www/antd-admin/dist 99 | proxy /api http://localhost:7000 100 | 101 | rewrite { 102 | if {path} not_match ^/api 103 | to {path} {path}/ / 104 | } 105 | } 106 | 107 | 108 | antd-admin.zuiidea.com/public { 109 | gzip 110 | root /home/www/antd-admin/dist/static/public 111 | } 112 | 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | Most asked 4 | 5 | ## create new page 6 | 7 | 1. just copy a page in /src/pages (route here auto generated by [umi](https://umijs.org/guide/router.html#basic-routing)) 8 | 2. modify namespace/pathToRegexp in model.js 9 | 3. modify mock route.js to add a route -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | > Before delving into Ant Design React, a good knowledge base of [React](http://facebook.github.io/react/) 、 [ES2015+](http://es6.ruanyifeng.com/) 、 [Antd Design](https://ant.design/docs/react/introduce-cn) . Learn about [UmiJS](https://umijs.org/) , [Dva](http://github.com/dvajs/dva) . And properly installed and configured [Node.js](https://nodejs.org/) v8 or above, [Git](https://git-scm.com/). It would be helpful if you have pre-existing knowledge on those. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | git clone https://github.com/zuiidea/antd-admin.git my-project 9 | cd my-project 10 | ``` 11 | 12 | ## Scaffolding 13 | 14 | The project layout is as follows: 15 | 16 | ```bash 17 | ├── dist/ # Default build output directory 18 | ├── mock/ # Mock files 19 | ├── public/ # Static resource 20 | ├── src/ # Source code 21 | │ ├── components/ # Components 22 | │ ├── e2e/ # Integrated Test Case 23 | │ ├── layouts/ # Common Layouts 24 | │ ├── locales/ # i18n resources 25 | │ ├── models/ # Global dva Model 26 | │ ├── pages/ # Sub-pages and templates 27 | │ ├── services/ # Backend Services 28 | │ │ ├── api.js # API configuration 29 | │ │ └── index.js # API export 30 | │ ├── themes/ # Themes 31 | │ │ ├── default.less # Less variable 32 | │ │ ├── index.less # Global style 33 | │ │ ├── mixin.less # Less mixin 34 | │ │ └── vars.less # Less variable and mixin 35 | │ ├── utils/ # Utility 36 | │ │ ├── config.js # Application configuration 37 | │ │ ├── constant.js # Static constant 38 | │ │ ├── index.js # Utility methods 39 | │ │ ├── request.js # Request function(axios) 40 | │ │ └── theme.js # Style variables used in js 41 | ├── .editorconfig 42 | ├── .env 43 | ├── .eslintrc 44 | ├── .gitignore 45 | ├── .prettierignore 46 | ├── .prettierrc 47 | ├── .stylelintrc.json 48 | ├── .travis.yml 49 | └── .umirc.js 50 | └── package.json 51 | ``` 52 | 53 | ## Development 54 | 55 | 1. Install Dependencies. 56 | 57 | ```bash 58 | yarn install 59 | ``` 60 | 61 | Or 62 | 63 | ```bash 64 | npm install 65 | ``` 66 | 67 | 2. Start local server. 68 | 69 | ```bash 70 | npm run start 71 | ``` 72 | 73 | 3. After the startup is complete, open a browser and visit [http://localhost:7000](http://localhost:7000), If you need to change the startup port, you can configure it in the `.env` file. 74 | -------------------------------------------------------------------------------- /docs/i18n.md: -------------------------------------------------------------------------------- 1 | # globalization 2 | 3 | ## Add language 4 | 5 | Take Japanese as an example. 6 | 7 |  8 | 9 | 1. Add a language pack local file, `ja` is the Japanese language code, and a list of languages that support translation [有道智云](http://ai.youdao.com/docs/doc-trans-api.s#p05), the `src/locales/ja/messages.json` file will be generated after running the following command. 10 | 11 | ```bash 12 | npm run add-locale ja 13 | ``` 14 | 15 | 2. Extract the fields in the code that need to be translated, ie `<Trans>?message</Trans>`, `` t`message `` in the `message` field, run the following command after `src/locales/ja /messages.json` will appear after the extracted field configuration. 16 | 17 | ```bash 18 | npm run extract 19 | ``` 20 | 21 | You will see the following information: 22 | 23 | ```bash 24 | Catalog statistics: 25 | ┌─────────────┬─────────────┬─────────┐ 26 | │ Language │ Total count │ Missing │ 27 | ├─────────────┼─────────────┼─────────┤ 28 | │ en (source) │ 52 │ - │ 29 | │ ja │ 52 │ 52 │ 30 | │ zh │ 52 │ 0 │ 31 | └─────────────┴─────────────┴─────────┘ 32 | ``` 33 | 34 | 3. At the same time, we have added the relevant configuration in `src/utils/config.js`. 35 | 36 | ```javascript 37 | { 38 | ... 39 | i18n: { 40 | languages: [ 41 | ... 42 | { 43 | key:'ja', 44 | title: '日本語', 45 | flag: '/japanese.svg', 46 | }, 47 | ], 48 | }, 49 | } 50 | ``` 51 | 52 | > Routing related effects, after the configuration `npm run start` takes effect after restart. 53 | 54 | 4. Use the built-in commands for automatic translation. You will see the translated configuration in `src/locales/ja/messages.json`. 55 | 56 | ```bash 57 | npm run trans:only 58 | ``` 59 | 60 | You will see the following information: 61 | 62 | ```bash 63 | start: en -> ja 64 | ... 65 | youdao: en -> ja: Unpublished -> 未発表 66 | youdao: en -> ja: Update -> 更新 67 | youdao: en -> ja: Update User -> ユーザーの更新 68 | youdao: en -> ja: Username -> 名 69 | ... 70 | All translations have been completed. 71 | ``` 72 | 73 | > `npm run trans` will execute `npm run extract` and `npm run trans:only` in order. 74 | 75 | 5. Finally, you can adjust the inaccurate fields in `src/locales/ja/messages.json`. Start the development mode `npm run start`, open [http://localhost:7000/ja/login](http://localhost:7000/ja/login) and you will see the Japanese version of the app. 76 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8"> 6 | <title>antd-admin - An admin dashboard application demo built upon Ant Design and UmiJS</title> 7 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 8 | <meta name="description" content="An admin dashboard application demo built upon Ant Design and UmiJS"> 9 | <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> 10 | <link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css"> 11 | <link rel="icon" href="/_media/favicon.ico" /> 12 | </head> 13 | 14 | <body> 15 | <nav data-cloak class="app-nav"> 16 | <a href="/">En</a> 17 | <a href="#/zh-cn/">中文</a> 18 | </nav> 19 | <div id="app">Loading...</div> 20 | <script> 21 | window.$docsify = { 22 | name: 'AntD Admin', 23 | loadSidebar: true, 24 | maxLevel: 3, 25 | subMaxLevel: 3, 26 | auto2top: true, 27 | autoHeader: true, 28 | repo: 'zuiidea/antd-admin', 29 | themeColor: '#1890ff', 30 | search: { 31 | paths: 'auto', 32 | placeholder: { 33 | '/zh-cn/': '搜索', 34 | '/': 'Type to search' 35 | }, 36 | 37 | noData: { 38 | '/zh-cn/': '找不到结果', 39 | '/': 'No Results' 40 | } 41 | } 42 | } 43 | 44 | // if (typeof navigator.serviceWorker !== 'undefined') { 45 | // navigator.serviceWorker.register('sw.js') 46 | // } 47 | </script> 48 | <script src="//unpkg.com/docsify/lib/docsify.min.js"></script> 49 | <script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script> 50 | <script src="//unpkg.com/docsify/lib/plugins/emoji.min.js"></script> 51 | <script src="//unpkg.com/docsify/lib/plugins/zoom-image.min.js"></script> 52 | <script src="//unpkg.com/docsify-copy-code"></script> 53 | </body> 54 | 55 | </html> -------------------------------------------------------------------------------- /docs/layout.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | 3 | ## Add a new layout 4 | 5 | Take a new layout named `secondary` as an example to make the route starting with `secondary` use this layout. 6 | 7 | 1. Add related configuration in `src/utils/config.js`. For details, please refer to [layouts](/configuration?id=layouts). 8 | 9 | ```javascript 10 | layouts: [ 11 | { 12 | name: 'primary', 13 | include: [/.*/], 14 | exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/], 15 | }, 16 | { 17 | name: 'secondary', 18 | include: [/(\/(en|zh))*\/secondary\/(.*)/], 19 | }, 20 | ], 21 | ``` 22 | 23 | 2. Add the `secondary` layout component to the `src/layouts/BaseLayout.js` file. 24 | 25 | ```javascript 26 | import SecondaryLayout from './SecondaryLayout' 27 | 28 | const LayoutMap = { 29 | primary: PrimaryLayout, 30 | public: PublicLayout, 31 | secondary: SecondaryLayout, 32 | } 33 | ``` 34 | 35 | 3. Add the `SecondaryLayout.js` file to the `src/layouts/` directory. 36 | 37 | ```javascript 38 | import React from 'react' 39 | 40 | export default ({ children }) => { 41 | return ( 42 | <div> 43 | <h1>Secondary</h1> 44 | {children} 45 | </div> 46 | ) 47 | } 48 | ``` 49 | 50 | 4. Add a `secondary/index.js` file to the `src/pages/` directory. 51 | 52 | ```javascript 53 | import React from 'react' 54 | 55 | export default ({ children }) => { 56 | Return <div>Secondary page Content</div> 57 | } 58 | ``` 59 | 60 | 5. Finally, start the development mode `npm run start`, open [http://localhost:7000/secondary/](http://localhost:7000/secondary/) and you will see the page for the `secondary` layout. 61 | -------------------------------------------------------------------------------- /docs/request.md: -------------------------------------------------------------------------------- 1 | # HTTP request 2 | 3 | this project use axios for http service, file located in src/utils/request.js 4 | 5 | ## Custom Header 6 | 7 | As for privilege access or modify cookie, you could add header param by yourself 8 | 9 | ``` 10 | axios.defaults.headers.common['Authorization'] = 'token' 11 | ``` 12 | 13 | Or 14 | 15 | ``` 16 | axios.interceptors.request.use(function (config) { 17 | config.headers.token = window.localStorage.getItem('token'); 18 | return config; 19 | }, function (error) { 20 | return Promise.reject(error); 21 | }); 22 | ``` -------------------------------------------------------------------------------- /docs/sw.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * docsify sw.js 3 | * =========================================================== 4 | * Copyright 2016 @huxpro 5 | * Licensed under Apache 2.0 6 | * Register service worker. 7 | * ========================================================== */ 8 | 9 | const RUNTIME = 'docsify' 10 | const HOSTNAME_WHITELIST = [ 11 | self.location.hostname, 12 | 'fonts.gstatic.com', 13 | 'fonts.googleapis.com', 14 | 'unpkg.com' 15 | ] 16 | 17 | // The Util Function to hack URLs of intercepted requests 18 | const getFixedUrl = (req) => { 19 | var now = Date.now() 20 | var url = new URL(req.url) 21 | 22 | // 1. fixed http URL 23 | // Just keep syncing with location.protocol 24 | // fetch(httpURL) belongs to active mixed content. 25 | // And fetch(httpRequest) is not supported yet. 26 | url.protocol = self.location.protocol 27 | 28 | // 2. add query for caching-busting. 29 | // Github Pages served with Cache-Control: max-age=600 30 | // max-age on mutable content is error-prone, with SW life of bugs can even extend. 31 | // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. 32 | // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 33 | if (url.hostname === self.location.hostname) { 34 | url.search += (url.search ? '&' : '?') + 'cache-bust=' + now 35 | } 36 | return url.href 37 | } 38 | 39 | /** 40 | * @Lifecycle Activate 41 | * New one activated when old isnt being used. 42 | * 43 | * waitUntil(): activating ====> activated 44 | */ 45 | self.addEventListener('activate', event => { 46 | event.waitUntil(self.clients.claim()) 47 | }) 48 | 49 | /** 50 | * @Functional Fetch 51 | * All network requests are being intercepted here. 52 | * 53 | * void respondWith(Promise<Response> r) 54 | */ 55 | self.addEventListener('fetch', event => { 56 | // Skip some of cross-origin requests, like those for Google Analytics. 57 | if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) { 58 | // Stale-while-revalidate 59 | // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale 60 | // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 61 | const cached = caches.match(event.request) 62 | const fixedUrl = getFixedUrl(event.request) 63 | const fetched = fetch(fixedUrl, { cache: 'no-store' }) 64 | const fetchedCopy = fetched.then(resp => resp.clone()) 65 | 66 | // Call respondWith() with whatever we get first. 67 | // If the fetch fails (e.g disconnected), wait for the cache. 68 | // If there’s nothing in cache, wait for the fetch. 69 | // If neither yields a response, return offline pages. 70 | event.respondWith( 71 | Promise.race([fetched.catch(_ => cached), cached]) 72 | .then(resp => resp || fetched) 73 | .catch(_ => { /* eat any errors */ }) 74 | ) 75 | 76 | // Update the cache with the version we fetched (only for ok status) 77 | event.waitUntil( 78 | Promise.all([fetchedCopy, caches.open(RUNTIME)]) 79 | .then(([response, cache]) => response.ok && cache.put(event.request, response)) 80 | .catch(_ => { /* eat any errors */ }) 81 | ) 82 | } 83 | }) -------------------------------------------------------------------------------- /docs/zh-cn/API-configuration.md: -------------------------------------------------------------------------------- 1 | # 接口配置 2 | 3 | ## 为什么 4 | 5 | 在使用了`redux`或者`dva`项目中,我们经常会写类似下面的`service`层的函数,使代码结构更清晰,但是很容易看出,我们会写很多相似的代码,在`antd-admin@5.0`中,使用了更加简洁的配置方式实现了相同的功能。 6 | 7 | ```javascript 8 | export async function login(data) { 9 | return request({ 10 | url: '/api/v1/user/login', 11 | method: 'post', 12 | data, 13 | }) 14 | } 15 | ``` 16 | 17 | ## 配置和使用 18 | 19 | 在`src/services/api.js`文件中,你会看到如下配置对象,对象的键用于调用时的函数名称,对象的值为请求的`url`,默认请求方式为`GET`,如果是其他请求方式对象的值的格式则为`'method url'`。 20 | 21 | ```javascript 22 | export default { 23 | ... 24 | queryUser: '/user/:id', 25 | queryUserList: '/users', 26 | updateUser: 'Patch /user/:id', 27 | createUser: 'POST /user/:id', 28 | removeUser: 'DELETE /user/:id', 29 | removeUserList: 'POST /users/delete', 30 | ... 31 | } 32 | ``` 33 | 34 | 在其他文件中使用 35 | 36 | ```javascript 37 | import { queryUser } from 'api' 38 | 39 | // 一般文件中 40 | ... 41 | queryUser(option).then(data => console.log(data)) 42 | ... 43 | 44 | // model文件中 45 | ... 46 | yield call(queryUser, option) 47 | ... 48 | ``` 49 | 50 | ## 实现方式 51 | 52 | 参考`src/services/index.js`文件,对api配置进行遍历,每个属性都返回对应的封装后的request函数。 53 | 54 | ```javascript 55 | import request from 'utils/request' 56 | import { apiPrefix } from 'utils/config' 57 | 58 | import api from './api' 59 | 60 | const gen = params => { 61 | let url = apiPrefix + params 62 | let method = 'GET' 63 | 64 | const paramsArray = params.split(' ') 65 | if (paramsArray.length === 2) { 66 | method = paramsArray[0] 67 | url = apiPrefix + paramsArray[1] 68 | } 69 | 70 | return function(data) { 71 | return request({ 72 | url, 73 | data, 74 | method, 75 | }) 76 | } 77 | } 78 | 79 | const APIFunction = {} 80 | for (const key in api) { 81 | APIFunction[key] = gen(api[key]) 82 | } 83 | 84 | module.exports = APIFunction 85 | 86 | ``` -------------------------------------------------------------------------------- /docs/zh-cn/_sidebar.md: -------------------------------------------------------------------------------- 1 | - 入门 2 | - [快速上手](zh-cn/getting-started.md) 3 | - 定制化 4 | - [配置项](zh-cn/configuration.md) 5 | - [接口配置](zh-cn/API-configuration.md) 6 | - [国际化](zh-cn/i18n.md) 7 | - [布局](zh-cn/layout.md) 8 | - [http 请求](zh-cn/request.md) 9 | - 指南 10 | - [部署](zh-cn/deploy.md) 11 | - [更新日志](zh-cn/change-log.md) 12 | -------------------------------------------------------------------------------- /docs/zh-cn/change-log.md: -------------------------------------------------------------------------------- 1 | ## 5.0.0 2 | 3 | #### 优化 4 | 5 | - 尽量使用修饰器,简化代码编写,提高代码可读性。 6 | 7 | - API 配置化,简化获取数据方式。 8 | 9 | - `utils` 内文件拆分,各司其职。 10 | 11 | - 简化`utils/request`文件,不做特殊处理。 12 | 13 | #### 规范 14 | 15 | - 函数添加描述、参数、返回值等注释,含糊不清的代码增加注释,规范参考 [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html#appendices-jsdoc-tag-reference)。 16 | 17 | - 语义化版本号,规范参加 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/)。 18 | 19 | - 静态代码检查,统一代码风格,代码提交前将会使用 `prettier`、`stylelint`、`eslint` 规范代码。 20 | 21 | - Git 提交信息规范化,[git-commit-emoji-cn](https://github.com/liuchengxu/git-commit-emoji-cn)。 22 | 23 | - 基于 `Umi` 的约定式路由,无需再写路由配置文件。 24 | 25 | - 使用 `React 16` 新特性,如 `Fragment`、`Context`、 `PureComponent`等。 26 | 27 | #### 功能 28 | 29 | - 支持国际化,源码中抽离翻译字段,按需加载语言包,自动在线翻译。 30 | 31 | - 支持按需引入 `lodash` 函数。 32 | 33 | - 支持多布局,可根据规则规定哪些路由使用哪种布局。 34 | 35 | - 支持 Antd Admin 在 Travis 上自动编译和部署。 36 | 37 | - 使用 `Docsify` 生成文档网站。 38 | 39 | 40 | #### 样式 41 | 42 | - 新增 Antd Admin 独立 Logo。 43 | 44 | - 重写整体布局组件,优化菜单、面包屑导航自动高亮,菜单自动展开等逻辑。 45 | 46 | - 移动端菜单更改为抽屉式。 47 | 48 | #### 其他 49 | 50 | - 废弃 `IconFont`、 `Search`、`DataTable`等组件,因为在 `Antd` 中有很好的支持和可替代的。 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/zh-cn/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置项 2 | 3 | 你可以在 `/src/utils/config.js` 里做一些自定义配置: 4 | 5 | ## siteName 6 | 7 | - 类型: `String` 8 | 9 | 配置站点名称,应用到登录框,侧边栏顶部的标题文字显示。 10 | 11 | ## copyright 12 | 13 | - 类型: `String` 14 | 15 | 配置版权声明,应用到登录页、`Primay`布局底部。 16 | 17 | ## logoPath 18 | 19 | - 类型: `String` 20 | 21 | 配置站点 Logo,应用到登录框,侧边栏顶部的 Logo 显示。 22 | 23 | ## apiPrefix 24 | 25 | - 类型: `String` 26 | 27 | 配置项目中接口的前缀,接口相关文档可查看 [接口配置](API-configuration.md) 28 | 29 | ## fixedHeader 30 | 31 | - 类型: `String` 32 | 33 | 在`Primary`布局下,页面滚动时是否固定顶部。 34 | 35 | ## layouts 36 | 37 | - 类型: `Array` 38 | 39 | 配置哪些路由使用哪种布局,未指定路由使用默认布局 `Public`,项目中目前有 `Primary` 和 `Public` 两种布局, 40 | 默认配置如下: 41 | 42 | ```javascript 43 | layouts: [ 44 | { 45 | name: 'primary', 46 | include: [/.*/], 47 | exclude: [/(\/(en|zh))*\/login/], 48 | }, 49 | ], 50 | ``` 51 | 52 | 每种布局的对象属性如下: 53 | 54 | - `name` - 布局的名称; 55 | 56 | - `include` - 指定使用该布局的路由规则列表,规则可为正则表达式或者字符串; 57 | 58 | - `exclude` - 指定不使用该布局的路由规则列表,规则可为正则表达式或者字符串。 59 | 60 | > 注意:`exclude` 优先级高于 `include`,前面的布局优先级高于后面的布局。开发过程中可能需要结合`src/layouts`目录下的布局使用,具体方法可查看 [使用布局](./layout.md)。 61 | 62 | ## i18n 63 | 64 | - 类型: `Object` 65 | 66 | 配置国际化,默认配置如下: 67 | 68 | ```javascript 69 | i18n: { 70 | languages: [ 71 | { 72 | key: 'en', 73 | title: 'English', 74 | flag: '/america.svg', 75 | }, 76 | { 77 | key: 'zh', 78 | title: '中文', 79 | flag: '/china.svg', 80 | }, 81 | ], 82 | defaultLanguage: 'en', 83 | } 84 | ``` 85 | 86 | ### i18n.languages 87 | 88 | - 类型: `Array` 89 | 90 | 指定应用支持哪些语言,每种语言的对象属性如下: 91 | 92 | - `key` - 语言的`key`,应用到页面 url 上以区分语言,也对应 `src/locales` 目录下的语言包文件夹名; 93 | 94 | - `title` - 语言名称,在登录页底部、`Primay` 布局顶部语言切换显示; 95 | 96 | - `flag` - 语言的国旗图标的路径,在 `Primay` 布局顶部语言切换显示。 97 | 98 | ### i18n.defaultLanguage 99 | 100 | - 类型: `String` 101 | 102 | 配置默认语言。 103 | -------------------------------------------------------------------------------- /docs/zh-cn/deploy.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | 3 | 完成开发并且在开发环境验证之后,就需要部署给我们的用户了。 4 | 5 |  6 | 7 | ## 构建 8 | 9 | 先执行下面的命令, 10 | 11 | ```bash 12 | npm run build 13 | ``` 14 | 15 | 几秒后,输出应该如下: 16 | 17 | ```bash 18 | > antd-admin@5.0.0-beta build /Users/zuiidea/web/antd-admin 19 | > umi build 20 | 21 | [21:13:17] webpack compiled in 43s 868ms 22 | DONE Compiled successfully in 43877ms 21:13:17 23 | 24 | File sizes after gzip: 25 | 26 | 1.3 MB dist/vendors.async.js 27 | 308.21 KB dist/umi.js 28 | 45.49 KB dist/vendors.chunk.css 29 | 36.08 KB dist/p__chart__highCharts__index.async.js 30 | 33.53 KB dist/p__user__index.async.js 31 | 22.36 KB dist/p__chart__ECharts__index.async.js 32 | 4.21 KB dist/p__dashboard__index.async.js 33 | 4.06 KB dist/umi.css 34 | ... 35 | ``` 36 | 37 | `build` 命令会打包所有的资源,包含 JavaScript, CSS, web fonts, images, html 等。你可以在 `dist/` 目录下找到这些文件。 38 | 39 | > 如果有使用 HashHistory 、 部署 html 到非根目录、静态化等需求,请查看[Umi 部署](https://umijs.org/zh/guide/deploy.html)。 40 | 41 | ## 本地验证 42 | 43 | 44 | 发布之前,可以通过 `serve` 做本地验证, 45 | 46 | ``` 47 | $ yarn global add serve 48 | $ serve ./dist 49 | 50 | Serving! 51 | 52 | - Local: http://localhost:5000 53 | - On Your Network: http://{Your IP}:5000 54 | 55 | Copied local address to clipboard! 56 | 57 | ``` 58 | 59 | 访问 [http://localhost:5000](http://localhost:5000),正常情况下法应该是和 `npm start` 一致的(接口可能无法获取到正确数据)。 60 | 61 | 62 | ## 部署 63 | 64 | 接下来,我们可以把静态文件上传到服务器,如果使用 Nginx 作为 Web server,你可以在 `ngnix.conf` 中这样配置: 65 | 66 | ``` 67 | server 68 | { 69 | listen 80; 70 | # 指定可访问的域名 71 | server_name antd-admin.zuiidea.com; 72 | # 编译后的文件存放的目录 73 | root /home/www/antd-admin/dist; 74 | 75 | # 代理服务端接口,避免跨域 76 | location /api { 77 | proxy_pass http://localhost:7000/api; 78 | } 79 | 80 | # 因为前端使用了BrowserHistory,所以将路由 fallback 到 index.html 81 | location / { 82 | index index.html; 83 | try_files $uri $uri/ /index.html; 84 | } 85 | } 86 | ``` 87 | 88 | 重启 Web server,访问 [http://antd-admin.zuiidea.com](http://antd-admin.zuiidea.com) ,你将看到正确的页面。 89 | 90 | ```bash 91 | nginx -s reload 92 | ``` 93 | 94 | 类似的,如果你使用 Caddy 作为 Web server,你可以在 `Caddyfile` 中这样配置: 95 | 96 | ``` 97 | antd-admin.zuiidea.com { 98 | gzip 99 | root /home/www/antd-admin/dist 100 | proxy /api http://localhost:7000 101 | 102 | rewrite { 103 | if {path} not_match ^/api 104 | to {path} {path}/ / 105 | } 106 | } 107 | 108 | 109 | antd-admin.zuiidea.com/public { 110 | gzip 111 | root /home/www/antd-admin/dist/static/public 112 | } 113 | 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/zh-cn/faq.md: -------------------------------------------------------------------------------- 1 | # 问题集锦 2 | 3 | ## 新建页面 4 | 5 | 1. 直接从/src/pages复制一个page (会自动创建路由[umi](https://umijs.org/zh/guide/router.html#%E7%BA%A6%E5%AE%9A%E5%BC%8F%E8%B7%AF%E7%94%B1)) 6 | 2. 修改 namespace/pathToRegexp 在 model.js 7 | 3. 修改 mock中route.js增加一条route -------------------------------------------------------------------------------- /docs/zh-cn/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | > 在开始之前,推荐先学习 [React](http://facebook.github.io/react/) 、 [ES2015+](http://es6.ruanyifeng.com/) 、 [Antd Design](https://ant.design/docs/react/introduce-cn) , 了解 [UmiJS](https://umijs.org/) 、[Dva](http://github.com/dvajs/dva) ,并正确安装和配置了 [Node.js](https://nodejs.org/) v8 或以上 、[Git](https://git-scm.com/)。提前了解和学习这些知识会非常有帮助。 4 | 5 | ## 安装 6 | 7 | ```bash 8 | git clone https://github.com/zuiidea/antd-admin.git my-project 9 | cd my-project 10 | ``` 11 | 12 | ## 目录结构 13 | 14 | 应用的目录结构如下 15 | 16 | ```bash 17 | ├── dist/ # 默认build输出目录 18 | ├── mock/ # Mock文件目录 19 | ├── public/ # 静态资源文件目录 20 | ├── src/ # 源码目录 21 | │ ├── components/ # 组件目录 22 | │ ├── e2e/ # e2e目录 23 | │ ├── layouts/ # 布局目录 24 | │ ├── locales/ # 国际化文件目录 25 | │ ├── models/ # 数据模型目录 26 | │ ├── pages/ # 页面组件目录 27 | │ ├── services/ # 数据接口目录 28 | │ │ ├── api.js # 接口配置 29 | │ │ └── index.js # 接口输出 30 | │ ├── themes/ # 项目样式目录 31 | │ │ ├── default.less # 样式变量 32 | │ │ ├── index.less # 全局样式 33 | │ │ ├── mixin.less # 样式函数 34 | │ │ └── vars.less # 样式变量及函数 35 | │ ├── utils/ # 工具函数目录 36 | │ │ ├── config.js # 项目配置 37 | │ │ ├── constant.js # 静态常量 38 | │ │ ├── index.js # 工具函数 39 | │ │ ├── request.js # 异步请求函数(axios) 40 | │ │ └── theme.js # 项目需要在js中使用到样式变量 41 | ├── .editorconfig # 编辑器配置 42 | ├── .env # 环境变量 43 | ├── .eslintrc # ESlint配置 44 | ├── .gitignore # Git忽略文件配置 45 | ├── .prettierignore # Prettier忽略文件配置 46 | ├── .prettierrc # Prettier配置 47 | ├── .stylelintrc.json # Stylelint配置 48 | ├── .travis.yml # Travis配置 49 | └── .umirc.js # Umi配置 50 | └── package.json # 项目信息 51 | ``` 52 | 53 | ## 本地开发 54 | 55 | 1. 进入目录安装依赖,国内用户推荐使用 [cnpm](https://cnpmjs.org) 进行加速 56 | 57 | ```bash 58 | yarn install 59 | ``` 60 | 61 | 或者 62 | 63 | ```bash 64 | npm install 65 | ``` 66 | 67 | 2. 启动本地服务器 68 | 69 | ```bash 70 | npm run start 71 | ``` 72 | 73 | 3. 启动完成后打开浏览器访问 [http://localhost:7000](http://localhost:7000),如果需要更改启动端口,可在 `.env` 文件中配置。 74 | -------------------------------------------------------------------------------- /docs/zh-cn/i18n.md: -------------------------------------------------------------------------------- 1 | # 国际化 2 | 3 | ## 新增应用语言 4 | 5 | 以新增日语为例。 6 | 7 |  8 | 9 | 1. 添加语言包本地文件,`ja` 为日语的语言代码,支持翻译的语言列表参考 [有道智云](http://ai.youdao.com/docs/doc-trans-api.s#p05),运行下面命令后会生成 `src/locales/ja/messages.json` 文件。 10 | 11 | ```bash 12 | npm run add-locale ja 13 | ``` 14 | 15 | 2. 提取代码中需要翻译的字段,即 `<Trans>message</Trans>`、`` intl.formatMessage({ id: 'message `` 中 `message` 字段,运行下面命令后 `src/locales/ja/messages.json' }) 将会出现提取后的字段配置。 16 | 17 | ```bash 18 | npm run extract 19 | ``` 20 | 21 | 你将看到如下信息: 22 | 23 | ```bash 24 | Catalog statistics: 25 | ┌─────────────┬─────────────┬─────────┐ 26 | │ Language │ Total count │ Missing │ 27 | ├─────────────┼─────────────┼─────────┤ 28 | │ en (source) │ 52 │ - │ 29 | │ ja │ 52 │ 52 │ 30 | │ zh │ 52 │ 0 │ 31 | └─────────────┴─────────────┴─────────┘ 32 | ``` 33 | 34 | 3. 与此同时,我们在 `src/utils/config.js` 新增相关配置。 35 | 36 | ```javascript 37 | { 38 | ... 39 | i18n: { 40 | languages: [ 41 | ... 42 | { 43 | key:'ja', 44 | title: '日本語', 45 | flag: '/japanese.svg', 46 | }, 47 | ], 48 | }, 49 | } 50 | ``` 51 | 52 | > 路由相关效果,配置后 `npm run start` 重启后生效。 53 | 54 | 4. 使用内置的命令进行自动翻译,在 `src/locales/ja/messages.json` 中将会看到翻译后的配置。 55 | 56 | ```bash 57 | npm run trans:only 58 | ``` 59 | 60 | 你将看到如下信息: 61 | 62 | ```bash 63 | start: en -> ja 64 | ... 65 | youdao: en -> ja: Unpublished -> 未発表 66 | youdao: en -> ja: Update -> 更新 67 | youdao: en -> ja: Update User -> ユーザーの更新 68 | youdao: en -> ja: Username -> 名 69 | ... 70 | All translations have been completed. 71 | ``` 72 | 73 | > `npm run trans` 将会依次执行 `npm run extract` 和 `npm run trans:only` 74 | 75 | 5. 最后,可以在 `src/locales/ja/messages.json` 中对翻译不准确的的字段进行调整。启动开发模式 `npm run start`,打开 [http://localhost:7000/ja/login](http://localhost:7000/ja/login),你将看到日语版本的应用。 76 | -------------------------------------------------------------------------------- /docs/zh-cn/layout.md: -------------------------------------------------------------------------------- 1 | # 布局 2 | 3 | ## 新增布局 4 | 5 | 以新增名为 `secondary` 的布局为例,使以 `secondary` 开头的路由都使用该布局。 6 | 7 | 1. 在 `src/utils/config.js` 新增相关配置,参数详细请查看 [layouts](/zh-cn/configuration?id=layouts)。 8 | 9 | ```javascript 10 | layouts: [ 11 | { 12 | name: 'primary', 13 | include: [/.*/], 14 | exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/], 15 | }, 16 | { 17 | name: 'secondary', 18 | include: [/(\/(en|zh))*\/secondary\/(.*)/], 19 | }, 20 | ], 21 | ``` 22 | 23 | 2. 在`src/layouts/BaseLayout.js` 文件中新增 `secondary` 布局组件。 24 | 25 | ```javascript 26 | import SecondaryLayout from './SecondaryLayout' 27 | 28 | const LayoutMap = { 29 | primary: PrimaryLayout, 30 | public: PublicLayout, 31 | secondary: SecondaryLayout, 32 | } 33 | ``` 34 | 35 | 3. 在`src/layouts/` 目录中新增 `SecondaryLayout.js` 文件。 36 | 37 | ```javascript 38 | import React from 'react' 39 | 40 | export default ({ children }) => { 41 | return ( 42 | <div> 43 | <h1>Secondary</h1> 44 | {children} 45 | </div> 46 | ) 47 | } 48 | ``` 49 | 50 | 4. 在`src/pages/` 目录中新增 `secondary/index.js` 文件。 51 | 52 | ```javascript 53 | import React from 'react' 54 | 55 | export default ({ children }) => { 56 | return <div>Secondary page Content</div> 57 | } 58 | ``` 59 | 60 | 5. 最后,启动开发模式 `npm run start`,打开 [http://localhost:7000/secondary/](http://localhost:7000/secondary/),你将看到 `secondary` 布局的页面。 61 | -------------------------------------------------------------------------------- /docs/zh-cn/request.md: -------------------------------------------------------------------------------- 1 | # HTTP请求 2 | 3 | 本项目使用了axios提供http请求服务,文件在src/utils/request.js 4 | 5 | ## 自定义Header 6 | 7 | 为了提供鉴权、修改cookie等服务,可以手动修改Header 8 | 9 | ``` 10 | axios.defaults.headers.common['Authorization'] = 'token' 11 | ``` 12 | 13 | 或者 14 | 15 | ``` 16 | // 添加请求拦截器 17 | axios.interceptors.request.use(function (config) { 18 | // 在发送请求之前做些什么 19 | config.headers.token = window.localStorage.getItem('token'); 20 | return config; 21 | }, function (error) { 22 | return Promise.reject(error); 23 | }); 24 | ``` -------------------------------------------------------------------------------- /docs/zh-cn/router.md: -------------------------------------------------------------------------------- 1 | # 路由 2 | 3 | 本项目中采用约定式路由 4 | 5 | 参考[umi 路由](https://umijs.org/zh/guide/router.html) 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | } 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Antd-Admin", 3 | "start_url": ".", 4 | "display": "standalone", 5 | "background_color": "#fff", 6 | "description": "A front-end solution for enterprise applications built upon Ant Design and UmiJS", 7 | "icons": [{ 8 | "src": "logo/logo@96.png", 9 | "sizes": "72x72" 10 | }, 11 | { 12 | "src": "logo/logo@128.png", 13 | "sizes": "128x128" 14 | }, 15 | { 16 | "src": "logo/logo@144.png", 17 | "sizes": "144x144" 18 | }, 19 | { 20 | "src": "logo/logo@152.png", 21 | "sizes": "152x152" 22 | }, 23 | { 24 | "src": "logo/logo@192.png", 25 | "sizes": "192x192" 26 | }, 27 | { 28 | "src": "logo/logo@384.png", 29 | "sizes": "384x384" 30 | }, 31 | { 32 | "src": "logo/logo@512.png", 33 | "sizes": "512x512" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /mock/_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Query objects that specify keys and values in an array where all values are objects. 3 | * @param {array} array An array where all values are objects, like [{key:1},{key:2}]. 4 | * @param {string} key The key of the object that needs to be queried. 5 | * @param {string} value The value of the object that needs to be queried. 6 | * @return {object|undefined} Return frist object when query success. 7 | */ 8 | export function queryArray(array, key, value) { 9 | if (!Array.isArray(array)) { 10 | return 11 | } 12 | return array.filter(_ => _[key] === value) 13 | } 14 | 15 | export function randomNumber(min, max) { 16 | return Math.floor(Math.random() * (max - min) + min) 17 | } 18 | 19 | export function randomAvatar() { 20 | const avatarList = [ 21 | 'photo-1549492864-2ec7d66ffb04.jpeg', 22 | 'photo-1480535339474-e083439a320d.jpeg', 23 | 'photo-1523419409543-a5e549c1faa8.jpeg', 24 | 'photo-1519648023493-d82b5f8d7b8a.jpeg', 25 | 'photo-1523307730650-594bc63f9d67.jpeg', 26 | 'photo-1522962506050-a2f0267e4895.jpeg', 27 | 'photo-1489779162738-f81aed9b0a25.jpeg', 28 | 'photo-1534308143481-c55f00be8bd7.jpeg', 29 | 'photo-1519336555923-59661f41bb45.jpeg', 30 | 'photo-1551438632-e8c7d9a5d1b7.jpeg', 31 | 'photo-1525879000488-bff3b1c387cf.jpeg', 32 | 'photo-1487412720507-e7ab37603c6f.jpeg', 33 | 'photo-1510227272981-87123e259b17.jpeg' 34 | ] 35 | return `//image.zuiidea.com/${avatarList[randomNumber(0, avatarList.length - 1)]}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim` 36 | } 37 | 38 | export const Constant = { 39 | ApiPrefix: '/api/v1', 40 | NotFound: { 41 | message: 'Not Found', 42 | documentation_url: '', 43 | }, 44 | Color: { 45 | green: '#64ea91', 46 | blue: '#8fc9fb', 47 | purple: '#d897eb', 48 | red: '#f69899', 49 | yellow: '#f8c82e', 50 | peach: '#f797d6', 51 | borderBase: '#e5e5e5', 52 | borderSplit: '#f4f4f4', 53 | grass: '#d6fbb5', 54 | sky: '#c1e0fc', 55 | }, 56 | } 57 | 58 | export Mock from 'mockjs' 59 | export qs from 'qs' 60 | -------------------------------------------------------------------------------- /mock/dashboard.js: -------------------------------------------------------------------------------- 1 | import { Mock, Constant } from './_utils' 2 | 3 | const { ApiPrefix, Color } = Constant 4 | 5 | const Dashboard = Mock.mock({ 6 | 'sales|8': [ 7 | { 8 | 'name|+1': 2008, 9 | 'Clothes|200-500': 1, 10 | 'Food|180-400': 1, 11 | 'Electronics|300-550': 1, 12 | }, 13 | ], 14 | cpu: { 15 | 'usage|50-600': 1, 16 | space: 825, 17 | 'cpu|40-90': 1, 18 | 'data|20': [ 19 | { 20 | 'cpu|20-80': 1, 21 | }, 22 | ], 23 | }, 24 | browser: [ 25 | { 26 | name: 'Google Chrome', 27 | percent: 43.3, 28 | status: 1, 29 | }, 30 | { 31 | name: 'Mozilla Firefox', 32 | percent: 33.4, 33 | status: 2, 34 | }, 35 | { 36 | name: 'Apple Safari', 37 | percent: 34.6, 38 | status: 3, 39 | }, 40 | { 41 | name: 'Internet Explorer', 42 | percent: 12.3, 43 | status: 4, 44 | }, 45 | { 46 | name: 'Opera Mini', 47 | percent: 3.3, 48 | status: 1, 49 | }, 50 | { 51 | name: 'Chromium', 52 | percent: 2.53, 53 | status: 1, 54 | }, 55 | ], 56 | user: { 57 | name: 'github', 58 | sales: 3241, 59 | sold: 3556, 60 | }, 61 | 'completed|12': [ 62 | { 63 | 'name|+1': 2008, 64 | 'Task complete|200-1000': 1, 65 | 'Cards Complete|200-1000': 1, 66 | }, 67 | ], 68 | 'comments|5': [ 69 | { 70 | name: '@last', 71 | 'status|1-3': 1, 72 | content: '@sentence', 73 | avatar() { 74 | return Mock.Random.image( 75 | '48x48', 76 | Mock.Random.color(), 77 | '#757575', 78 | 'png', 79 | this.name.substr(0, 1) 80 | ) 81 | }, 82 | date() { 83 | return `2016-${Mock.Random.date('MM-dd')} ${Mock.Random.time( 84 | 'HH:mm:ss' 85 | )}` 86 | }, 87 | }, 88 | ], 89 | 'recentSales|36': [ 90 | { 91 | 'id|+1': 1, 92 | name: '@last', 93 | 'status|1-4': 1, 94 | date() { 95 | return `${Mock.Random.integer(2015, 2016)}-${Mock.Random.date( 96 | 'MM-dd' 97 | )} ${Mock.Random.time('HH:mm:ss')}` 98 | }, 99 | 'price|10-200.1-2': 1, 100 | }, 101 | ], 102 | quote: { 103 | name: 'Joho Doe', 104 | title: 'Graphic Designer', 105 | content: 106 | "I'm selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can't handle me at my worst, then you sure as hell don't deserve me at my best.", 107 | avatar: 108 | '//cdn.antd-admin.zuiidea.com/bc442cf0cc6f7940dcc567e465048d1a8d634493198c4-sPx5BR_fw236', 109 | }, 110 | numbers: [ 111 | { 112 | icon: 'pay-circle-o', 113 | color: Color.green, 114 | title: 'Online Review', 115 | number: 2781, 116 | }, 117 | { 118 | icon: 'team', 119 | color: Color.blue, 120 | title: 'New Customers', 121 | number: 3241, 122 | }, 123 | { 124 | icon: 'message', 125 | color: Color.purple, 126 | title: 'Active Projects', 127 | number: 253, 128 | }, 129 | { 130 | icon: 'shopping-cart', 131 | color: Color.red, 132 | title: 'Referrals', 133 | number: 4324, 134 | }, 135 | ], 136 | }) 137 | 138 | module.exports = { 139 | [`GET ${ApiPrefix}/dashboard`](req, res) { 140 | res.json(Dashboard) 141 | }, 142 | } 143 | -------------------------------------------------------------------------------- /mock/post.js: -------------------------------------------------------------------------------- 1 | import { Mock, Constant } from './_utils' 2 | 3 | const { ApiPrefix } = Constant 4 | 5 | let postId = 0 6 | const database = Mock.mock({ 7 | 'data|100': [ 8 | { 9 | id() { 10 | postId += 1 11 | return postId + 10000 12 | }, 13 | 'status|1-2': 1, 14 | title: '@title', 15 | author: '@last', 16 | categories: '@word', 17 | tags: '@word', 18 | 'views|10-200': 1, 19 | 'comments|10-200': 1, 20 | visibility: () => { 21 | return Mock.mock( 22 | '@pick(["Public",' + '"Password protected", ' + '"Private"])' 23 | ) 24 | }, 25 | date: '@dateTime', 26 | image() { 27 | return Mock.Random.image( 28 | '100x100', 29 | Mock.Random.color(), 30 | '#757575', 31 | 'png', 32 | this.author.substr(0, 1) 33 | ) 34 | }, 35 | }, 36 | ], 37 | }).data 38 | 39 | module.exports = { 40 | [`GET ${ApiPrefix}/posts`](req, res) { 41 | const { query } = req 42 | let { pageSize, page, ...other } = query 43 | pageSize = pageSize || 10 44 | page = page || 1 45 | 46 | let newData = database 47 | for (let key in other) { 48 | if ({}.hasOwnProperty.call(other, key)) { 49 | newData = newData.filter(item => { 50 | if ({}.hasOwnProperty.call(item, key)) { 51 | return ( 52 | String(item[key]) 53 | .trim() 54 | .indexOf(decodeURI(other[key]).trim()) > -1 55 | ) 56 | } 57 | return true 58 | }) 59 | } 60 | } 61 | 62 | res.status(200).json({ 63 | data: newData.slice((page - 1) * pageSize, page * pageSize), 64 | total: newData.length, 65 | }) 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /mock/route.js: -------------------------------------------------------------------------------- 1 | import { Constant } from './_utils' 2 | const { ApiPrefix } = Constant 3 | 4 | const database = [ 5 | { 6 | id: '1', 7 | icon: 'dashboard', 8 | name: 'Dashboard', 9 | zh: { 10 | name: '仪表盘' 11 | }, 12 | 'pt-br': { 13 | name: 'Dashboard' 14 | }, 15 | route: '/dashboard', 16 | }, 17 | { 18 | id: '2', 19 | breadcrumbParentId: '1', 20 | name: 'Users', 21 | zh: { 22 | name: '用户管理' 23 | }, 24 | 'pt-br': { 25 | name: 'Usuário' 26 | }, 27 | icon: 'user', 28 | route: '/user', 29 | }, 30 | { 31 | id: '7', 32 | breadcrumbParentId: '1', 33 | name: 'Posts', 34 | zh: { 35 | name: '用户管理' 36 | }, 37 | 'pt-br': { 38 | name: 'Posts' 39 | }, 40 | icon: 'shopping-cart', 41 | route: '/post', 42 | }, 43 | { 44 | id: '21', 45 | menuParentId: '-1', 46 | breadcrumbParentId: '2', 47 | name: 'User Detail', 48 | zh: { 49 | name: '用户详情' 50 | }, 51 | 'pt-br': { 52 | name: 'Detalhes do usuário' 53 | }, 54 | route: '/user/:id', 55 | }, 56 | { 57 | id: '3', 58 | breadcrumbParentId: '1', 59 | name: 'Request', 60 | zh: { 61 | name: 'Request' 62 | }, 63 | 'pt-br': { 64 | name: 'Requisição' 65 | }, 66 | icon: 'api', 67 | route: '/request', 68 | }, 69 | { 70 | id: '4', 71 | breadcrumbParentId: '1', 72 | name: 'UI Element', 73 | zh: { 74 | name: 'UI组件' 75 | }, 76 | 'pt-br': { 77 | name: 'Elementos UI' 78 | }, 79 | icon: 'camera-o', 80 | }, 81 | { 82 | id: '45', 83 | breadcrumbParentId: '4', 84 | menuParentId: '4', 85 | name: 'Editor', 86 | zh: { 87 | name: 'Editor' 88 | }, 89 | 'pt-br': { 90 | name: 'Editor' 91 | }, 92 | icon: 'edit', 93 | route: '/editor', 94 | }, 95 | { 96 | id: '5', 97 | breadcrumbParentId: '1', 98 | name: 'Charts', 99 | zh: { 100 | name: 'Charts' 101 | }, 102 | 'pt-br': { 103 | name: 'Graficos' 104 | }, 105 | icon: 'code-o', 106 | }, 107 | { 108 | id: '51', 109 | breadcrumbParentId: '5', 110 | menuParentId: '5', 111 | name: 'ECharts', 112 | zh: { 113 | name: 'ECharts' 114 | }, 115 | 'pt-br': { 116 | name: 'ECharts' 117 | }, 118 | icon: 'line-chart', 119 | route: '/chart/ECharts', 120 | }, 121 | { 122 | id: '52', 123 | breadcrumbParentId: '5', 124 | menuParentId: '5', 125 | name: 'HighCharts', 126 | zh: { 127 | name: 'HighCharts' 128 | }, 129 | 'pt-br': { 130 | name: 'HighCharts' 131 | }, 132 | icon: 'bar-chart', 133 | route: '/chart/highCharts', 134 | }, 135 | { 136 | id: '53', 137 | breadcrumbParentId: '5', 138 | menuParentId: '5', 139 | name: 'Rechartst', 140 | zh: { 141 | name: 'Rechartst' 142 | }, 143 | 'pt-br': { 144 | name: 'Rechartst' 145 | }, 146 | icon: 'area-chart', 147 | route: '/chart/Recharts', 148 | }, 149 | ] 150 | 151 | module.exports = { 152 | [`GET ${ApiPrefix}/routes`](req, res) { 153 | res.status(200).json(database) 154 | }, 155 | } 156 | -------------------------------------------------------------------------------- /public/america.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="iso-8859-1"?> 2 | <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 4 | viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> 5 | <rect style="fill:#F0F0F0;" width="512" height="512"/> 6 | <g> 7 | <rect y="64" style="fill:#D80027;" width="512" height="64"/> 8 | <rect y="192" style="fill:#D80027;" width="512" height="64"/> 9 | <rect y="320" style="fill:#D80027;" width="512" height="64"/> 10 | <rect y="448" style="fill:#D80027;" width="512" height="64"/> 11 | </g> 12 | <rect style="fill:#2E52B2;" width="256" height="275.69"/> 13 | <g> 14 | <polygon style="fill:#F0F0F0;" points="51.518,115.318 45.924,132.529 27.826,132.529 42.469,143.163 36.875,160.375 15 | 51.518,149.741 66.155,160.375 60.56,143.163 75.203,132.529 57.106,132.529 "/> 16 | <polygon style="fill:#F0F0F0;" points="57.106,194.645 51.518,177.434 45.924,194.645 27.826,194.645 42.469,205.279 17 | 36.875,222.49 51.518,211.857 66.155,222.49 60.56,205.279 75.203,194.645 "/> 18 | <polygon style="fill:#F0F0F0;" points="51.518,53.202 45.924,70.414 27.826,70.414 42.469,81.047 36.875,98.259 51.518,87.625 19 | 66.155,98.259 60.56,81.047 75.203,70.414 57.106,70.414 "/> 20 | <polygon style="fill:#F0F0F0;" points="128.003,115.318 122.409,132.529 104.311,132.529 118.954,143.163 113.36,160.375 21 | 128.003,149.741 142.64,160.375 137.045,143.163 151.689,132.529 133.591,132.529 "/> 22 | <polygon style="fill:#F0F0F0;" points="133.591,194.645 128.003,177.434 122.409,194.645 104.311,194.645 118.954,205.279 23 | 113.36,222.49 128.003,211.857 142.64,222.49 137.045,205.279 151.689,194.645 "/> 24 | <polygon style="fill:#F0F0F0;" points="210.076,194.645 204.489,177.434 198.894,194.645 180.797,194.645 195.44,205.279 25 | 189.845,222.49 204.489,211.857 219.125,222.49 213.531,205.279 228.174,194.645 "/> 26 | <polygon style="fill:#F0F0F0;" points="204.489,115.318 198.894,132.529 180.797,132.529 195.44,143.163 189.845,160.375 27 | 204.489,149.741 219.125,160.375 213.531,143.163 228.174,132.529 210.076,132.529 "/> 28 | <polygon style="fill:#F0F0F0;" points="128.003,53.202 122.409,70.414 104.311,70.414 118.954,81.047 113.36,98.259 29 | 128.003,87.625 142.64,98.259 137.045,81.047 151.689,70.414 133.591,70.414 "/> 30 | <polygon style="fill:#F0F0F0;" points="204.489,53.202 198.894,70.414 180.797,70.414 195.44,81.047 189.845,98.259 31 | 204.489,87.625 219.125,98.259 213.531,81.047 228.174,70.414 210.076,70.414 "/> 32 | </g> 33 | <g> 34 | </g> 35 | <g> 36 | </g> 37 | <g> 38 | </g> 39 | <g> 40 | </g> 41 | <g> 42 | </g> 43 | <g> 44 | </g> 45 | <g> 46 | </g> 47 | <g> 48 | </g> 49 | <g> 50 | </g> 51 | <g> 52 | </g> 53 | <g> 54 | </g> 55 | <g> 56 | </g> 57 | <g> 58 | </g> 59 | <g> 60 | </g> 61 | <g> 62 | </g> 63 | </svg> 64 | -------------------------------------------------------------------------------- /public/china.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 4 | viewBox="-49 141 512 512" style="enable-background:new -49 141 512 512;" xml:space="preserve"> 5 | <style type="text/css"> 6 | .st0{fill:#D80027;} 7 | .st1{fill:#FFDA44;} 8 | </style> 9 | <rect x="-49" y="141" class="st0" width="512" height="512"/> 10 | 11 | <g> 12 | <polygon class="st1" points="91.1,296.8 113.2,364.8 184.7,364.8 126.9,406.9 149,474.9 91.1,432.9 33.2,474.9 55.4,406.9 13 | -2.5,364.8 69,364.8 "/> 14 | <polygon class="st1" points="254.5,537.5 237.6,516.7 212.6,526.4 227.1,503.9 210.2,483 236.1,489.9 250.7,467.4 252.1,494.2 15 | 278.1,501.1 253,510.7 "/> 16 | <polygon class="st1" points="288.1,476.5 296.1,450.9 274.2,435.4 301,435 308.9,409.4 317.6,434.8 344.4,434.5 322.9,450.5 17 | 331.5,475.9 309.6,460.4 "/> 18 | <polygon class="st1" points="333.4,328.9 321.6,353 340.8,371.7 314.3,367.9 302.5,391.9 297.9,365.5 271.3,361.7 295.1,349.2 19 | 290.5,322.7 309.7,341.4 "/> 20 | <polygon class="st1" points="255.2,255.9 253.2,282.6 278.1,292.7 252,299.1 250.1,325.9 236,303.1 209.9,309.5 227.2,289 21 | 213,266.3 237.9,276.4 "/> 22 | </g> 23 | </svg> 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | <svg width="169px" height="141px" viewBox="0 0 169 141" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch --> 4 | <desc>Created with Sketch.</desc> 5 | <defs> 6 | <linearGradient x1="54.0428975%" y1="4.39752391%" x2="54.0428975%" y2="108.456714%" id="linearGradient-1"> 7 | <stop stop-color="#29CDFF" offset="0%"></stop> 8 | <stop stop-color="#148EFF" offset="62.3089445%"></stop> 9 | <stop stop-color="#0A60FF" offset="100%"></stop> 10 | </linearGradient> 11 | <linearGradient x1="50%" y1="14.2201464%" x2="50%" y2="113.263844%" id="linearGradient-2"> 12 | <stop stop-color="#FA816E" offset="0%"></stop> 13 | <stop stop-color="#F74A5C" offset="65.9092442%"></stop> 14 | <stop stop-color="#F51D2C" offset="100%"></stop> 15 | </linearGradient> 16 | </defs> 17 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 18 | <g id="Group" transform="translate(0.000000, -5.000000)"> 19 | <rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(83.718923, 75.312358) rotate(-24.000000) translate(-83.718923, -75.312358) " x="68.7189234" y="0.312357954" width="30" height="150" rx="15"></rect> 20 | <rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(129.009910, 75.580213) rotate(-24.000000) translate(-129.009910, -75.580213) " x="114.00991" y="0.580212739" width="30" height="150" rx="15"></rect> 21 | <circle id="Oval" fill="url(#linearGradient-2)" cx="25" cy="120" r="25"></circle> 22 | </g> 23 | </g> 24 | </svg> -------------------------------------------------------------------------------- /public/logo/logo@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@128.png -------------------------------------------------------------------------------- /public/logo/logo@144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@144.png -------------------------------------------------------------------------------- /public/logo/logo@152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@152.png -------------------------------------------------------------------------------- /public/logo/logo@192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@192.png -------------------------------------------------------------------------------- /public/logo/logo@384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@384.png -------------------------------------------------------------------------------- /public/logo/logo@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@512.png -------------------------------------------------------------------------------- /public/logo/logo@72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@72.png -------------------------------------------------------------------------------- /public/logo/logo@96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/logo/logo@96.png -------------------------------------------------------------------------------- /public/portugal.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="iso-8859-1"?> 2 | <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 4 | viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> 5 | <rect style="fill:#D80027;" width="512" height="512"/> 6 | <polygon style="fill:#6DA544;" points="196.641,0 196.641,264.348 196.641,512 0,512 0,0 "/> 7 | <circle style="fill:#FFDA44;" cx="196.641" cy="256" r="96"/> 8 | <path style="fill:#D80027;" d="M142.638,208v60c0,29.823,24.178,54,54,54s54-24.178,54-54v-60H142.638z"/> 9 | <path style="fill:#F0F0F0;" d="M196.638,286c-9.925,0-18-8.075-18-18v-24.001h36V268C214.638,277.925,206.563,286,196.638,286z"/> 10 | <g> 11 | </g> 12 | <g> 13 | </g> 14 | <g> 15 | </g> 16 | <g> 17 | </g> 18 | <g> 19 | </g> 20 | <g> 21 | </g> 22 | <g> 23 | </g> 24 | <g> 25 | </g> 26 | <g> 27 | </g> 28 | <g> 29 | </g> 30 | <g> 31 | </g> 32 | <g> 33 | </g> 34 | <g> 35 | </g> 36 | <g> 37 | </g> 38 | <g> 39 | </g> 40 | </svg> 41 | -------------------------------------------------------------------------------- /scripts/translate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Youdao Translate, My private account is for testing purposes only. 3 | * Please go to the official account to apply for an account. Thank you for your cooperation. 4 | * APP ID:055c2d71f9a05214 5 | * Secret key:ZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN 6 | */ 7 | 8 | /* eslint-disable */ 9 | const md5 = require('md5') 10 | const qs = require('qs') 11 | const fs = require('fs') 12 | const path = require('path') 13 | const axios = require('axios') 14 | const jsonFormat = require('json-format') 15 | const { i18n } = require('../src/utils/config') 16 | const { languages, defaultLanguage } = i18n 17 | 18 | const locales = {} 19 | 20 | languages.forEach(item => { 21 | locales[item.key] = require(`../src/locales/${item.key}/messages.json`) 22 | }) 23 | 24 | const youdao = ({ q, from, to }) => 25 | new Promise((resolve, reject) => { 26 | { 27 | const appid = '055c2d71f9a05214' 28 | const appse = 'ZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN' 29 | const salt = Date.now() 30 | 31 | const sign = md5(appid + q + salt + appse) 32 | const query = qs.stringify({ 33 | q, 34 | from, 35 | to, 36 | appKey: appid, 37 | salt, 38 | sign, 39 | }) 40 | 41 | axios.get(`http://openapi.youdao.com/api?${query}`).then(({ data }) => { 42 | if (data.query && data.translation[0]) { 43 | resolve(data.translation[0]) 44 | } else { 45 | resolve(q) 46 | } 47 | }) 48 | } 49 | }) 50 | 51 | const transform = async ({ from, to, locales, outputPath }) => { 52 | for (const key in locales[from]) { 53 | if (locales[to][key]) { 54 | console.log(`add to skip: ${key}`) 55 | } else { 56 | let res = key 57 | let way = 'youdao' 58 | if (key.indexOf('/') !== 0) { 59 | const reg = '{([^{}]*)}' 60 | const tasks = key 61 | .match(new RegExp(`${reg}|((?<=(${reg}|^)).*?(?=(${reg}|$)))`, 'g')) 62 | .map(item => { 63 | if (new RegExp(reg).test(item)) { 64 | return Promise.resolve(item) 65 | } 66 | return youdao({ 67 | q: item, 68 | from, 69 | to, 70 | }) 71 | }) 72 | 73 | res = (await Promise.all(tasks)).join('') 74 | } else { 75 | res = `/${to + key}` 76 | way = 'link' 77 | } 78 | if (res !== key) { 79 | locales[to][key] = res 80 | console.log(`${way}: ${from} -> ${to}: ${key} -> ${res}`) 81 | } else { 82 | console.log(`same: ${from} -> ${to}: ${key}`) 83 | } 84 | } 85 | } 86 | await fs.writeFileSync( 87 | path.resolve(__dirname, outputPath), 88 | jsonFormat(locales[to], { 89 | type: 'space', 90 | size: 2, 91 | }) 92 | ) 93 | } 94 | ;(async () => { 95 | const tasks = languages 96 | .map(item => ({ 97 | from: defaultLanguage, 98 | to: item.key, 99 | })) 100 | .filter(item => item.from !== item.to) 101 | 102 | for (const item of tasks) { 103 | console.log(`start: ${item.from} -> ${item.to}`) 104 | await transform({ 105 | from: item.from, 106 | to: item.to, 107 | locales, 108 | outputPath: `../src/locales/${item.to}/messages.json`, 109 | }) 110 | console.log(`completed: ${item.from} -> ${item.to}`) 111 | } 112 | 113 | console.log('All translations have been completed.') 114 | })() 115 | -------------------------------------------------------------------------------- /src/components/DropOption/DropOption.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { BarsOutlined, DownOutlined } from '@ant-design/icons' 4 | import { Dropdown, Button, Menu } from 'antd' 5 | 6 | const DropOption = ({ 7 | onMenuClick, 8 | menuOptions = [], 9 | buttonStyle, 10 | dropdownProps, 11 | }) => { 12 | const menu = menuOptions.map(item => ( 13 | <Menu.Item key={item.key}>{item.name}</Menu.Item> 14 | )) 15 | return ( 16 | <Dropdown 17 | overlay={<Menu onClick={onMenuClick}>{menu}</Menu>} 18 | {...dropdownProps} 19 | > 20 | <Button style={{ border: 'none', ...buttonStyle }}> 21 | <BarsOutlined style={{ marginRight: 2 }} /> 22 | <DownOutlined /> 23 | </Button> 24 | </Dropdown> 25 | ) 26 | } 27 | 28 | DropOption.propTypes = { 29 | onMenuClick: PropTypes.func, 30 | menuOptions: PropTypes.array.isRequired, 31 | buttonStyle: PropTypes.object, 32 | dropdownProps: PropTypes.object, 33 | } 34 | 35 | export default DropOption 36 | -------------------------------------------------------------------------------- /src/components/DropOption/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DropOption", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./DropOption.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Editor } from 'react-draft-wysiwyg' 3 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' 4 | import styles from './Editor.less' 5 | 6 | const DraftEditor = props => { 7 | return ( 8 | <Editor 9 | toolbarClassName={styles.toolbar} 10 | wrapperClassName={styles.wrapper} 11 | editorClassName={styles.editor} 12 | {...props} 13 | /> 14 | ) 15 | } 16 | 17 | export default DraftEditor 18 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | height: 500px; 3 | 4 | :global { 5 | .rdw-dropdownoption-default { 6 | padding: 6px; 7 | } 8 | 9 | .rdw-dropdown-optionwrapper { 10 | box-sizing: content-box; 11 | width: 100%; 12 | border-radius: 0 0 2px 2px; 13 | &:hover { 14 | box-shadow: none; 15 | } 16 | } 17 | 18 | .rdw-inline-wrapper { 19 | flex-wrap: wrap; 20 | margin-bottom: 0; 21 | 22 | .rdw-option-wrapper { 23 | margin-bottom: 6px; 24 | } 25 | } 26 | 27 | .rdw-option-active { 28 | box-shadow: 1px 1px 0 #e8e8e8 inset; 29 | } 30 | 31 | .rdw-colorpicker-option { 32 | box-shadow: none; 33 | } 34 | 35 | .rdw-colorpicker-modal, 36 | .rdw-embedded-modal, 37 | .rdw-emoji-modal, 38 | .rdw-image-modal, 39 | .rdw-link-modal { 40 | box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05); 41 | } 42 | 43 | .rdw-colorpicker-modal, 44 | .rdw-embedded-modal, 45 | .rdw-link-modal { 46 | height: auto; 47 | } 48 | 49 | .rdw-emoji-modal { 50 | width: 214px; 51 | } 52 | 53 | .rdw-colorpicker-modal { 54 | width: auto; 55 | } 56 | 57 | .rdw-embedded-modal-btn, 58 | .rdw-image-modal-btn, 59 | .rdw-link-modal-btn { 60 | height: 32px; 61 | margin-top: 12px; 62 | } 63 | 64 | .rdw-embedded-modal-input, 65 | .rdw-embedded-modal-size-input, 66 | .rdw-link-modal-input { 67 | padding: 2px 6px; 68 | height: 32px; 69 | } 70 | 71 | .rdw-dropdown-selectedtext { 72 | color: #000; 73 | } 74 | 75 | .rdw-dropdown-wrapper, 76 | .rdw-option-wrapper { 77 | min-width: 36px; 78 | transition: all 0.2s ease; 79 | height: 30px; 80 | 81 | &:active { 82 | box-shadow: 1px 1px 0 #e8e8e8 inset; 83 | } 84 | 85 | &:hover { 86 | box-shadow: 1px 1px 0 #e8e8e8; 87 | } 88 | } 89 | 90 | .rdw-dropdown-wrapper { 91 | min-width: 60px; 92 | } 93 | 94 | .rdw-editor-main { 95 | box-sizing: border-box; 96 | } 97 | } 98 | 99 | .editor { 100 | border: 1px solid #f1f1f1; 101 | padding: 5px; 102 | border-radius: 2px; 103 | height: auto; 104 | min-height: 200px; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/Editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Editor", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Editor.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TooltipProps } from 'antd/lib/tooltip'; 3 | 4 | export interface EllipsisTooltipProps extends TooltipProps { 5 | title?: undefined; 6 | overlayStyle?: undefined; 7 | } 8 | 9 | export interface EllipsisProps { 10 | tooltip?: boolean | EllipsisTooltipProps; 11 | length?: number; 12 | lines?: number; 13 | style?: React.CSSProperties; 14 | className?: string; 15 | fullWidthRecognition?: boolean; 16 | } 17 | 18 | export function getStrFullLength(str: string): number; 19 | export function cutStrByFullLength(str: string, maxLength: number): string; 20 | 21 | export default class Ellipsis extends React.Component<EllipsisProps, any> {} 22 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.less: -------------------------------------------------------------------------------- 1 | .ellipsis { 2 | display: inline-block; 3 | width: 100%; 4 | overflow: hidden; 5 | word-break: break-all; 6 | } 7 | 8 | .lines { 9 | position: relative; 10 | .shadow { 11 | position: absolute; 12 | z-index: -999; 13 | display: block; 14 | color: transparent; 15 | opacity: 0; 16 | } 17 | } 18 | 19 | .lineClamp { 20 | position: relative; 21 | display: -webkit-box; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ellipsis 3 | subtitle: 文本自动省略号 4 | cols: 1 5 | order: 10 6 | --- 7 | 8 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 9 | 10 | ## API 11 | 12 | | 参数 | 说明 | 类型 | 默认值 | 13 | | -------------------- | ------------------------------------------------ | ------- | ------ | 14 | | tooltip | 移动到文本展示完整内容的提示 | boolean | - | 15 | | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - | 16 | | lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` | 17 | | fullWidthRecognition | 是否将全角字符的长度视为 2 来计算字符串长度 | boolean | - | 18 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.test.js: -------------------------------------------------------------------------------- 1 | import { getStrFullLength, cutStrByFullLength } from './index'; 2 | 3 | describe('test calculateShowLength', () => { 4 | it('get full length', () => { 5 | expect(getStrFullLength('一二,a,')).toEqual(8); 6 | }); 7 | it('cut str by full length', () => { 8 | expect(cutStrByFullLength('一二,a,', 7)).toEqual('一二,a'); 9 | }); 10 | it('cut str when length small', () => { 11 | expect(cutStrByFullLength('一22三', 5)).toEqual('一22'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/FilterItem/FilterItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styles from './FilterItem.less' 4 | 5 | const FilterItem = ({ label = '', children }) => { 6 | const labelArray = label.split('') 7 | return ( 8 | <div className={styles.filterItem}> 9 | {labelArray.length > 0 && ( 10 | <div className={styles.labelWrap}> 11 | {labelArray.map((item, index) => ( 12 | <span className="labelText" key={index}> 13 | {item} 14 | </span> 15 | ))} 16 | </div> 17 | )} 18 | <div className={styles.item}>{children}</div> 19 | </div> 20 | ) 21 | } 22 | 23 | FilterItem.propTypes = { 24 | label: PropTypes.string, 25 | children: PropTypes.element.isRequired, 26 | } 27 | 28 | export default FilterItem 29 | -------------------------------------------------------------------------------- /src/components/FilterItem/FilterItem.less: -------------------------------------------------------------------------------- 1 | .filterItem { 2 | display: flex; 3 | justify-content: space-between; 4 | 5 | .labelWrap { 6 | width: 64px; 7 | line-height: 28px; 8 | margin-right: 12px; 9 | justify-content: space-between; 10 | display: flex; 11 | overflow: hidden; 12 | } 13 | 14 | .item { 15 | flex: 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/FilterItem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FilterItem", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./FilterItem.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/GlobalFooter/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export interface GlobalFooterProps { 3 | links?: Array<{ 4 | key?: string; 5 | title: React.ReactNode; 6 | href: string; 7 | blankTarget?: boolean; 8 | }>; 9 | copyright?: React.ReactNode; 10 | style?: React.CSSProperties; 11 | className?: string; 12 | } 13 | 14 | export default class GlobalFooter extends React.Component<GlobalFooterProps, any> {} 15 | -------------------------------------------------------------------------------- /src/components/GlobalFooter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.less'; 4 | 5 | const GlobalFooter = ({ className, links, copyright }) => { 6 | const clsString = classNames(styles.globalFooter, className); 7 | return ( 8 | <footer className={clsString}> 9 | {links && ( 10 | <div className={styles.links}> 11 | {links.map(link => ( 12 | <a 13 | key={link.key} 14 | title={link.key} 15 | target={link.blankTarget ? '_blank' : '_self'} 16 | href={link.href} 17 | > 18 | {link.title} 19 | </a> 20 | ))} 21 | </div> 22 | )} 23 | {copyright && <div className={styles.copyright}>{copyright}</div>} 24 | </footer> 25 | ); 26 | }; 27 | 28 | export default GlobalFooter; 29 | -------------------------------------------------------------------------------- /src/components/GlobalFooter/index.less: -------------------------------------------------------------------------------- 1 | /* @import '~antd/lib/style/themes/default.less'; */ 2 | 3 | .globalFooter { 4 | margin: 48px 0 24px 0; 5 | padding: 0 16px; 6 | text-align: center; 7 | 8 | .links { 9 | margin-bottom: 8px; 10 | 11 | a { 12 | color: @text-color-secondary; 13 | transition: all 0.3s; 14 | 15 | &:not(:last-child) { 16 | margin-right: 40px; 17 | } 18 | 19 | &:hover { 20 | color: @text-color; 21 | } 22 | } 23 | } 24 | 25 | .copyright { 26 | color: @text-color-secondary; 27 | font-size: @font-size-base; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/GlobalFooter/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GlobalFooter 3 | subtitle: 全局页脚 4 | cols: 1 5 | order: 7 6 | --- 7 | 8 | 页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。 9 | 10 | ## API 11 | 12 | | 参数 | 说明 | 类型 | 默认值 | 13 | | --------- | -------- | ---------------------------------------------------------------- | ------ | 14 | | links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - | 15 | | copyright | 版权信息 | ReactNode | - | 16 | -------------------------------------------------------------------------------- /src/components/Layout/Bread.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Breadcrumb } from 'antd' 4 | import { Link, withRouter } from 'umi' 5 | import { t } from "@lingui/macro" 6 | import iconMap from 'utils/iconMap' 7 | const { pathToRegexp } = require('path-to-regexp') 8 | import { queryAncestors } from 'utils' 9 | import styles from './Bread.less' 10 | 11 | @withRouter 12 | class Bread extends PureComponent { 13 | generateBreadcrumbs = (paths) => { 14 | return paths.map((item, key) => { 15 | const content = item && ( 16 | <Fragment> 17 | {item.icon && ( 18 | <span style={{ marginRight: 4 }}>{iconMap[item.icon]}</span> 19 | )} 20 | {item.name} 21 | </Fragment> 22 | ) 23 | 24 | return ( 25 | item && ( 26 | <Breadcrumb.Item key={key}> 27 | {paths.length - 1 !== key ? ( 28 | <Link to={item.route || '#'}>{content}</Link> 29 | ) : ( 30 | content 31 | )} 32 | </Breadcrumb.Item> 33 | ) 34 | ) 35 | }) 36 | } 37 | render() { 38 | const { routeList, location } = this.props 39 | 40 | // Find a route that matches the pathname. 41 | const currentRoute = routeList.find( 42 | (_) => _.route && pathToRegexp(_.route).exec(location.pathname) 43 | ) 44 | 45 | // Find the breadcrumb navigation of the current route match and all its ancestors. 46 | const paths = currentRoute 47 | ? queryAncestors(routeList, currentRoute, 'breadcrumbParentId').reverse() 48 | : [ 49 | routeList[0], 50 | { 51 | id: 404, 52 | name: t`Not Found`, 53 | }, 54 | ] 55 | 56 | return ( 57 | <Breadcrumb className={styles.bread}> 58 | {this.generateBreadcrumbs(paths)} 59 | </Breadcrumb> 60 | ) 61 | } 62 | } 63 | 64 | Bread.propTypes = { 65 | routeList: PropTypes.array, 66 | } 67 | 68 | export default Bread 69 | -------------------------------------------------------------------------------- /src/components/Layout/Bread.less: -------------------------------------------------------------------------------- 1 | .bread { 2 | margin-bottom: 24px; 3 | 4 | :global { 5 | .ant-breadcrumb { 6 | display: flex; 7 | align-items: center; 8 | } 9 | } 10 | } 11 | 12 | @media (max-width: 767px) { 13 | .bread { 14 | margin-bottom: 12px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Layout/Header.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .header { 4 | padding: 0; 5 | box-shadow: @shadow-2; 6 | position: relative; 7 | display: flex; 8 | justify-content: space-between; 9 | height: 72px; 10 | z-index: 9; 11 | align-items: center; 12 | background-color: #fff; 13 | 14 | &.fixed { 15 | position: fixed; 16 | top: 0; 17 | right: 0; 18 | width: ~'calc(100% - 256px)'; 19 | z-index: 29; 20 | transition: width 0.2s; 21 | 22 | &.collapsed { 23 | width: ~'calc(100% - 80px)'; 24 | } 25 | } 26 | 27 | :global { 28 | .ant-menu-submenu-title { 29 | height: 72px; 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .ant-menu-horizontal { 35 | line-height: 72px; 36 | 37 | & > .ant-menu-submenu:hover { 38 | color: @primary-color; 39 | background-color: @hover-color; 40 | } 41 | } 42 | 43 | .ant-menu { 44 | border-bottom: none; 45 | height: 72px; 46 | } 47 | 48 | .ant-menu-horizontal > .ant-menu-submenu { 49 | top: 0; 50 | margin-top: 0; 51 | } 52 | 53 | .ant-menu-horizontal > .ant-menu-item, 54 | .ant-menu-horizontal > .ant-menu-submenu { 55 | border-bottom: none; 56 | } 57 | 58 | .ant-menu-horizontal > .ant-menu-item-active, 59 | .ant-menu-horizontal > .ant-menu-item-open, 60 | .ant-menu-horizontal > .ant-menu-item-selected, 61 | .ant-menu-horizontal > .ant-menu-item:hover, 62 | .ant-menu-horizontal > .ant-menu-submenu-active, 63 | .ant-menu-horizontal > .ant-menu-submenu-open, 64 | .ant-menu-horizontal > .ant-menu-submenu-selected, 65 | .ant-menu-horizontal > .ant-menu-submenu:hover { 66 | border-bottom: none; 67 | } 68 | } 69 | 70 | .rightContainer { 71 | display: flex; 72 | align-items: center; 73 | } 74 | 75 | .button { 76 | width: 72px; 77 | height: 72px; 78 | line-height: 72px; 79 | text-align: center; 80 | font-size: 18px; 81 | cursor: pointer; 82 | transition: @transition-ease-in; 83 | 84 | &:hover { 85 | color: @primary-color; 86 | background-color: @hover-color; 87 | } 88 | } 89 | } 90 | 91 | .iconButton { 92 | width: 48px; 93 | height: 48px; 94 | display: flex; 95 | justify-content: center; 96 | align-items: center; 97 | border-radius: 24px; 98 | cursor: pointer; 99 | .background-hover(); 100 | 101 | &:hover { 102 | .iconFont { 103 | color: @primary-color; 104 | } 105 | } 106 | 107 | & + .iconButton { 108 | margin-left: 8px; 109 | } 110 | 111 | .iconFont { 112 | color: #b2b0c7; 113 | font-size: 24px; 114 | } 115 | } 116 | 117 | .notification { 118 | padding: 24px 0; 119 | width: 320px; 120 | .notificationItem { 121 | transition: all 0.3s; 122 | padding: 12px 24px; 123 | cursor: pointer; 124 | &:hover { 125 | background-color: @hover-color; 126 | } 127 | } 128 | .clearButton { 129 | text-align: center; 130 | height: 48px; 131 | line-height: 48px; 132 | cursor: pointer; 133 | .background-hover(); 134 | } 135 | } 136 | 137 | .notificationPopover { 138 | :global { 139 | .ant-popover-inner-content { 140 | padding: 0; 141 | } 142 | .ant-popover-arrow { 143 | display: none; 144 | } 145 | .ant-list-item-content { 146 | flex: 0; 147 | margin-left: 16px; 148 | } 149 | } 150 | } 151 | 152 | @media (max-width: 767px) { 153 | .header { 154 | width: 100% !important; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/components/Layout/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu } from 'antd' 4 | import { NavLink, withRouter } from 'umi' 5 | import { pathToRegexp } from 'path-to-regexp' 6 | import { arrayToTree, queryAncestors } from 'utils' 7 | import iconMap from 'utils/iconMap' 8 | import store from 'store' 9 | 10 | const { SubMenu } = Menu 11 | 12 | @withRouter 13 | class SiderMenu extends PureComponent { 14 | state = { 15 | openKeys: store.get('openKeys') || [], 16 | } 17 | 18 | onOpenChange = openKeys => { 19 | const { menus } = this.props 20 | const rootSubmenuKeys = menus.filter(_ => !_.menuParentId).map(_ => _.id) 21 | 22 | const latestOpenKey = openKeys.find( 23 | key => this.state.openKeys.indexOf(key) === -1 24 | ) 25 | 26 | let newOpenKeys = openKeys 27 | if (rootSubmenuKeys.indexOf(latestOpenKey) !== -1) { 28 | newOpenKeys = latestOpenKey ? [latestOpenKey] : [] 29 | } 30 | 31 | this.setState({ 32 | openKeys: newOpenKeys, 33 | }) 34 | store.set('openKeys', newOpenKeys) 35 | } 36 | 37 | generateMenus = data => { 38 | return data.map(item => { 39 | if (item.children) { 40 | return ( 41 | <SubMenu 42 | key={item.id} 43 | title={ 44 | <Fragment> 45 | {item.icon && iconMap[item.icon]} 46 | <span>{item.name}</span> 47 | </Fragment> 48 | } 49 | > 50 | {this.generateMenus(item.children)} 51 | </SubMenu> 52 | ) 53 | } 54 | return ( 55 | <Menu.Item key={item.id}> 56 | <NavLink to={item.route || '#'}> 57 | {item.icon && iconMap[item.icon]} 58 | <span>{item.name}</span> 59 | </NavLink> 60 | </Menu.Item> 61 | ) 62 | }) 63 | } 64 | 65 | render() { 66 | const { 67 | collapsed, 68 | theme, 69 | menus, 70 | location, 71 | isMobile, 72 | onCollapseChange, 73 | } = this.props 74 | 75 | // Generating tree-structured data for menu content. 76 | const menuTree = arrayToTree(menus, 'id', 'menuParentId') 77 | 78 | // Find a menu that matches the pathname. 79 | const currentMenu = menus.find( 80 | _ => _.route && pathToRegexp(_.route).exec(location.pathname) 81 | ) 82 | 83 | // Find the key that should be selected according to the current menu. 84 | const selectedKeys = currentMenu 85 | ? queryAncestors(menus, currentMenu, 'menuParentId').map(_ => _.id) 86 | : [] 87 | 88 | const menuProps = collapsed 89 | ? {} 90 | : { 91 | openKeys: this.state.openKeys, 92 | } 93 | 94 | return ( 95 | <Menu 96 | mode="inline" 97 | theme={theme} 98 | onOpenChange={this.onOpenChange} 99 | selectedKeys={selectedKeys} 100 | onClick={ 101 | isMobile 102 | ? () => { 103 | onCollapseChange(true) 104 | } 105 | : undefined 106 | } 107 | {...menuProps} 108 | > 109 | {this.generateMenus(menuTree)} 110 | </Menu> 111 | ) 112 | } 113 | } 114 | 115 | SiderMenu.propTypes = { 116 | menus: PropTypes.array, 117 | theme: PropTypes.string, 118 | isMobile: PropTypes.bool, 119 | onCollapseChange: PropTypes.func, 120 | } 121 | 122 | export default SiderMenu 123 | -------------------------------------------------------------------------------- /src/components/Layout/Sider.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Switch, Layout } from 'antd' 4 | import { t } from "@lingui/macro" 5 | import { Trans } from "@lingui/macro" 6 | import { BulbOutlined } from '@ant-design/icons' 7 | import ScrollBar from '../ScrollBar' 8 | import { config } from 'utils' 9 | import SiderMenu from './Menu' 10 | import styles from './Sider.less' 11 | 12 | class Sider extends PureComponent { 13 | render() { 14 | const { 15 | menus, 16 | theme, 17 | isMobile, 18 | collapsed, 19 | onThemeChange, 20 | onCollapseChange, 21 | } = this.props 22 | 23 | return ( 24 | <Layout.Sider 25 | width={256} 26 | theme={theme} 27 | breakpoint="lg" 28 | trigger={null} 29 | collapsible 30 | collapsed={collapsed} 31 | onBreakpoint={!isMobile ? onCollapseChange : (broken) => {}} 32 | className={styles.sider} 33 | > 34 | <div className={styles.brand}> 35 | <div className={styles.logo}> 36 | <img alt="logo" src={config.logoPath} /> 37 | {!collapsed && <h1>{config.siteName}</h1>} 38 | </div> 39 | </div> 40 | 41 | <div className={styles.menuContainer}> 42 | <ScrollBar 43 | options={{ 44 | // Disabled horizontal scrolling, https://github.com/utatti/perfect-scrollbar#options 45 | suppressScrollX: true, 46 | }} 47 | > 48 | <SiderMenu 49 | menus={menus} 50 | theme={theme} 51 | isMobile={isMobile} 52 | collapsed={collapsed} 53 | onCollapseChange={onCollapseChange} 54 | /> 55 | </ScrollBar> 56 | </div> 57 | {!collapsed && ( 58 | <div className={styles.switchTheme}> 59 | <span> 60 | <BulbOutlined /> 61 | <Trans>Switch Theme</Trans> 62 | </span> 63 | <Switch 64 | onChange={onThemeChange.bind( 65 | this, 66 | theme === 'dark' ? 'light' : 'dark' 67 | )} 68 | defaultChecked={theme === 'dark'} 69 | checkedChildren={t`Dark`} 70 | unCheckedChildren={t`Light`} 71 | /> 72 | </div> 73 | )} 74 | </Layout.Sider> 75 | ) 76 | } 77 | } 78 | 79 | Sider.propTypes = { 80 | menus: PropTypes.array, 81 | theme: PropTypes.string, 82 | isMobile: PropTypes.bool, 83 | collapsed: PropTypes.bool, 84 | onThemeChange: PropTypes.func, 85 | onCollapseChange: PropTypes.func, 86 | } 87 | 88 | export default Sider 89 | -------------------------------------------------------------------------------- /src/components/Layout/Sider.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .sider { 4 | box-shadow: fade(@primary-color, 10%) 0 0 28px 0; 5 | z-index: 10; 6 | :global { 7 | .ant-layout-sider-children { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | } 12 | } 13 | } 14 | 15 | .brand { 16 | z-index: 1; 17 | height: 72px; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | padding: 0 24px; 22 | box-shadow: 0 1px 9px -3px rgba(0, 0, 0, 0.2); 23 | .logo { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | 28 | img { 29 | width: 36px; 30 | margin-right: 8px; 31 | } 32 | 33 | h1 { 34 | vertical-align: text-bottom; 35 | font-size: 16px; 36 | text-transform: uppercase; 37 | display: inline-block; 38 | font-weight: 700; 39 | color: @primary-color; 40 | white-space: nowrap; 41 | margin-bottom: 0; 42 | .text-gradient(); 43 | 44 | :local { 45 | animation: fadeRightIn 300ms @ease-in-out; 46 | animation-fill-mode: both; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .menuContainer { 53 | height: ~'calc(100vh - 120px)'; 54 | overflow-x: hidden; 55 | flex: 1; 56 | padding: 24px 0; 57 | 58 | &::-webkit-scrollbar-thumb { 59 | background-color: transparent; 60 | } 61 | 62 | &:hover { 63 | &::-webkit-scrollbar-thumb { 64 | background-color: rgba(0, 0, 0, 0.2); 65 | } 66 | } 67 | 68 | :global { 69 | .ant-menu-inline { 70 | border-right: none; 71 | } 72 | } 73 | } 74 | 75 | .switchTheme { 76 | width: 100%; 77 | height: 48px; 78 | display: flex; 79 | justify-content: space-between; 80 | align-items: center; 81 | padding: 0 16px; 82 | overflow: hidden; 83 | transition: all 0.3s; 84 | 85 | span { 86 | white-space: nowrap; 87 | overflow: hidden; 88 | font-size: 12px; 89 | } 90 | 91 | :global { 92 | .anticon { 93 | min-width: 14px; 94 | margin-right: 4px; 95 | font-size: 14px; 96 | } 97 | } 98 | } 99 | 100 | @keyframes fadeLeftIn { 101 | 0% { 102 | transform: translateX(5px); 103 | opacity: 0; 104 | } 105 | 106 | 100% { 107 | transform: translateX(0); 108 | opacity: 1; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | import Menu from './Menu' 3 | import Bread from './Bread' 4 | import Sider from './Sider' 5 | 6 | export { Header, Menu, Bread, Sider } 7 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import styles from './Loader.less' 5 | 6 | const Loader = ({ spinning = false, fullScreen }) => { 7 | return ( 8 | <div 9 | className={classNames(styles.loader, { 10 | [styles.hidden]: !spinning, 11 | [styles.fullScreen]: fullScreen, 12 | })} 13 | > 14 | <div className={styles.warpper}> 15 | <div className={styles.inner} /> 16 | <div className={styles.text}>LOADING</div> 17 | </div> 18 | </div> 19 | ) 20 | } 21 | 22 | Loader.propTypes = { 23 | spinning: PropTypes.bool, 24 | fullScreen: PropTypes.bool, 25 | } 26 | 27 | export default Loader 28 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | background-color: #fff; 3 | width: 100%; 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | z-index: 100000; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | opacity: 1; 13 | text-align: center; 14 | 15 | &.fullScreen { 16 | position: fixed; 17 | } 18 | 19 | .warpper { 20 | width: 100px; 21 | height: 100px; 22 | display: inline-flex; 23 | flex-direction: column; 24 | justify-content: space-around; 25 | } 26 | 27 | .inner { 28 | width: 40px; 29 | height: 40px; 30 | margin: 0 auto; 31 | text-indent: -12345px; 32 | border-top: 1px solid rgba(0, 0, 0, 0.08); 33 | border-right: 1px solid rgba(0, 0, 0, 0.08); 34 | border-bottom: 1px solid rgba(0, 0, 0, 0.08); 35 | border-left: 1px solid rgba(0, 0, 0, 0.7); 36 | border-radius: 50%; 37 | z-index: 100001; 38 | 39 | :local { 40 | animation: spinner 600ms infinite linear; 41 | } 42 | } 43 | 44 | .text { 45 | width: 100px; 46 | height: 20px; 47 | text-align: center; 48 | font-size: 12px; 49 | letter-spacing: 4px; 50 | color: #000; 51 | } 52 | 53 | &.hidden { 54 | z-index: -1; 55 | opacity: 0; 56 | transition: opacity 1s ease 0.5s, z-index 0.1s ease 1.5s; 57 | } 58 | } 59 | @keyframes spinner { 60 | 0% { 61 | transform: rotate(0deg); 62 | } 63 | 64 | 100% { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Loader", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Loader.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Page/Page.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import Loader from '../Loader' 5 | import styles from './Page.less' 6 | 7 | export default class Page extends Component { 8 | render() { 9 | const { className, children, loading = false, inner = false } = this.props 10 | const loadingStyle = { 11 | height: 'calc(100vh - 184px)', 12 | overflow: 'hidden', 13 | } 14 | return ( 15 | <div 16 | className={classnames(className, { 17 | [styles.contentInner]: inner, 18 | })} 19 | style={loading ? loadingStyle : null} 20 | > 21 | {loading ? <Loader spinning /> : ''} 22 | {children} 23 | </div> 24 | ) 25 | } 26 | } 27 | 28 | Page.propTypes = { 29 | className: PropTypes.string, 30 | children: PropTypes.node, 31 | loading: PropTypes.bool, 32 | inner: PropTypes.bool, 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Page/Page.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .contentInner { 4 | background: #fff; 5 | padding: 24px; 6 | box-shadow: @shadow-1; 7 | min-height: ~'calc(100vh - 230px)'; 8 | position: relative; 9 | } 10 | 11 | @media (max-width: 767px) { 12 | .contentInner { 13 | padding: 12px; 14 | min-height: ~'calc(100vh - 160px)'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Page", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Page.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ScrollBar/index.js: -------------------------------------------------------------------------------- 1 | import ScrollBar from 'react-perfect-scrollbar' 2 | import 'react-perfect-scrollbar/dist/css/styles.css' 3 | import './index.less' 4 | 5 | export default ScrollBar 6 | -------------------------------------------------------------------------------- /src/components/ScrollBar/index.less: -------------------------------------------------------------------------------- 1 | :global { 2 | .ps--active-x > .ps__rail-x, 3 | .ps--active-y > .ps__rail-y { 4 | background-color: transparent; 5 | } 6 | 7 | .ps__rail-x:hover > .ps__thumb-x, 8 | .ps__rail-x:focus > .ps__thumb-x { 9 | height: 8px; 10 | } 11 | 12 | .ps__rail-y:hover > .ps__thumb-y, 13 | .ps__rail-y:focus > .ps__thumb-y { 14 | width: 8px; 15 | } 16 | 17 | .ps__rail-y, 18 | .ps__rail-x { 19 | z-index: 9; 20 | } 21 | 22 | .ps__thumb-y { 23 | width: 4px; 24 | right: 4px; 25 | } 26 | 27 | .ps__thumb-x { 28 | height: 4px; 29 | bottom: 4px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Editor from './Editor' 2 | import FilterItem from './FilterItem' 3 | import DropOption from './DropOption' 4 | import Loader from './Loader' 5 | import ScrollBar from './ScrollBar' 6 | import GlobalFooter from './GlobalFooter' 7 | import Ellipsis from './Ellipsis' 8 | import * as MyLayout from './Layout/index.js' 9 | import Page from './Page' 10 | 11 | export { MyLayout, Editor, GlobalFooter, Ellipsis, FilterItem, DropOption, Loader, Page, ScrollBar } 12 | -------------------------------------------------------------------------------- /src/e2e/login.e2e.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | 3 | describe('Login', () => { 4 | let browser 5 | let page 6 | 7 | beforeAll(async () => { 8 | browser = await puppeteer.launch({ args: ['--no-sandbox'] }) 9 | }) 10 | 11 | beforeEach(async () => { 12 | page = await browser.newPage() 13 | await page.goto('http://localhost:8000/en/login', { 14 | waitUntil: 'networkidle2', 15 | }) 16 | }) 17 | 18 | afterEach(() => page.close()) 19 | 20 | it('should login with failure', async () => { 21 | await page.waitFor(selector => !!document.querySelector('#username'), { 22 | timeout: 3000, 23 | }) 24 | await page.type('#username', 'wrong_user') 25 | await page.type('#password', 'wrong_password') 26 | await page.click('button[type="button"]') 27 | await page.waitForSelector('.anticon-close-circle') // should display error 28 | }) 29 | 30 | it('should login successfully', async () => { 31 | await page.waitForSelector('#username', { timeout: 3000 }) 32 | await page.type('#username', 'admin') 33 | await page.type('#password', 'admin') 34 | await page.click('button[type="button"]') 35 | await page.waitForSelector('.ant-layout-footer') 36 | const text = await page.evaluate(() => document.body.innerHTML) 37 | expect(text).toContain('Ant Design Admin') 38 | }) 39 | 40 | afterAll(() => browser.close()) 41 | }) 42 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'umi' 4 | import { Helmet } from 'react-helmet' 5 | import { Loader } from 'components' 6 | import { queryLayout } from 'utils' 7 | import NProgress from 'nprogress' 8 | import config from 'utils/config' 9 | import { withRouter } from 'umi' 10 | 11 | import PublicLayout from './PublicLayout' 12 | import PrimaryLayout from './PrimaryLayout' 13 | import './BaseLayout.less' 14 | 15 | const LayoutMap = { 16 | primary: PrimaryLayout, 17 | public: PublicLayout, 18 | } 19 | 20 | @withRouter 21 | @connect(({ loading }) => ({ loading })) 22 | class BaseLayout extends PureComponent { 23 | previousPath = '' 24 | 25 | render() { 26 | const { loading, children, location } = this.props 27 | const Container = LayoutMap[queryLayout(config.layouts, location.pathname)] 28 | 29 | const currentPath = location.pathname + location.search 30 | if (currentPath !== this.previousPath) { 31 | NProgress.start() 32 | } 33 | 34 | if (!loading.global) { 35 | NProgress.done() 36 | this.previousPath = currentPath 37 | } 38 | 39 | return ( 40 | <Fragment> 41 | <Helmet> 42 | <title>{config.siteName}</title> 43 | </Helmet> 44 | <Loader fullScreen spinning={loading.effects['app/query']} /> 45 | <Container>{children}</Container> 46 | </Fragment> 47 | ) 48 | } 49 | } 50 | 51 | BaseLayout.propTypes = { 52 | loading: PropTypes.object, 53 | } 54 | 55 | export default BaseLayout 56 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | @import '~themes/index.less'; 3 | 4 | :global { 5 | #nprogress { 6 | pointer-events: none; 7 | 8 | .bar { 9 | background: @primary-color; 10 | position: fixed; 11 | z-index: 2048; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | width: 100%; 16 | height: 2px; 17 | } 18 | 19 | .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color; 26 | opacity: 1; 27 | transform: rotate(3deg) translate(0, -4px); 28 | } 29 | 30 | .spinner { 31 | display: block; 32 | position: fixed; 33 | z-index: 1031; 34 | top: 15px; 35 | right: 15px; 36 | } 37 | 38 | .spinner-icon { 39 | width: 18px; 40 | height: 18px; 41 | box-sizing: border-box; 42 | border: solid 2px transparent; 43 | border-top-color: @primary-color; 44 | border-left-color: @primary-color; 45 | border-radius: 50%; 46 | 47 | :local { 48 | animation: nprogress-spinner 400ms linear infinite; 49 | } 50 | } 51 | } 52 | 53 | .nprogress-custom-parent { 54 | overflow: hidden; 55 | position: relative; 56 | 57 | #nprogress { 58 | .bar, 59 | .spinner { 60 | position: absolute; 61 | } 62 | } 63 | } 64 | } 65 | 66 | @keyframes nprogress-spinner { 67 | 0% { 68 | transform: rotate(0deg); 69 | } 70 | 71 | 100% { 72 | transform: rotate(360deg); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/layouts/PrimaryLayout.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .backTop { 4 | right: 50px; 5 | 6 | :global { 7 | .ant-back-top-content { 8 | background: @primary-color; 9 | opacity: 0.3; 10 | transition: all 0.3s; 11 | box-shadow: 0 0 15px 1px rgba(69, 65, 78, 0.1); 12 | 13 | &:hover { 14 | opacity: 1; 15 | } 16 | } 17 | } 18 | } 19 | 20 | .content { 21 | padding: 24px; 22 | min-height: ~'calc(100% - 72px)'; 23 | // overflow-y: scroll; 24 | } 25 | 26 | .container { 27 | height: 100vh; 28 | flex: 1; 29 | width: ~'calc(100% - 256px)'; 30 | overflow-y: scroll; 31 | overflow-x: hidden; 32 | } 33 | 34 | .footer { 35 | background: #fff; 36 | margin-top: 0; 37 | margin-bottom: 0; 38 | padding-top: 24px; 39 | padding-bottom: 24px; 40 | min-height: 72px; 41 | } 42 | 43 | @media (max-width: 767px) { 44 | .content { 45 | padding: 12px; 46 | } 47 | 48 | .backTop { 49 | right: 20px; 50 | bottom: 20px; 51 | } 52 | 53 | .container { 54 | height: 100vh; 55 | flex: 1; 56 | width: 100%; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/layouts/PublicLayout.js: -------------------------------------------------------------------------------- 1 | export default ({ children }) => { 2 | return children 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { withRouter } from 'umi' 3 | import { ConfigProvider } from 'antd' 4 | import { i18n } from "@lingui/core" 5 | import { I18nProvider } from '@lingui/react' 6 | import { getLocale } from 'utils' 7 | import { zh, en, pt } from 'make-plural/plurals' 8 | import zhCN from 'antd/lib/locale/zh_CN' 9 | import enUS from 'antd/lib/locale/en_US' 10 | import ptBR from 'antd/lib/locale/pt_BR' 11 | 12 | import BaseLayout from './BaseLayout' 13 | 14 | i18n.loadLocaleData({ 15 | en: { plurals: en }, 16 | zh: { plurals: zh }, 17 | 'pt-br': { plurals: pt } 18 | }) 19 | 20 | // antd 21 | const languages = { 22 | zh: zhCN, 23 | en: enUS, 24 | 'pt-br': ptBR 25 | } 26 | 27 | const { defaultLanguage } = i18n 28 | 29 | @withRouter 30 | class Layout extends Component { 31 | state = { 32 | } 33 | 34 | componentDidMount() { 35 | } 36 | 37 | loadCatalog = async (lan) => { 38 | const catalog = await import( 39 | `../locales/${lan}/messages.json` 40 | ) 41 | 42 | i18n.load(lan, catalog) 43 | i18n.activate(lan) 44 | } 45 | 46 | render() { 47 | const { children } = this.props 48 | 49 | let language = getLocale() 50 | 51 | if (!languages[language]) language = defaultLanguage 52 | 53 | this.loadCatalog(language) 54 | 55 | return ( 56 | <ConfigProvider locale={languages[language]}> 57 | <I18nProvider i18n={i18n}> 58 | <BaseLayout>{children}</BaseLayout> 59 | </I18nProvider> 60 | </ConfigProvider> 61 | ) 62 | } 63 | } 64 | 65 | export default Layout 66 | -------------------------------------------------------------------------------- /src/locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dashboard": "/dashboard", 3 | "Add Param": "Add Param", 4 | "Address": "Address", 5 | "Age": "Age", 6 | "Are you sure delete this record?": "Are you sure delete this record?", 7 | "Author": "Author", 8 | "Avatar": "Avatar", 9 | "Categories": "Categories", 10 | "Clear notifications": "Clear notifications", 11 | "Comments": "Comments", 12 | "Create": "Create", 13 | "Create User": "Create User", 14 | "CreateTime": "CreateTime", 15 | "Dark": "Dark", 16 | "Delete": "Delete", 17 | "Email": "Email", 18 | "Female": "Female", 19 | "Gender": "Gender", 20 | "Hi,": "Hi,", 21 | "Image": "Image", 22 | "Light": "Light", 23 | "Male": "Male", 24 | "Name": "Name", 25 | "NickName": "NickName", 26 | "Not Found": "Not Found", 27 | "Operation": "Operation", 28 | "Params": "Params", 29 | "Password": "Password", 30 | "Phone": "Phone", 31 | "Pick an address": "Pick an address", 32 | "Please pick an address": "Please pick an address", 33 | "Publised": "Publised", 34 | "Publish Date": "Publish Date", 35 | "Reset": "Reset", 36 | "Search": "Search", 37 | "Search Name": "Search Name", 38 | "Send": "Send", 39 | "Sign in": "Sign in", 40 | "Sign out": "Sign out", 41 | "Switch Theme": "Switch Theme", 42 | "Tags": "Tags", 43 | "The input is not valid E-mail!": "The input is not valid E-mail!", 44 | "The input is not valid phone!": "The input is not valid phone!", 45 | "Title": "Title", 46 | "Total {total} Items": "Total {total} Items", 47 | "Unpublished": "Unpublished", 48 | "Update": "Update", 49 | "Update User": "Update User", 50 | "Username": "Username", 51 | "Views": "Views", 52 | "Visibility": "Visibility", 53 | "You have viewed all notifications.": "You have viewed all notifications." 54 | } -------------------------------------------------------------------------------- /src/locales/pt-br/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dashboard": "/dashboard", 3 | "Add Param": "Add Parametro", 4 | "Address": "Endereço", 5 | "Age": "Ano", 6 | "Are you sure delete this record?": "Tem certeza de excluir este registro?", 7 | "Author": "Autor", 8 | "Avatar": "Avatar", 9 | "Categories": "Categorias", 10 | "Clear notifications": "limpar notificações", 11 | "Comments": "Comentarios", 12 | "Create": "Criar", 13 | "Create User": "Criar Usuário", 14 | "CreateTime": "CreateTime", 15 | "Dark": "Escuro", 16 | "Delete": "Deletar", 17 | "Email": "Email", 18 | "Female": "Feminino", 19 | "Gender": "Genero", 20 | "Hi,": "Olá,", 21 | "Image": "Imagem", 22 | "Light": "Claro", 23 | "Male": "masculino", 24 | "Name": "Nome", 25 | "NickName": "NickName", 26 | "Not Found": "Não Encontrado", 27 | "Operation": "Operation", 28 | "Params": "Parametros", 29 | "Password": "Senha", 30 | "Phone": "Fone", 31 | "Pick an address": "Escolha um endereço", 32 | "Please pick an address": "Por favor, escolha um endereço", 33 | "Publised": "Publicado", 34 | "Publish Date": "Data de publicação", 35 | "Reset": "Reset", 36 | "Search": "procurar", 37 | "Search Name": "Search Name", 38 | "Send": "Enviar", 39 | "Sign in": "Sign in", 40 | "Sign out": "Sign out", 41 | "Switch Theme": "Trocar tema", 42 | "Tags": "Tags", 43 | "The input is not valid E-mail!": "Não é um E-mail valido!", 44 | "The input is not valid phone!": "Não é um telefone Valido!", 45 | "Title": "Titulo", 46 | "Total {total} Items": "Total {total} Items", 47 | "Unpublished": "Não publicado", 48 | "Update": "Atualizar", 49 | "Update User": "Atualizar Usuário", 50 | "Username": "Usuário", 51 | "Views": "visualizações", 52 | "Visibility": "Visibilidade", 53 | "You have viewed all notifications.": "Você visualizou todas as notificações." 54 | } 55 | -------------------------------------------------------------------------------- /src/locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dashboard": "/zh/dashboard", 3 | "Add Param": "添加参数", 4 | "Address": "地址", 5 | "Age": "年龄", 6 | "Are you sure delete this record?": "您确定要删除这条记录吗?", 7 | "Author": "作者", 8 | "Avatar": "头像", 9 | "Categories": "类别", 10 | "Clear notifications": "清空消息", 11 | "Comments": "评论数", 12 | "Create": "创建", 13 | "Create User": "创建用户", 14 | "CreateTime": "创建时间", 15 | "Dark": "暗", 16 | "Delete": "删除", 17 | "Email": "电子邮件", 18 | "Female": "女", 19 | "Gender": "性别", 20 | "Hi,": "你好,", 21 | "Image": "图像", 22 | "Light": "明", 23 | "Male": "男性", 24 | "Name": "名字", 25 | "NickName": "昵称", 26 | "Not Found": "未找到", 27 | "Operation": "操作", 28 | "Params": "参数", 29 | "Password": "密码", 30 | "Phone": "电话", 31 | "Pick an address": "选择地址", 32 | "Please pick an address": "选择地址", 33 | "Publised": "已发布", 34 | "Publish Date": "发布日期", 35 | "Reset": "重置", 36 | "Search": "搜索", 37 | "Search Name": "搜索名字", 38 | "Send": "发送", 39 | "Sign in": "登录", 40 | "Sign out": "退出登录", 41 | "Switch Theme": "切换主题", 42 | "Tags": "标签", 43 | "The input is not valid E-mail!": "输入的电子邮件无效!", 44 | "The input is not valid phone!": "输入无效的手机!", 45 | "Title": "标题", 46 | "Total {total} Items": "总共 {total} 条记录", 47 | "Unpublished": "未发布", 48 | "Update": "更新", 49 | "Update User": "更新用户", 50 | "Username": "用户名", 51 | "Views": "浏览数", 52 | "Visibility": "可见性", 53 | "You have viewed all notifications.": "您已查看所有通知" 54 | } -------------------------------------------------------------------------------- /src/pages/404.less: -------------------------------------------------------------------------------- 1 | .error { 2 | color: black; 3 | text-align: center; 4 | position: absolute; 5 | top: 30%; 6 | margin-top: -50px; 7 | left: 50%; 8 | margin-left: -100px; 9 | width: 200px; 10 | 11 | :global .anticon { 12 | font-size: 48px; 13 | margin-bottom: 16px; 14 | } 15 | 16 | h1 { 17 | font-family: cursive; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FrownOutlined } from '@ant-design/icons' 3 | import { Page } from 'components' 4 | import styles from './404.less' 5 | 6 | const Error = () => ( 7 | <Page inner> 8 | <div className={styles.error}> 9 | <FrownOutlined /> 10 | <h1>404 Not Found</h1> 11 | </div> 12 | </Page> 13 | ) 14 | 15 | export default Error 16 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/ChartShowLoadingComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | 4 | class ChartShowLoadingComponent extends React.Component { 5 | constructor() { 6 | super() 7 | this._t = null 8 | this.onChartReady = this.onChartReady.bind(this) 9 | } 10 | componentWillUnmount() { 11 | clearTimeout(this._t) 12 | } 13 | 14 | onChartReady(chart) { 15 | this._t = setTimeout(() => { 16 | chart.hideLoading() 17 | }, 3000) 18 | } 19 | 20 | render() { 21 | const getOtion = () => { 22 | const option = { 23 | title: { 24 | text: '基础雷达图', 25 | }, 26 | tooltip: {}, 27 | legend: { 28 | data: ['预算分配(Allocated Budget)', '实际开销(Actual Spending)'], 29 | }, 30 | radar: { 31 | indicator: [ 32 | { name: '销售(sales)', max: 6500 }, 33 | { name: '管理(Administration)', max: 16000 }, 34 | { name: '信息技术(Information Techology)', max: 30000 }, 35 | { name: '客服(Customer Support)', max: 38000 }, 36 | { name: '研发(Development)', max: 52000 }, 37 | { name: '市场(Marketing)', max: 25000 }, 38 | ], 39 | }, 40 | series: [ 41 | { 42 | name: '预算 vs 开销(Budget vs spending)', 43 | type: 'radar', 44 | data: [ 45 | { 46 | value: [4300, 10000, 28000, 35000, 50000, 19000], 47 | name: '预算分配(Allocated Budget)', 48 | }, 49 | { 50 | value: [5000, 14000, 28000, 31000, 42000, 21000], 51 | name: '实际开销(Actual Spending)', 52 | }, 53 | ], 54 | }, 55 | ], 56 | } 57 | return option 58 | } 59 | const getLoadingOption = () => { 60 | const option = { 61 | text: '加载中...', 62 | color: '#4413c2', 63 | textColor: '#270240', 64 | maskColor: 'rgba(194, 88, 86, 0.3)', 65 | zlevel: 0, 66 | } 67 | return option 68 | } 69 | 70 | let code = 71 | 'onChartReady: function(chart) {\n' + 72 | " 'chart.hideLoading();\n" + 73 | '}\n\n' + 74 | '<ReactEcharts \n' + 75 | ' option={this.getOtion()} \n' + 76 | ' onChartReady={this.onChartReady} \n' + 77 | ' loadingOption={this.getLoadingOption()} \n' + 78 | ' showLoading={true} />' 79 | 80 | return ( 81 | <div className="examples"> 82 | <div className="parent"> 83 | <label> 84 | {' '} 85 | Chart loading With <strong> showLoading </strong>: (when chart 86 | ready, hide the loading mask.) 87 | </label> 88 | <ReactEcharts 89 | option={getOtion()} 90 | onChartReady={this.onChartReady} 91 | loadingOption={getLoadingOption()} 92 | showLoading 93 | /> 94 | <label> code below: </label> 95 | <pre> 96 | <code>{code}</code> 97 | </pre> 98 | </div> 99 | </div> 100 | ) 101 | } 102 | } 103 | 104 | export default ChartShowLoadingComponent 105 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/ChartWithEventComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | 4 | const ChartWithEventComponent = () => { 5 | const onChartReady = echart => { 6 | /* eslint-disable */ 7 | console.log('echart is ready', echart) 8 | } 9 | const onChartLegendselectchanged = (param, echart) => { 10 | console.log(param, echart) 11 | } 12 | const onChartClick = (param, echart) => { 13 | console.log(param, echart) 14 | } 15 | const getOtion = () => { 16 | const option = { 17 | title: { 18 | text: '某站点用户访问来源', 19 | subtext: '纯属虚构', 20 | x: 'center', 21 | }, 22 | tooltip: { 23 | trigger: 'item', 24 | formatter: '{a} <br/>{b} : {c} ({d}%)', 25 | }, 26 | legend: { 27 | orient: 'vertical', 28 | left: 'left', 29 | data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎'], 30 | }, 31 | series: [ 32 | { 33 | name: '访问来源', 34 | type: 'pie', 35 | radius: '55%', 36 | center: ['50%', '60%'], 37 | data: [ 38 | { value: 335, name: '直接访问' }, 39 | { value: 310, name: '邮件营销' }, 40 | { value: 234, name: '联盟广告' }, 41 | { value: 135, name: '视频广告' }, 42 | { value: 1548, name: '搜索引擎' }, 43 | ], 44 | itemStyle: { 45 | emphasis: { 46 | shadowBlur: 10, 47 | shadowOffsetX: 0, 48 | shadowColor: 'rgba(0, 0, 0, 0.5)', 49 | }, 50 | }, 51 | }, 52 | ], 53 | } 54 | return option 55 | } 56 | 57 | let onEvents = { 58 | click: onChartClick, 59 | legendselectchanged: onChartLegendselectchanged, 60 | } 61 | let code = 62 | 'let onEvents = {\n' + 63 | " 'click': onChartClick,\n" + 64 | " 'legendselectchanged': onChartLegendselectchanged\n" + 65 | '}\n\n' + 66 | '<ReactEcharts \n' + 67 | ' option={getOtion()} \n' + 68 | ' style={{height: 300}} \n' + 69 | ' onChartReady={onChartReady} \n' + 70 | ' onEvents={onEvents} />' 71 | 72 | return ( 73 | <div className="examples"> 74 | <div className="parent"> 75 | <label> 76 | {' '} 77 | Chart With event <strong> onEvents </strong>: (Click the chart, and 78 | watch the console) 79 | </label> 80 | <ReactEcharts 81 | option={getOtion()} 82 | style={{ height: 300 }} 83 | onChartReady={onChartReady} 84 | onEvents={onEvents} 85 | /> 86 | <label> code below: </label> 87 | <pre> 88 | <code>{code}</code> 89 | </pre> 90 | </div> 91 | </div> 92 | ) 93 | } 94 | 95 | export default ChartWithEventComponent 96 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/EchartsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import SimpleChartComponent from './SimpleChartComponent' 5 | import ChartWithEventComponent from './ChartWithEventComponent' 6 | import ThemeChartComponent from './ThemeChartComponent' 7 | import ChartShowLoadingComponent from './ChartShowLoadingComponent' 8 | import ChartAPIComponent from './ChartAPIComponent' 9 | import DynamicChartComponent from './DynamicChartComponent' 10 | import MapChartComponent from './MapChartComponent' 11 | 12 | // v1.2.0 add 7 demo. 13 | import AirportCoordComponent from './AirportCoordComponent' 14 | import CalendarComponent from './CalendarComponent' 15 | import GaugeComponent from './GaugeComponent' 16 | import GCalendarComponent from './GCalendarComponent' 17 | import GraphComponent from './GraphComponent' 18 | import LunarCalendarComponent from './LunarCalendarComponent' 19 | import TreemapComponent from './TreemapComponent' 20 | import LiquidfillComponent from './LiquidfillComponent' 21 | import BubbleGradientComponent from './BubbleGradientComponent' 22 | import TransparentBar3DComPonent from './TransparentBar3DComPonent' 23 | 24 | const EchartsComponent = ({ type }) => { 25 | if (type === 'simple') return <SimpleChartComponent /> 26 | if (type === 'loading') return <ChartShowLoadingComponent /> 27 | if (type === 'api') return <ChartAPIComponent /> 28 | if (type === 'events') return <ChartWithEventComponent /> 29 | if (type === 'theme') return <ThemeChartComponent /> 30 | if (type === 'dynamic') return <DynamicChartComponent /> 31 | if (type === 'map') return <MapChartComponent /> 32 | if (type === 'airport') return <AirportCoordComponent /> 33 | if (type === 'graph') return <GraphComponent /> 34 | if (type === 'calendar') return <CalendarComponent /> 35 | if (type === 'treemap') return <TreemapComponent /> 36 | if (type === 'gauge') return <GaugeComponent /> 37 | if (type === 'gcalendar') return <GCalendarComponent /> 38 | if (type === 'lunar') return <LunarCalendarComponent /> 39 | if (type === 'liquid') return <LiquidfillComponent /> 40 | if (type === 'BubbleGradientComponent') return <BubbleGradientComponent /> 41 | if (type === 'TransparentBar3DComPonent') return <TransparentBar3DComPonent /> 42 | return <DynamicChartComponent /> 43 | } 44 | 45 | EchartsComponent.propTypes = { 46 | type: PropTypes.string, 47 | } 48 | 49 | export default EchartsComponent 50 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/GCalendarComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | import * as echarts from 'echarts' 4 | 5 | const GCalendarComponent = () => { 6 | const getVirtulData = year => { 7 | year = year || '2017' 8 | let date = +echarts.number.parseDate(`${year}-01-01`) 9 | let end = +echarts.number.parseDate(`${+year + 1}-01-01`) 10 | let dayTime = 3600 * 24 * 1000 11 | let data = [] 12 | for (let time = date; time < end; time += dayTime) { 13 | data.push([ 14 | echarts.format.formatTime('yyyy-MM-dd', time), 15 | Math.floor(Math.random() * 1000), 16 | ]) 17 | } 18 | return data 19 | } 20 | 21 | const option = { 22 | tooltip: { 23 | position: 'top', 24 | }, 25 | visualMap: { 26 | min: 0, 27 | max: 1000, 28 | calculable: true, 29 | orient: 'horizontal', 30 | left: 'center', 31 | top: 'top', 32 | }, 33 | 34 | calendar: [ 35 | { 36 | range: '2017', 37 | cellSize: ['auto', 20], 38 | }, 39 | { 40 | top: 260, 41 | range: '2016', 42 | cellSize: ['auto', 20], 43 | }, 44 | ], 45 | 46 | series: [ 47 | { 48 | type: 'heatmap', 49 | coordinateSystem: 'calendar', 50 | calendarIndex: 0, 51 | data: getVirtulData(2017), 52 | }, 53 | { 54 | type: 'heatmap', 55 | coordinateSystem: 'calendar', 56 | calendarIndex: 1, 57 | data: getVirtulData(2016), 58 | }, 59 | ], 60 | } 61 | 62 | return ( 63 | <div className="examples"> 64 | <div className="parent"> 65 | <label> render a calendar like github commit history. </label> 66 | <ReactEcharts 67 | option={option} 68 | style={{ height: '500px', width: '100%' }} 69 | className="react_for_echarts" 70 | /> 71 | </div> 72 | </div> 73 | ) 74 | } 75 | 76 | export default GCalendarComponent 77 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/LiquidfillComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | 4 | require('echarts-liquidfill') 5 | 6 | const LiquidfillComponent = () => { 7 | const option = { 8 | series: [ 9 | { 10 | type: 'liquidFill', 11 | data: [0.6], 12 | }, 13 | ], 14 | } 15 | return ( 16 | <div className="examples"> 17 | <div className="parent"> 18 | <label>render a Liquidfill chart:</label> 19 | <ReactEcharts 20 | option={option} 21 | style={{ 22 | height: '400px', 23 | width: '100%', 24 | }} 25 | className="react_for_echarts" 26 | /> 27 | </div> 28 | </div> 29 | ) 30 | } 31 | 32 | export default LiquidfillComponent 33 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/MainPageComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AdSense from 'react-adsense' 3 | import { Link } from 'umi' 4 | import DynamicChartComponent from './DynamicChartComponent.js' 5 | 6 | const MainPageComponent = () => { 7 | return ( 8 | <div> 9 | <h1> echarts-for-react {this.props.params.type} </h1> 10 | <h3> 11 | {' '} 12 | A very simple echarts(v3.0) wrapper for React.{' '} 13 | <a href="https://github.com/hustcc/echarts-for-react"> 14 | hustcc/echarts-for-react 15 | </a> 16 | </h3> 17 | 18 | <AdSense.Google client="ca-pub-7292810486004926" slot="7806394673" /> 19 | 20 | <h4> 21 | <Link to="/echarts/simple">Simple demo</Link> | 22 | <Link to="/echarts/loading">Echarts loading</Link> | 23 | <Link to="/echarts/api">Echarts API</Link> | 24 | <Link to="/echarts/events">Echarts events</Link> | 25 | <Link to="/echarts/theme">Echarts theme</Link> | 26 | <Link to="/echarts/dynamic">Dynamic chart</Link> | 27 | <Link to="/echarts/map">Map chart</Link> 28 | </h4> 29 | <h4> 30 | <span style={{ color: 'red' }}>New</span> 31 | : 32 | <Link to="/echarts/airport">Airport</Link> | 33 | <Link to="/echarts/graph">Graph</Link> | 34 | <Link to="/echarts/calendar">Calendar</Link> | 35 | <Link to="/echarts/treemap">Treemap</Link> | 36 | <Link to="/echarts/gauge">Gauge</Link> | 37 | <Link to="/echarts/gcalendar">GCalendar</Link> | 38 | <Link to="/echarts/lunar">Lunar</Link> | 39 | <Link to="/echarts/liquid">Liquidfill</Link> 40 | </h4> 41 | {this.props.children || <DynamicChartComponent />} 42 | 43 | <h3> 44 | Get it on GitHub!{' '} 45 | <a href="https://github.com/hustcc/echarts-for-react"> 46 | hustcc/echarts-for-react 47 | </a> 48 | </h3> 49 | </div> 50 | ) 51 | } 52 | 53 | export default MainPageComponent 54 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/ModuleLoadChartComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | 4 | const ModuleLoadChartComponent = () => { 5 | const option = { 6 | title: { text: 'ECharts 入门示例' }, 7 | tooltip: {}, 8 | xAxis: { 9 | data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'], 10 | }, 11 | yAxis: {}, 12 | series: [ 13 | { 14 | name: '销量', 15 | type: 'bar', 16 | data: [5, 20, 36, 10, 10, 20], 17 | }, 18 | ], 19 | } 20 | 21 | let code = 22 | '<ReactEcharts \n' + 23 | ' option={this.getOtion()} \n' + 24 | " style={{height: '350px', width: '100%'}} \n" + 25 | " modules={['echarts/lib/chart/bar', 'echarts/lib/component/tooltip', 'echarts/lib/component/title']} \n" + 26 | " className='react_for_echarts' />" 27 | return ( 28 | <div className="examples"> 29 | <div className="parent"> 30 | <label> 31 | {' '} 32 | load echarts module as you wish <strong> 33 | reduce the file size 34 | </strong>:{' '} 35 | </label> 36 | <ReactEcharts 37 | option={option} 38 | style={{ height: '350px', width: '100%' }} 39 | modules={[ 40 | 'echarts/lib/chart/bar', 41 | 'echarts/lib/component/tooltip', 42 | 'echarts/lib/component/title', 43 | ]} 44 | className="react_for_echarts" 45 | /> 46 | <label> code below: </label> 47 | <pre> 48 | <code>{code}</code> 49 | </pre> 50 | </div> 51 | </div> 52 | ) 53 | } 54 | 55 | export default ModuleLoadChartComponent 56 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/SimpleChartComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | import './theme/macarons.js' 4 | 5 | const SimpleChartComponent = () => { 6 | const option = { 7 | title: { 8 | text: '堆叠区域图', 9 | }, 10 | tooltip: { 11 | trigger: 'axis', 12 | }, 13 | legend: { 14 | data: ['邮件营销', '联盟广告', '视频广告'], 15 | }, 16 | toolbox: { 17 | feature: { 18 | saveAsImage: {}, 19 | }, 20 | }, 21 | grid: { 22 | left: '3%', 23 | right: '4%', 24 | bottom: '3%', 25 | containLabel: true, 26 | }, 27 | xAxis: [ 28 | { 29 | type: 'category', 30 | boundaryGap: false, 31 | data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], 32 | }, 33 | ], 34 | yAxis: [ 35 | { 36 | type: 'value', 37 | }, 38 | ], 39 | series: [ 40 | { 41 | name: '邮件营销', 42 | type: 'line', 43 | stack: '总量', 44 | areaStyle: { normal: {} }, 45 | data: [120, 132, 101, 134, 90, 230, 210], 46 | }, 47 | { 48 | name: '联盟广告', 49 | type: 'line', 50 | stack: '总量', 51 | areaStyle: { normal: {} }, 52 | data: [220, 182, 191, 234, 290, 330, 310], 53 | }, 54 | { 55 | name: '视频广告', 56 | type: 'line', 57 | stack: '总量', 58 | areaStyle: { normal: {} }, 59 | data: [150, 232, 201, 154, 190, 330, 410], 60 | }, 61 | ], 62 | } 63 | let code = 64 | '<ReactEcharts \n' + 65 | ' option={this.getOtion()} \n' + 66 | " style={{height: '350px', width: '100%'}} \n" + 67 | " className='react_for_echarts' />" 68 | return ( 69 | <div className="examples"> 70 | <div className="parent"> 71 | <label> 72 | {' '} 73 | render a Simple echart With <strong>option and height</strong>:{' '} 74 | </label> 75 | <ReactEcharts 76 | option={option} 77 | style={{ height: '350px', width: '100%' }} 78 | className="react_for_echarts" 79 | theme="macarons" 80 | /> 81 | <label> code below: </label> 82 | <pre> 83 | <code>{code}</code> 84 | </pre> 85 | </div> 86 | </div> 87 | ) 88 | } 89 | 90 | export default SimpleChartComponent 91 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/ThemeChartComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactEcharts from 'echarts-for-react' 3 | 4 | import * as echarts from 'echarts' 5 | 6 | const ThemeChartComponent = () => { 7 | const option = { 8 | title: { 9 | text: '阶梯瀑布图', 10 | subtext: 'From ExcelHome', 11 | sublink: 'http://e.weibo.com/1341556070/Aj1J2x5a5', 12 | }, 13 | tooltip: { 14 | trigger: 'axis', 15 | axisPointer: { 16 | // 坐标轴指示器,坐标轴触发有效 17 | type: 'shadow', // 默认为直线,可选为:'line' | 'shadow' 18 | }, 19 | }, 20 | legend: { 21 | data: ['支出', '收入'], 22 | }, 23 | grid: { 24 | left: '3%', 25 | right: '4%', 26 | bottom: '3%', 27 | containLabel: true, 28 | }, 29 | xAxis: { 30 | type: 'category', 31 | splitLine: { show: false }, 32 | data: [ 33 | '11月1日', 34 | '11月2日', 35 | '11月3日', 36 | '11月4日', 37 | '11月5日', 38 | '11月6日', 39 | '11月7日', 40 | '11月8日', 41 | '11月9日', 42 | '11月10日', 43 | '11月11日', 44 | ], 45 | }, 46 | yAxis: { 47 | type: 'value', 48 | }, 49 | series: [ 50 | { 51 | name: '辅助', 52 | type: 'bar', 53 | stack: '总量', 54 | itemStyle: { 55 | normal: { 56 | barBorderColor: 'rgba(0,0,0,0)', 57 | color: 'rgba(0,0,0,0)', 58 | }, 59 | emphasis: { 60 | barBorderColor: 'rgba(0,0,0,0)', 61 | color: 'rgba(0,0,0,0)', 62 | }, 63 | }, 64 | data: [0, 900, 1245, 1530, 1376, 1376, 1511, 1689, 1856, 1495, 1292], 65 | }, 66 | { 67 | name: '收入', 68 | type: 'bar', 69 | stack: '总量', 70 | label: { 71 | normal: { 72 | show: true, 73 | position: 'top', 74 | }, 75 | }, 76 | data: [900, 345, 393, '-', '-', 135, 178, 286, '-', '-', '-'], 77 | }, 78 | { 79 | name: '支出', 80 | type: 'bar', 81 | stack: '总量', 82 | label: { 83 | normal: { 84 | show: true, 85 | position: 'bottom', 86 | }, 87 | }, 88 | data: ['-', '-', '-', 108, 154, '-', '-', '-', 119, 361, 203], 89 | }, 90 | ], 91 | } 92 | 93 | echarts.registerTheme('my_theme', { 94 | backgroundColor: '#f4cccc', 95 | }) 96 | 97 | let code = 98 | "echarts.registerTheme('my_theme', {\n" + 99 | " backgroundColor: '#f4cccc'\n" + 100 | '});\n\n' + 101 | '<ReactEcharts \n' + 102 | ' option={this.getOtion()} \n' + 103 | " theme='my_theme' />" 104 | return ( 105 | <div className="examples"> 106 | <div className="parent"> 107 | <label> 108 | {' '} 109 | render a echart With <strong>theme</strong>, should{' '} 110 | <strong>echarts.registerTheme(themeName, themeObj)</strong> before 111 | use. 112 | </label> 113 | <ReactEcharts option={option} theme="my_theme" /> 114 | <label> 115 | {' '} 116 | the theme object format: 117 | https://github.com/ecomfe/echarts/blob/master/theme/dark.js 118 | </label> 119 | <pre> 120 | <code>{code}</code> 121 | </pre> 122 | </div> 123 | </div> 124 | ) 125 | } 126 | 127 | export default ThemeChartComponent 128 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Radio } from 'antd' 3 | import { Page } from 'components' 4 | import EchartsComponent from './EchartsComponent' 5 | import styles from './index.less' 6 | 7 | const RadioGroup = Radio.Group 8 | 9 | const chartList = [ 10 | { 11 | label: 'SimpleChart', 12 | value: 'simple', 13 | }, 14 | { 15 | label: 'ChartShowLoading', 16 | value: 'loading', 17 | }, 18 | { 19 | label: 'ChartAPI', 20 | value: 'api', 21 | }, 22 | { 23 | label: 'ChartWithEvent', 24 | value: 'events', 25 | }, 26 | { 27 | label: 'ThemeChart', 28 | value: 'theme', 29 | }, 30 | { 31 | label: 'DynamicChart', 32 | value: 'dynamic', 33 | }, 34 | { 35 | label: 'MapChart', 36 | value: 'map', 37 | }, 38 | { 39 | label: 'AirportCoord', 40 | value: 'airport', 41 | }, 42 | { 43 | label: 'Graph', 44 | value: 'graph', 45 | }, 46 | { 47 | label: 'Calendar', 48 | value: 'calendar', 49 | }, 50 | { 51 | label: 'Treemap', 52 | value: 'treemap', 53 | }, 54 | { 55 | label: 'Gauge', 56 | value: 'gauge', 57 | }, 58 | { 59 | label: 'GCalendar', 60 | value: 'gcalendar', 61 | }, 62 | { 63 | label: 'LunarCalendar', 64 | value: 'lunar', 65 | }, 66 | { 67 | label: 'Liquidfill', 68 | value: 'liquid', 69 | }, 70 | { 71 | label: 'BubbleGradient', 72 | value: 'BubbleGradientComponent', 73 | }, 74 | { 75 | label: 'TransparentBar3D', 76 | value: 'TransparentBar3DComPonent', 77 | }, 78 | ] 79 | 80 | class Chart extends React.Component { 81 | constructor() { 82 | super() 83 | this.state = { 84 | type: '', 85 | } 86 | this.handleRadioGroupChange = this.handleRadioGroupChange.bind(this) 87 | } 88 | handleRadioGroupChange(e) { 89 | this.setState({ 90 | type: e.target.value, 91 | }) 92 | } 93 | render() { 94 | return ( 95 | <Page inner id="EChartsMain"> 96 | <RadioGroup 97 | options={chartList} 98 | defaultValue="dynamic" 99 | onChange={this.handleRadioGroupChange} 100 | /> 101 | <div className={styles.chart}> 102 | <EchartsComponent type={this.state.type} /> 103 | </div> 104 | <div style={{ padding: 24, marginTop: 24 }}> 105 | All demos from{' '} 106 | <a href="https://github.com/hustcc/echarts-for-react"> 107 | https://github.com/hustcc/echarts-for-react 108 | </a> 109 | </div> 110 | </Page> 111 | ) 112 | } 113 | } 114 | 115 | export default Chart 116 | -------------------------------------------------------------------------------- /src/pages/chart/ECharts/index.less: -------------------------------------------------------------------------------- 1 | .chart { 2 | label { 3 | margin: 24px 0; 4 | display: block; 5 | font-size: 14px; 6 | } 7 | 8 | pre { 9 | padding: 16px; 10 | overflow: auto; 11 | font-size: 12px; 12 | line-height: 2; 13 | background-color: #f6f8fa; 14 | border-radius: 3px; 15 | 16 | code { 17 | display: inline; 18 | max-width: auto; 19 | padding: 0; 20 | margin: 0; 21 | overflow: visible; 22 | line-height: inherit; 23 | word-wrap: normal; 24 | background-color: transparent; 25 | border: 0; 26 | } 27 | } 28 | } 29 | 30 | :global { 31 | .ant-radio-wrapper { 32 | margin-bottom: 16px; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/chart/Recharts/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ResponsiveContainer } from 'recharts' 4 | import styles from './Container.less' 5 | 6 | const Container = ({ 7 | children, 8 | ratio = 5 / 2, 9 | minHeight = 250, 10 | maxHeight = 350, 11 | }) => ( 12 | <div className={styles.container} style={{ minHeight, maxHeight }}> 13 | <div style={{ marginTop: `${100 / ratio}%` || '100%' }} /> 14 | <div className={styles.content} style={{ minHeight, maxHeight }}> 15 | <ResponsiveContainer>{children}</ResponsiveContainer> 16 | </div> 17 | </div> 18 | ) 19 | 20 | Container.propTypes = { 21 | children: PropTypes.element.isRequired, 22 | ratio: PropTypes.number, 23 | minHeight: PropTypes.number, 24 | maxHeight: PropTypes.number, 25 | } 26 | 27 | export default Container 28 | -------------------------------------------------------------------------------- /src/pages/chart/Recharts/Container.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | position: relative; 4 | display: inline-block; 5 | 6 | :global { 7 | .recharts-responsive-container { 8 | width: e('calc(100% + 56px)') !important; 9 | margin-left: -32px; 10 | } 11 | } 12 | } 13 | 14 | .content { 15 | position: absolute; 16 | left: 0; 17 | right: 0; 18 | top: 0; 19 | bottom: 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/chart/Recharts/ReChartsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AreaChartComponent from './AreaChartComponent' 5 | import BarChartComponent from './BarChartComponent' 6 | import LineChartComponent from './LineChartComponent' 7 | 8 | const ReChartsComponent = ({ type }) => { 9 | if (type === 'areaChart') return <AreaChartComponent /> 10 | if (type === 'barChart') return <BarChartComponent /> 11 | return <LineChartComponent /> 12 | } 13 | 14 | ReChartsComponent.propTypes = { 15 | type: PropTypes.string, 16 | } 17 | 18 | export default ReChartsComponent 19 | -------------------------------------------------------------------------------- /src/pages/chart/Recharts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Radio } from 'antd' 3 | import { Page } from 'components' 4 | import ReChartsComponent from './ReChartsComponent' 5 | import styles from './index.less' 6 | 7 | const RadioGroup = Radio.Group 8 | 9 | const chartList = [ 10 | { 11 | label: 'lineChart', 12 | value: 'lineChart', 13 | }, 14 | { 15 | label: 'barChart', 16 | value: 'barChart', 17 | }, 18 | { 19 | label: 'areaChart', 20 | value: 'areaChart', 21 | }, 22 | ] 23 | 24 | class Chart extends React.Component { 25 | constructor() { 26 | super() 27 | this.state = { 28 | type: '', 29 | } 30 | this.handleRadioGroupChange = this.handleRadioGroupChange.bind(this) 31 | } 32 | handleRadioGroupChange(e) { 33 | this.setState({ 34 | type: e.target.value, 35 | }) 36 | } 37 | render() { 38 | return ( 39 | <Page inner> 40 | <RadioGroup 41 | options={chartList} 42 | defaultValue="lineChart" 43 | onChange={this.handleRadioGroupChange} 44 | /> 45 | <div className={styles.chart}> 46 | <ReChartsComponent type={this.state.type} /> 47 | </div> 48 | </Page> 49 | ) 50 | } 51 | } 52 | 53 | export default Chart 54 | -------------------------------------------------------------------------------- /src/pages/chart/Recharts/index.less: -------------------------------------------------------------------------------- 1 | .chart { 2 | :global { 3 | .ant-card { 4 | overflow: hidden; 5 | margin-bottom: 24px; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/HighChartsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import HighstockComponent from './HighstockComponent' 5 | import HighmapsComponent from './HighmapsComponent' 6 | import HighMoreComponent from './HighMoreComponent' 7 | 8 | const HighChartsComponent = ({ type }) => { 9 | if (type === 'Highmaps') return <HighmapsComponent /> 10 | if (type === 'HighMore') return <HighMoreComponent /> 11 | return <HighstockComponent /> 12 | } 13 | 14 | HighChartsComponent.propTypes = { 15 | type: PropTypes.string, 16 | } 17 | 18 | export default HighChartsComponent 19 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/HighMoreComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactHighcharts from 'react-highcharts' 3 | import HighchartsExporting from 'highcharts-exporting' 4 | import HighchartsMore from 'highcharts-more' 5 | 6 | HighchartsMore(ReactHighcharts.Highcharts) 7 | HighchartsExporting(ReactHighcharts.Highcharts) 8 | 9 | const config = { 10 | chart: { 11 | polar: true, 12 | }, 13 | xAxis: { 14 | categories: [ 15 | 'Jan', 16 | 'Feb', 17 | 'Mar', 18 | 'Apr', 19 | 'May', 20 | 'Jun', 21 | 'Jul', 22 | 'Aug', 23 | 'Sep', 24 | 'Oct', 25 | 'Nov', 26 | 'Dec', 27 | ], 28 | }, 29 | series: [ 30 | { 31 | data: [ 32 | 29.9, 33 | 71.5, 34 | 106.4, 35 | 129.2, 36 | 144.0, 37 | 176.0, 38 | 135.6, 39 | 148.5, 40 | 216.4, 41 | 194.1, 42 | 95.6, 43 | 54.4, 44 | ], 45 | }, 46 | ], 47 | } 48 | 49 | const HighMoreComponent = () => { 50 | return <ReactHighcharts config={config} /> 51 | } 52 | 53 | export default HighMoreComponent 54 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/HighmapsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactHighmaps from 'react-highcharts/ReactHighmaps.src' 3 | import maps from './mapdata/europe' 4 | 5 | const config = { 6 | chart: { 7 | spacingBottom: 20, 8 | }, 9 | title: { 10 | text: 'Europe time zones', 11 | }, 12 | 13 | legend: { 14 | enabled: true, 15 | }, 16 | 17 | plotOptions: { 18 | map: { 19 | allAreas: false, 20 | joinBy: ['iso-a2', 'code'], 21 | dataLabels: { 22 | enabled: true, 23 | color: 'white', 24 | style: { 25 | fontWeight: 'bold', 26 | }, 27 | }, 28 | mapData: maps, 29 | tooltip: { 30 | headerFormat: '', 31 | pointFormat: '{point.name}: <b>{series.name}</b>', 32 | }, 33 | }, 34 | }, 35 | 36 | series: [ 37 | { 38 | name: 'UTC', 39 | data: ['IE', 'IS', 'GB', 'PT'].map(code => { 40 | return { code } 41 | }), 42 | }, 43 | { 44 | name: 'UTC + 1', 45 | data: [ 46 | 'NO', 47 | 'SE', 48 | 'DK', 49 | 'DE', 50 | 'NL', 51 | 'BE', 52 | 'LU', 53 | 'ES', 54 | 'FR', 55 | 'PL', 56 | 'CZ', 57 | 'AT', 58 | 'CH', 59 | 'LI', 60 | 'SK', 61 | 'HU', 62 | 'SI', 63 | 'IT', 64 | 'SM', 65 | 'HR', 66 | 'BA', 67 | 'YF', 68 | 'ME', 69 | 'AL', 70 | 'MK', 71 | ].map(code => { 72 | return { code } 73 | }), 74 | }, 75 | ], 76 | } 77 | 78 | const HighmapsComponent = () => { 79 | return <ReactHighmaps config={config} /> 80 | } 81 | export default HighmapsComponent 82 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/HighstockComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactHighstock from 'react-highcharts/ReactHighstock.src' 3 | 4 | const data = [ 5 | [1220832000000, 22.56], 6 | [1220918400000, 21.67], 7 | [1221004800000, 21.66], 8 | [1221091200000, 21.81], 9 | [1221177600000, 21.28], 10 | [1221436800000, 20.05], 11 | [1221523200000, 19.98], 12 | [1221609600000, 18.26], 13 | [1221696000000, 19.16], 14 | [1221782400000, 20.13], 15 | [1222041600000, 18.72], 16 | [1222128000000, 18.12], 17 | [1222214400000, 18.39], 18 | [1222300800000, 18.85], 19 | [1222387200000, 18.32], 20 | [1222646400000, 15.04], 21 | [1222732800000, 16.24], 22 | [1222819200000, 15.59], 23 | [1222905600000, 14.3], 24 | [1222992000000, 13.87], 25 | [1223251200000, 14.02], 26 | [1223337600000, 12.74], 27 | [1223424000000, 12.83], 28 | [1223510400000, 12.68], 29 | [1223596800000, 13.8], 30 | [1223856000000, 15.75], 31 | [1223942400000, 14.87], 32 | [1224028800000, 13.99], 33 | [1224115200000, 14.56], 34 | [1224201600000, 13.91], 35 | [1224460800000, 14.06], 36 | [1224547200000, 13.07], 37 | [1224633600000, 13.84], 38 | [1224720000000, 14.03], 39 | [1224806400000, 13.77], 40 | [1225065600000, 13.16], 41 | [1225152000000, 14.27], 42 | [1225238400000, 14.94], 43 | [1225324800000, 15.86], 44 | [1225411200000, 15.37], 45 | [1225670400000, 15.28], 46 | [1225756800000, 15.86], 47 | [1225843200000, 14.76], 48 | [1225929600000, 14.16], 49 | [1226016000000, 14.03], 50 | [1226275200000, 13.7], 51 | [1226361600000, 13.54], 52 | [1226448000000, 12.87], 53 | [1226534400000, 13.78], 54 | [1226620800000, 12.89], 55 | [1226880000000, 12.59], 56 | [1226966400000, 12.84], 57 | [1227052800000, 12.33], 58 | [1227139200000, 11.5], 59 | [1227225600000, 11.8], 60 | [1227484800000, 13.28], 61 | [1227571200000, 12.97], 62 | [1227657600000, 13.57], 63 | [1227830400000, 13.24], 64 | [1228089600000, 12.7], 65 | [1228176000000, 13.21], 66 | [1228262400000, 13.7], 67 | [1228348800000, 13.06], 68 | [1228435200000, 13.43], 69 | [1228694400000, 14.25], 70 | [1228780800000, 14.29], 71 | [1228867200000, 14.03], 72 | [1228953600000, 13.57], 73 | [1229040000000, 14.04], 74 | [1229299200000, 13.54], 75 | ] 76 | 77 | const config = { 78 | rangeSelector: { 79 | selected: 1, 80 | }, 81 | title: { 82 | text: 'AAPL Stock Price', 83 | }, 84 | series: [ 85 | { 86 | name: 'AAPL', 87 | data, 88 | tooltip: { 89 | valueDecimals: 2, 90 | }, 91 | }, 92 | ], 93 | } 94 | 95 | const HighstockComponent = () => { 96 | return <ReactHighstock config={config} /> 97 | } 98 | 99 | export default HighstockComponent 100 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Radio } from 'antd' 3 | import { Page } from 'components' 4 | import HighChartsComponent from './HighChartsComponent' 5 | import styles from './index.less' 6 | 7 | const RadioGroup = Radio.Group 8 | 9 | const chartList = [ 10 | { 11 | label: 'Highstock', 12 | value: 'Highstock', 13 | }, 14 | { 15 | label: 'Highmaps', 16 | value: 'Highmaps', 17 | }, 18 | { 19 | label: 'HighMore', 20 | value: 'HighMore', 21 | }, 22 | ] 23 | 24 | class Chart extends React.Component { 25 | constructor() { 26 | super() 27 | this.state = { 28 | type: '', 29 | } 30 | this.handleRadioGroupChange = this.handleRadioGroupChange.bind(this) 31 | } 32 | handleRadioGroupChange(e) { 33 | this.setState({ 34 | type: e.target.value, 35 | }) 36 | } 37 | render() { 38 | return ( 39 | <Page inner> 40 | <RadioGroup 41 | options={chartList} 42 | defaultValue="Highstock" 43 | onChange={this.handleRadioGroupChange} 44 | /> 45 | <div className={styles.chart}> 46 | <HighChartsComponent type={this.state.type} /> 47 | </div> 48 | </Page> 49 | ) 50 | } 51 | } 52 | 53 | export default Chart 54 | -------------------------------------------------------------------------------- /src/pages/chart/highCharts/index.less: -------------------------------------------------------------------------------- 1 | .chart { 2 | :global { 3 | .ant-radio-wrapper { 4 | margin-bottom: 16px; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Table, Tag } from 'antd' 4 | import { Color } from 'utils' 5 | import styles from './browser.less' 6 | 7 | const status = { 8 | 1: { 9 | color: Color.green, 10 | }, 11 | 2: { 12 | color: Color.red, 13 | }, 14 | 3: { 15 | color: Color.blue, 16 | }, 17 | 4: { 18 | color: Color.yellow, 19 | }, 20 | } 21 | 22 | function Browser({ data }) { 23 | const columns = [ 24 | { 25 | title: 'name', 26 | dataIndex: 'name', 27 | className: styles.name, 28 | }, 29 | { 30 | title: 'percent', 31 | dataIndex: 'percent', 32 | className: styles.percent, 33 | render: (text, it) => <Tag color={status[it.status].color}>{text}%</Tag>, 34 | }, 35 | ] 36 | return ( 37 | <Table 38 | pagination={false} 39 | showHeader={false} 40 | columns={columns} 41 | rowKey='name' 42 | dataSource={data} 43 | /> 44 | ) 45 | } 46 | 47 | Browser.propTypes = { 48 | data: PropTypes.array, 49 | } 50 | 51 | export default Browser 52 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/browser.less: -------------------------------------------------------------------------------- 1 | .percent { 2 | text-align: right !important; 3 | } 4 | 5 | .name { 6 | text-align: left !important; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/comments.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Table, Tag } from 'antd' 4 | import { Color } from 'utils' 5 | import styles from './comments.less' 6 | 7 | const status = { 8 | 1: { 9 | color: Color.green, 10 | text: 'APPROVED', 11 | }, 12 | 2: { 13 | color: Color.yellow, 14 | text: 'PENDING', 15 | }, 16 | 3: { 17 | color: Color.red, 18 | text: 'REJECTED', 19 | }, 20 | } 21 | 22 | function Comments({ data }) { 23 | const columns = [ 24 | { 25 | title: 'avatar', 26 | dataIndex: 'avatar', 27 | width: 48, 28 | className: styles.avatarcolumn, 29 | render: text => ( 30 | <span 31 | style={{ backgroundImage: `url(${text})` }} 32 | className={styles.avatar} 33 | /> 34 | ), 35 | }, 36 | { 37 | title: 'content', 38 | dataIndex: 'content', 39 | render: (text, it) => ( 40 | <div> 41 | <h5 className={styles.name}>{it.name}</h5> 42 | <p className={styles.content}>{it.content}</p> 43 | <div className={styles.daterow}> 44 | <Tag color={status[it.status].color}>{status[it.status].text}</Tag> 45 | <span className={styles.date}>{it.date}</span> 46 | </div> 47 | </div> 48 | ), 49 | }, 50 | ] 51 | return ( 52 | <div className={styles.comments}> 53 | <Table 54 | pagination={false} 55 | showHeader={false} 56 | columns={columns} 57 | rowKey='avatar' 58 | dataSource={data.filter((item, key) => key < 3)} 59 | /> 60 | </div> 61 | ) 62 | } 63 | 64 | Comments.propTypes = { 65 | data: PropTypes.array, 66 | } 67 | 68 | export default Comments 69 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/comments.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .comments { 4 | :global .ant-table-thead > tr > th { 5 | background: #fff; 6 | border-bottom: solid 1px @border-color-base; 7 | } 8 | 9 | .avatar { 10 | width: 48px; 11 | height: 48px; 12 | background-position: center; 13 | background-size: cover; 14 | border-radius: 50%; 15 | background: #f8f8f8; 16 | display: inline-block; 17 | } 18 | 19 | .content { 20 | text-align: left; 21 | color: #757575; 22 | } 23 | 24 | .date { 25 | color: #a3a3a3; 26 | line-height: 30px; 27 | } 28 | 29 | .daterow { 30 | display: flex; 31 | justify-content: space-between; 32 | } 33 | 34 | .name { 35 | font-size: 14px; 36 | color: #474747; 37 | text-align: left; 38 | } 39 | 40 | .avatarcolumn { 41 | vertical-align: top; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/completed.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .sales { 4 | .title { 5 | margin-left: 32px; 6 | font-size: 16px; 7 | } 8 | } 9 | 10 | .radiusdot { 11 | width: 12px; 12 | height: 12px; 13 | margin-right: 8px; 14 | border-radius: 50%; 15 | display: inline-block; 16 | } 17 | 18 | .legend { 19 | text-align: right; 20 | color: #999; 21 | font-size: 14px; 22 | 23 | li { 24 | height: 48px; 25 | line-height: 48px; 26 | display: inline-block; 27 | 28 | & + li { 29 | margin-left: 24px; 30 | } 31 | } 32 | } 33 | 34 | .tooltip { 35 | background: #fff; 36 | padding: 20px; 37 | font-size: 14px; 38 | 39 | .tiptitle { 40 | font-weight: 700; 41 | font-size: 16px; 42 | margin-bottom: 8px; 43 | } 44 | 45 | .tipitem { 46 | height: 32px; 47 | line-height: 32px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/cpu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Color } from 'utils' 4 | import CountUp from 'react-countup' 5 | import { 6 | LineChart, 7 | Line, 8 | XAxis, 9 | YAxis, 10 | CartesianGrid, 11 | ResponsiveContainer, 12 | } from 'recharts' 13 | import styles from './cpu.less' 14 | 15 | const countUpProps = { 16 | start: 0, 17 | duration: 2.75, 18 | useEasing: true, 19 | useGrouping: true, 20 | separator: ',', 21 | } 22 | 23 | function Cpu({ usage = 0, space = 0, cpu = 0, data }) { 24 | return ( 25 | <div className={styles.cpu}> 26 | <div className={styles.number}> 27 | <div className={styles.item}> 28 | <p>usage</p> 29 | <p> 30 | <CountUp end={usage} suffix="GB" {...countUpProps} /> 31 | </p> 32 | </div> 33 | <div className={styles.item}> 34 | <p>space</p> 35 | <p> 36 | <CountUp end={space} suffix="GB" {...countUpProps} /> 37 | </p> 38 | </div> 39 | <div className={styles.item}> 40 | <p>cpu</p> 41 | <p> 42 | <CountUp end={cpu} suffix="%" {...countUpProps} /> 43 | </p> 44 | </div> 45 | </div> 46 | <ResponsiveContainer minHeight={300}> 47 | <LineChart data={data} margin={{ left: -40 }}> 48 | <XAxis 49 | dataKey="name" 50 | axisLine={{ stroke: Color.borderBase, strokeWidth: 1 }} 51 | tickLine={false} 52 | /> 53 | <YAxis axisLine={false} tickLine={false} /> 54 | <CartesianGrid 55 | vertical={false} 56 | stroke={Color.borderBase} 57 | strokeDasharray="3 3" 58 | /> 59 | <Line 60 | type="monotone" 61 | connectNulls 62 | dataKey="cpu" 63 | stroke={Color.blue} 64 | fill={Color.blue} 65 | /> 66 | </LineChart> 67 | </ResponsiveContainer> 68 | </div> 69 | ) 70 | } 71 | 72 | Cpu.propTypes = { 73 | data: PropTypes.array, 74 | usage: PropTypes.number, 75 | space: PropTypes.number, 76 | cpu: PropTypes.number, 77 | } 78 | 79 | export default Cpu 80 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/cpu.less: -------------------------------------------------------------------------------- 1 | .cpu { 2 | .number { 3 | display: flex; 4 | height: 64px; 5 | justify-content: space-between; 6 | margin-bottom: 32px; 7 | 8 | .item { 9 | text-align: center; 10 | height: 64px; 11 | width: 100%; 12 | position: relative; 13 | 14 | & + .item { 15 | &::before { 16 | content: ''; 17 | display: block; 18 | width: 1px; 19 | height: 40px; 20 | position: absolute; 21 | background: #f5f5f5; 22 | top: 12px; 23 | } 24 | } 25 | 26 | p { 27 | color: #757575; 28 | 29 | &:first-child { 30 | font-size: 16px; 31 | } 32 | 33 | &:last-child { 34 | font-size: 20px; 35 | font-weight: 700; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/index.js: -------------------------------------------------------------------------------- 1 | import NumberCard from './numberCard' 2 | import Quote from './quote' 3 | import Sales from './sales' 4 | import Weather from './weather' 5 | import RecentSales from './recentSales' 6 | import Comments from './comments' 7 | import Completed from './completed' 8 | import Browser from './browser' 9 | import Cpu from './cpu' 10 | import User from './user' 11 | 12 | export { 13 | NumberCard, 14 | Quote, 15 | Sales, 16 | Weather, 17 | RecentSales, 18 | Comments, 19 | Completed, 20 | Browser, 21 | Cpu, 22 | User, 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/numberCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Card } from 'antd' 4 | import CountUp from 'react-countup' 5 | import iconMap from 'utils/iconMap' 6 | import styles from './numberCard.less' 7 | 8 | 9 | function NumberCard({ icon, color, title, number, countUp }) { 10 | return ( 11 | <Card 12 | className={styles.numberCard} 13 | bordered={false} 14 | bodyStyle={{ padding: 10 }} 15 | > 16 | <span className={styles.iconWarp} style={{ color }}> 17 | {iconMap[icon]} 18 | </span> 19 | <div className={styles.content}> 20 | <p className={styles.title}>{title || 'No Title'}</p> 21 | <p className={styles.number}> 22 | <CountUp 23 | start={0} 24 | end={number} 25 | duration={2.75} 26 | useEasing 27 | useGrouping 28 | separator="," 29 | {...(countUp || {})} 30 | /> 31 | </p> 32 | </div> 33 | </Card> 34 | ) 35 | } 36 | 37 | NumberCard.propTypes = { 38 | icon: PropTypes.string, 39 | color: PropTypes.string, 40 | title: PropTypes.string, 41 | number: PropTypes.number, 42 | countUp: PropTypes.object, 43 | } 44 | 45 | export default NumberCard 46 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/numberCard.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .numberCard { 4 | padding: 32px; 5 | margin-bottom: 24px; 6 | cursor: pointer; 7 | 8 | .iconWarp { 9 | font-size: 54px; 10 | float: left; 11 | } 12 | 13 | .content { 14 | width: 100%; 15 | padding-left: 78px; 16 | 17 | .title { 18 | line-height: 16px; 19 | font-size: 16px; 20 | margin-bottom: 8px; 21 | height: 16px; 22 | .text-overflow(); 23 | } 24 | 25 | .number { 26 | line-height: 32px; 27 | font-size: 24px; 28 | height: 32px; 29 | .text-overflow(); 30 | margin-bottom: 0; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/quote.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styles from './quote.less' 4 | 5 | function Quote({ name, content, title, avatar }) { 6 | return ( 7 | <div className={styles.quote}> 8 | <div className={styles.inner}>{content}</div> 9 | <div className={styles.footer}> 10 | <div className={styles.description}> 11 | <p>-{name}-</p> 12 | <p>{title}</p> 13 | </div> 14 | <div 15 | className={styles.avatar} 16 | style={{ backgroundImage: `url(${avatar})` }} 17 | /> 18 | </div> 19 | </div> 20 | ) 21 | } 22 | 23 | Quote.propTypes = { 24 | name: PropTypes.string, 25 | content: PropTypes.string, 26 | title: PropTypes.string, 27 | avatar: PropTypes.string, 28 | } 29 | 30 | export default Quote 31 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/quote.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .quote { 4 | color: #fff; 5 | height: 100%; 6 | width: 100%; 7 | padding: 24px; 8 | font-size: 16px; 9 | font-weight: 700; 10 | 11 | .inner { 12 | text-overflow: ellipsis; 13 | word-wrap: normal; 14 | display: -webkit-box; 15 | -webkit-box-orient: vertical; 16 | -webkit-line-clamp: 4; 17 | overflow: hidden; 18 | text-indent: 24px; 19 | } 20 | 21 | .footer { 22 | position: relative; 23 | margin-top: 14px; 24 | 25 | .description { 26 | width: 100%; 27 | 28 | p { 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | margin-right: 64px; 33 | text-align: right; 34 | 35 | &:last-child { 36 | font-weight: 100; 37 | } 38 | } 39 | } 40 | 41 | .avatar { 42 | width: 48px; 43 | height: 48px; 44 | background-position: center; 45 | background-size: cover; 46 | border-radius: 50%; 47 | position: absolute; 48 | right: 0; 49 | top: 0; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/recentSales.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import dayjs from 'dayjs' 3 | import PropTypes from 'prop-types' 4 | import { Table, Tag } from 'antd' 5 | import { Color } from 'utils' 6 | import styles from './recentSales.less' 7 | 8 | const status = { 9 | 1: { 10 | color: Color.green, 11 | text: 'SALE', 12 | }, 13 | 2: { 14 | color: Color.yellow, 15 | text: 'REJECT', 16 | }, 17 | 3: { 18 | color: Color.red, 19 | text: 'TAX', 20 | }, 21 | 4: { 22 | color: Color.blue, 23 | text: 'EXTENDED', 24 | }, 25 | } 26 | 27 | function RecentSales({ data }) { 28 | const columns = [ 29 | { 30 | title: 'NAME', 31 | dataIndex: 'name', 32 | }, 33 | { 34 | title: 'STATUS', 35 | dataIndex: 'status', 36 | render: text => <Tag color={status[text].color}>{status[text].text}</Tag>, 37 | }, 38 | { 39 | title: 'DATE', 40 | dataIndex: 'date', 41 | render: text => dayjs(text).format('YYYY-MM-DD'), 42 | }, 43 | { 44 | title: 'PRICE', 45 | dataIndex: 'price', 46 | render: (text, it) => ( 47 | <span style={{ color: status[it.status].color }}>${text}</span> 48 | ), 49 | }, 50 | ] 51 | return ( 52 | <div className={styles.recentsales}> 53 | <Table 54 | pagination={false} 55 | columns={columns} 56 | rowKey='id' 57 | dataSource={data.filter((item, key) => key < 5)} 58 | /> 59 | </div> 60 | ) 61 | } 62 | 63 | RecentSales.propTypes = { 64 | data: PropTypes.array, 65 | } 66 | 67 | export default RecentSales 68 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/recentSales.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .recentsales { 4 | :global .ant-table-thead > tr > th { 5 | background: #fff; 6 | border-bottom: solid 1px @border-color-base; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/sales.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .sales { 4 | overflow: hidden; 5 | .title { 6 | margin-left: 32px; 7 | font-size: 16px; 8 | } 9 | } 10 | 11 | .radiusdot { 12 | width: 12px; 13 | height: 12px; 14 | margin-right: 8px; 15 | border-radius: 50%; 16 | display: inline-block; 17 | } 18 | 19 | .legend { 20 | text-align: right; 21 | color: #999; 22 | font-size: 14px; 23 | 24 | li { 25 | height: 48px; 26 | line-height: 48px; 27 | display: inline-block; 28 | 29 | & + li { 30 | margin-left: 24px; 31 | } 32 | } 33 | } 34 | 35 | .tooltip { 36 | background: #fff; 37 | padding: 20px; 38 | font-size: 14px; 39 | 40 | .tiptitle { 41 | font-weight: 700; 42 | font-size: 16px; 43 | margin-bottom: 8px; 44 | } 45 | 46 | .tipitem { 47 | height: 32px; 48 | line-height: 32px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/user-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/src/pages/dashboard/components/user-background.png -------------------------------------------------------------------------------- /src/pages/dashboard/components/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button, Avatar } from 'antd' 4 | import CountUp from 'react-countup' 5 | import { Color } from 'utils' 6 | import styles from './user.less' 7 | 8 | const countUpProps = { 9 | start: 0, 10 | duration: 2.75, 11 | useEasing: true, 12 | useGrouping: true, 13 | separator: ',', 14 | } 15 | 16 | function User({ avatar, username, sales = 0, sold = 0 }) { 17 | return ( 18 | <div className={styles.user}> 19 | <div className={styles.header}> 20 | <div className={styles.headerinner}> 21 | <Avatar size="large" src={avatar} /> 22 | <h5 className={styles.name}>{username}</h5> 23 | </div> 24 | </div> 25 | <div className={styles.number}> 26 | <div className={styles.item}> 27 | <p>EARNING SALES</p> 28 | <p style={{ color: Color.green }}> 29 | <CountUp end={sales} prefix="quot; {...countUpProps} /> 30 | </p> 31 | </div> 32 | <div className={styles.item}> 33 | <p>ITEM SOLD</p> 34 | <p style={{ color: Color.blue }}> 35 | <CountUp end={sold} {...countUpProps} /> 36 | </p> 37 | </div> 38 | </div> 39 | <div className={styles.footer}> 40 | <Button type="ghost" size="large"> 41 | View Profile 42 | </Button> 43 | </div> 44 | </div> 45 | ) 46 | } 47 | 48 | User.propTypes = { 49 | avatar: PropTypes.string, 50 | username: PropTypes.string, 51 | sales: PropTypes.number, 52 | sold: PropTypes.number, 53 | } 54 | 55 | export default User 56 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/user.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .user { 4 | .header { 5 | display: flex; 6 | justify-content: center; 7 | text-align: center; 8 | color: #fff; 9 | height: 200px; 10 | background-size: cover; 11 | align-items: center; 12 | 13 | .headerinner { 14 | z-index: 2; 15 | } 16 | 17 | &::after { 18 | content: ''; 19 | background-image: url('./user-background.png'); 20 | background-size: cover; 21 | position: absolute; 22 | width: 100%; 23 | height: 200px; 24 | left: 0; 25 | top: 0; 26 | opacity: 0.4; 27 | z-index: 1; 28 | } 29 | 30 | .name { 31 | font-size: 16px; 32 | margin-top: 8px; 33 | } 34 | } 35 | 36 | .number { 37 | display: flex; 38 | height: 116px; 39 | justify-content: space-between; 40 | border-bottom: solid 1px #f5f5f5; 41 | 42 | .item { 43 | text-align: center; 44 | height: 116px; 45 | width: 100%; 46 | position: relative; 47 | padding: 30px 0; 48 | 49 | & + .item { 50 | &::before { 51 | content: ''; 52 | display: block; 53 | width: 1px; 54 | height: 116px; 55 | position: absolute; 56 | background: #f5f5f5; 57 | top: 0; 58 | } 59 | } 60 | 61 | p { 62 | color: #757575; 63 | 64 | &:first-child { 65 | font-size: 16px; 66 | } 67 | 68 | &:last-child { 69 | font-size: 20px; 70 | font-weight: 700; 71 | } 72 | } 73 | } 74 | } 75 | 76 | .footer { 77 | height: 116px; 78 | display: flex; 79 | justify-content: center; 80 | align-items: center; 81 | 82 | :global .ant-btn { 83 | color: @purple; 84 | border-color: @purple; 85 | padding: 6px 16px; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/weather.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Spin } from 'antd' 4 | import styles from './weather.less' 5 | 6 | function Weather({ city, icon, dateTime, temperature, name, loading }) { 7 | return ( 8 | <Spin spinning={loading}> 9 | <div className={styles.weather}> 10 | <div className={styles.left}> 11 | <div 12 | className={styles.icon} 13 | style={{ 14 | backgroundImage: `url(${icon})`, 15 | }} 16 | /> 17 | <p>{name}</p> 18 | </div> 19 | <div className={styles.right}> 20 | <h1 className={styles.temperature}>{`${temperature}°`}</h1> 21 | <p className={styles.description}> 22 | {city},{dateTime} 23 | </p> 24 | </div> 25 | </div> 26 | </Spin> 27 | ) 28 | } 29 | 30 | Weather.propTypes = { 31 | city: PropTypes.string, 32 | icon: PropTypes.string, 33 | dateTime: PropTypes.string, 34 | temperature: PropTypes.string, 35 | name: PropTypes.string, 36 | loading: PropTypes.bool, 37 | } 38 | 39 | export default Weather 40 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/weather.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .weather { 4 | color: #fff; 5 | height: 204px; 6 | padding: 24px; 7 | justify-content: space-between; 8 | display: flex; 9 | font-size: 14px; 10 | 11 | .left { 12 | display: flex; 13 | flex-direction: column; 14 | width: 64px; 15 | padding-top: 55px; 16 | 17 | .icon { 18 | width: 64px; 19 | height: 64px; 20 | background-position: center; 21 | background-size: contain; 22 | } 23 | 24 | p { 25 | margin-top: 16px; 26 | } 27 | } 28 | 29 | .right { 30 | display: flex; 31 | flex-direction: column; 32 | width: 50%; 33 | 34 | .temperature { 35 | font-size: 36px; 36 | text-align: right; 37 | height: 64px; 38 | color: #fff; 39 | } 40 | 41 | .description { 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | text-align: right; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/dashboard/index.less: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | position: relative; 3 | :global { 4 | .ant-card { 5 | border-radius: 0; 6 | margin-bottom: 24px; 7 | &:hover { 8 | box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05); 9 | } 10 | } 11 | .ant-card-body { 12 | overflow-x: hidden; 13 | } 14 | } 15 | 16 | .weather { 17 | &:hover { 18 | box-shadow: 4px 4px 40px rgba(143, 201, 251, 0.6); 19 | } 20 | } 21 | 22 | .quote { 23 | &:hover { 24 | box-shadow: 4px 4px 40px rgba(246, 152, 153, 0.6); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/dashboard/model.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'qs' 2 | import modelExtend from 'dva-model-extend' 3 | import api from 'api' 4 | const { pathToRegexp } = require("path-to-regexp") 5 | import { model } from 'utils/model' 6 | 7 | const { queryDashboard, queryWeather } = api 8 | const avatar = '//cdn.antd-admin.zuiidea.com/bc442cf0cc6f7940dcc567e465048d1a8d634493198c4-sPx5BR_fw236.jpeg' 9 | 10 | export default modelExtend(model, { 11 | namespace: 'dashboard', 12 | state: { 13 | weather: { 14 | city: '深圳', 15 | temperature: '30', 16 | name: '晴', 17 | icon: '//cdn.antd-admin.zuiidea.com/sun.png', 18 | }, 19 | sales: [], 20 | quote: { 21 | avatar, 22 | }, 23 | numbers: [], 24 | recentSales: [], 25 | comments: [], 26 | completed: [], 27 | browser: [], 28 | cpu: {}, 29 | user: { 30 | avatar, 31 | }, 32 | }, 33 | subscriptions: { 34 | setup({ dispatch, history }) { 35 | history.listen(({ pathname }) => { 36 | if ( 37 | pathToRegexp('/dashboard').exec(pathname) || 38 | pathToRegexp('/').exec(pathname) 39 | ) { 40 | dispatch({ type: 'query' }) 41 | dispatch({ type: 'queryWeather' }) 42 | } 43 | }) 44 | }, 45 | }, 46 | effects: { 47 | *query({ payload }, { call, put }) { 48 | const data = yield call(queryDashboard, parse(payload)) 49 | yield put({ 50 | type: 'updateState', 51 | payload: data, 52 | }) 53 | }, 54 | *queryWeather({ payload = {} }, { call, put }) { 55 | payload.location = 'shenzhen' 56 | const result = yield call(queryWeather, payload) 57 | const { success } = result 58 | if (success) { 59 | const data = result.results[0] 60 | const weather = { 61 | city: data.location.name, 62 | temperature: data.now.temperature, 63 | name: data.now.text, 64 | icon: `//cdn.antd-admin.zuiidea.com/web/icons/3d_50/${data.now.code}.png`, 65 | } 66 | yield put({ 67 | type: 'updateState', 68 | payload: { 69 | weather, 70 | }, 71 | }) 72 | } 73 | }, 74 | }, 75 | }) 76 | -------------------------------------------------------------------------------- /src/pages/dashboard/services/dashboard.js: -------------------------------------------------------------------------------- 1 | import { request, config } from 'utils' 2 | 3 | const { api } = config 4 | const { dashboard } = api 5 | 6 | export function query(params) { 7 | return request({ 8 | url: dashboard, 9 | method: 'get', 10 | data: params, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/dashboard/services/weather.js: -------------------------------------------------------------------------------- 1 | import { request, config } from 'utils' 2 | 3 | const { APIV1 } = config 4 | 5 | export function query(params) { 6 | params.key = 'i7sau1babuzwhycn' 7 | return request({ 8 | url: `${APIV1}/weather/now.json`, 9 | method: 'get', 10 | data: params, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/editor/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import { Editor, Page } from 'components' 3 | import { convertToRaw } from 'draft-js' 4 | import { Row, Col, Card } from 'antd' 5 | import draftToHtml from 'draftjs-to-html' 6 | import draftToMarkdown from 'draftjs-to-markdown' 7 | 8 | export default class EditorPage extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | editorContent: null, 13 | } 14 | } 15 | 16 | onEditorStateChange = editorContent => { 17 | this.setState({ 18 | editorContent, 19 | }) 20 | } 21 | 22 | render() { 23 | const { editorContent } = this.state 24 | const colProps = { 25 | lg: 12, 26 | md: 24, 27 | style: { 28 | marginBottom: 32, 29 | } 30 | } 31 | const textareaStyle = { 32 | minHeight: 496, 33 | width: '100%', 34 | background: '#f7f7f7', 35 | borderColor: '#F1F1F1', 36 | padding: '16px 8px' 37 | } 38 | 39 | return ( 40 | <Page inner> 41 | <Row gutter={32}> 42 | <Col {...colProps}> 43 | <Card title="Editor" style={{ overflow: 'visible' }}> 44 | <Editor 45 | wrapperStyle={{ 46 | minHeight: 500, 47 | }} 48 | editorStyle={{ 49 | minHeight: 376, 50 | }} 51 | editorState={editorContent} 52 | onEditorStateChange={this.onEditorStateChange} 53 | /> 54 | </Card> 55 | </Col> 56 | <Col {...colProps}> 57 | <Card title="HTML"> 58 | <textarea 59 | style={textareaStyle} 60 | disabled 61 | value={ 62 | editorContent 63 | ? draftToHtml( 64 | convertToRaw(editorContent.getCurrentContent()) 65 | ) 66 | : '' 67 | } 68 | /> 69 | </Card> 70 | </Col> 71 | <Col {...colProps}> 72 | <Card title="Markdown"> 73 | <textarea 74 | style={textareaStyle} 75 | disabled 76 | value={ 77 | editorContent 78 | ? draftToMarkdown( 79 | convertToRaw(editorContent.getCurrentContent()) 80 | ) 81 | : '' 82 | } 83 | /> 84 | </Card> 85 | </Col> 86 | <Col {...colProps}> 87 | <Card title="JSON"> 88 | <textarea 89 | style={textareaStyle} 90 | disabled 91 | value={ 92 | editorContent 93 | ? JSON.stringify( 94 | convertToRaw(editorContent.getCurrentContent()) 95 | ) 96 | : '' 97 | } 98 | /> 99 | </Card> 100 | </Col> 101 | </Row> 102 | </Page> 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { Redirect } from 'umi' 3 | import { t } from "@lingui/macro" 4 | 5 | class Index extends PureComponent { 6 | render() { 7 | return <Redirect to={t`/dashboard`} /> 8 | } 9 | } 10 | 11 | export default Index 12 | -------------------------------------------------------------------------------- /src/pages/login/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'umi' 4 | import { Button, Row, Input, Form } from 'antd' 5 | import { GlobalFooter } from 'components' 6 | import { GithubOutlined } from '@ant-design/icons' 7 | import { t, Trans } from "@lingui/macro" 8 | import { setLocale } from 'utils' 9 | import config from 'utils/config' 10 | 11 | import styles from './index.less' 12 | 13 | const FormItem = Form.Item 14 | 15 | @connect(({ loading, dispatch }) => ({ loading, dispatch })) 16 | class Login extends PureComponent { 17 | 18 | render() { 19 | const { dispatch, loading } = this.props 20 | 21 | const handleOk = values => { 22 | dispatch({ type: 'login/login', payload: values }) 23 | } 24 | let footerLinks = [ 25 | { 26 | key: 'github', 27 | title: <GithubOutlined />, 28 | href: 'https://github.com/zuiidea/antd-admin', 29 | blankTarget: true, 30 | }, 31 | ] 32 | 33 | if (config.i18n) { 34 | footerLinks = footerLinks.concat( 35 | config.i18n.languages.map(item => ({ 36 | key: item.key, 37 | title: ( 38 | <span onClick={setLocale.bind(null, item.key)}>{item.title}</span> 39 | ), 40 | })) 41 | ) 42 | } 43 | 44 | return ( 45 | <Fragment> 46 | <div className={styles.form}> 47 | <div className={styles.logo}> 48 | <img alt="logo" src={config.logoPath} /> 49 | <span>{config.siteName}</span> 50 | </div> 51 | <Form 52 | onFinish={handleOk} 53 | > 54 | <FormItem name="username" 55 | rules={[{ required: true }]} hasFeedback> 56 | <Input 57 | placeholder={t`Username`} 58 | /> 59 | </FormItem> 60 | <Trans id="Password" render={({translation}) => ( 61 | <FormItem name="password" rules={[{ required: true }]} hasFeedback> 62 | <Input type='password' placeholder={translation} required /> 63 | </FormItem>)} 64 | /> 65 | <Row> 66 | <Button 67 | type="primary" 68 | htmlType="submit" 69 | loading={loading.effects.login} 70 | > 71 | <Trans>Sign in</Trans> 72 | </Button> 73 | <p> 74 | <span className="margin-right"> 75 | <Trans>Username</Trans> 76 | :guest 77 | </span> 78 | <span> 79 | <Trans>Password</Trans> 80 | :guest 81 | </span> 82 | </p> 83 | </Row> 84 | </Form> 85 | </div> 86 | <div className={styles.footer}> 87 | <GlobalFooter links={footerLinks} copyright={config.copyright} /> 88 | </div> 89 | </Fragment> 90 | ) 91 | } 92 | } 93 | 94 | Login.propTypes = { 95 | form: PropTypes.object, 96 | dispatch: PropTypes.func, 97 | loading: PropTypes.object, 98 | } 99 | 100 | export default Login 101 | -------------------------------------------------------------------------------- /src/pages/login/index.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .form { 4 | position: absolute; 5 | top: 45%; 6 | left: 50%; 7 | margin: -160px 0 0 -160px; 8 | width: 320px; 9 | height: 320px; 10 | padding: 36px; 11 | box-shadow: 0 0 100px rgba(0, 0, 0, 0.08); 12 | 13 | button { 14 | width: 100%; 15 | } 16 | 17 | p { 18 | color: rgb(204, 204, 204); 19 | text-align: center; 20 | margin-top: 16px; 21 | font-size: 12px; 22 | display: flex; 23 | justify-content: space-between; 24 | } 25 | } 26 | 27 | .logo { 28 | text-align: center; 29 | cursor: pointer; 30 | margin-bottom: 24px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | 35 | img { 36 | width: 40px; 37 | margin-right: 8px; 38 | } 39 | 40 | span { 41 | vertical-align: text-bottom; 42 | font-size: 16px; 43 | text-transform: uppercase; 44 | display: inline-block; 45 | font-weight: 700; 46 | color: @primary-color; 47 | .text-gradient(); 48 | } 49 | } 50 | 51 | .ant-spin-container, 52 | .ant-spin-nested-loading { 53 | height: 100%; 54 | } 55 | 56 | .footer { 57 | position: absolute; 58 | width: 100%; 59 | bottom: 0; 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/login/model.js: -------------------------------------------------------------------------------- 1 | import { history } from 'umi' 2 | const { pathToRegexp } = require("path-to-regexp") 3 | import api from 'api' 4 | 5 | const { loginUser } = api 6 | 7 | export default { 8 | namespace: 'login', 9 | 10 | state: {}, 11 | // subscriptions: { 12 | // setup({ dispatch, history }) { 13 | // history.listen(location => { 14 | // if (pathToRegexp('/login').exec(location.pathname)) { 15 | // } 16 | // }) 17 | // }, 18 | // }, 19 | effects: { 20 | *login({ payload }, { put, call, select }) { 21 | const data = yield call(loginUser, payload) 22 | const { locationQuery } = yield select(_ => _.app) 23 | if (data.success) { 24 | const { from } = locationQuery 25 | yield put({ type: 'app/query' }) 26 | if (!pathToRegexp('/login').exec(from)) { 27 | if (['', '/'].includes(from)) history.push('/dashboard') 28 | else history.push(from) 29 | } else { 30 | history.push('/dashboard') 31 | } 32 | } else { 33 | throw data 34 | } 35 | }, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/post/components/List.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { Table, Avatar } from 'antd' 3 | import { t } from "@lingui/macro" 4 | import { Ellipsis } from 'components' 5 | import styles from './List.less' 6 | 7 | class List extends PureComponent { 8 | render() { 9 | const { ...tableProps } = this.props 10 | const columns = [ 11 | { 12 | title: t`Image`, 13 | dataIndex: 'image', 14 | render: text => <Avatar shape="square" src={text} />, 15 | }, 16 | { 17 | title: t`Title`, 18 | dataIndex: 'title', 19 | render: text => ( 20 | <Ellipsis tooltip length={30}> 21 | {text} 22 | </Ellipsis> 23 | ), 24 | }, 25 | { 26 | title: t`Author`, 27 | dataIndex: 'author', 28 | }, 29 | { 30 | title: t`Categories`, 31 | dataIndex: 'categories', 32 | }, 33 | { 34 | title: t`Tags`, 35 | dataIndex: 'tags', 36 | }, 37 | { 38 | title: t`Visibility`, 39 | dataIndex: 'visibility', 40 | }, 41 | { 42 | title: t`Comments`, 43 | dataIndex: 'comments', 44 | }, 45 | { 46 | title: t`Views`, 47 | dataIndex: 'views', 48 | }, 49 | { 50 | title: t`Publish Date`, 51 | dataIndex: 'date', 52 | }, 53 | ] 54 | 55 | return ( 56 | <Table 57 | {...tableProps} 58 | pagination={{ 59 | ...tableProps.pagination, 60 | showTotal: total => t`Total ${total} Items`, 61 | }} 62 | bordered 63 | scroll={{ x: 1200 }} 64 | className={styles.table} 65 | columns={columns} 66 | simple 67 | rowKey={record => record.id} 68 | /> 69 | ) 70 | } 71 | } 72 | 73 | export default List 74 | -------------------------------------------------------------------------------- /src/pages/post/components/List.less: -------------------------------------------------------------------------------- 1 | .table { 2 | :global { 3 | .ant-table td { 4 | white-space: nowrap; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/post/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'umi' 4 | import { Tabs } from 'antd' 5 | import { history } from 'umi' 6 | import { stringify } from 'qs' 7 | import { t } from "@lingui/macro" 8 | import { Page } from 'components' 9 | import List from './components/List' 10 | 11 | const { TabPane } = Tabs 12 | 13 | const EnumPostStatus = { 14 | UNPUBLISH: 1, 15 | PUBLISHED: 2, 16 | } 17 | 18 | @connect(({ post, loading }) => ({ post, loading })) 19 | class Post extends PureComponent { 20 | handleTabClick = key => { 21 | const { pathname } = this.props.location 22 | 23 | history.push({ 24 | pathname, 25 | search: stringify({ 26 | status: key, 27 | }), 28 | }) 29 | } 30 | 31 | get listProps() { 32 | const { post, loading, location } = this.props 33 | const { list, pagination } = post 34 | const { query, pathname } = location 35 | 36 | return { 37 | pagination, 38 | dataSource: list, 39 | loading: loading.effects['post/query'], 40 | onChange(page) { 41 | history.push({ 42 | pathname, 43 | search: stringify({ 44 | ...query, 45 | page: page.current, 46 | pageSize: page.pageSize, 47 | }), 48 | }) 49 | }, 50 | } 51 | } 52 | 53 | render() { 54 | const { location } = this.props 55 | const { query } = location 56 | 57 | return ( 58 | <Page inner> 59 | <Tabs 60 | activeKey={ 61 | query.status === String(EnumPostStatus.UNPUBLISH) 62 | ? String(EnumPostStatus.UNPUBLISH) 63 | : String(EnumPostStatus.PUBLISHED) 64 | } 65 | onTabClick={this.handleTabClick} 66 | > 67 | <TabPane 68 | tab={t`Publised`} 69 | key={String(EnumPostStatus.PUBLISHED)} 70 | > 71 | <List {...this.listProps} /> 72 | </TabPane> 73 | <TabPane 74 | tab={t`Unpublished`} 75 | key={String(EnumPostStatus.UNPUBLISH)} 76 | > 77 | <List {...this.listProps} /> 78 | </TabPane> 79 | </Tabs> 80 | </Page> 81 | ) 82 | } 83 | } 84 | 85 | Post.propTypes = { 86 | post: PropTypes.object, 87 | loading: PropTypes.object, 88 | location: PropTypes.object, 89 | dispatch: PropTypes.func, 90 | } 91 | 92 | export default Post 93 | -------------------------------------------------------------------------------- /src/pages/post/model.js: -------------------------------------------------------------------------------- 1 | import modelExtend from 'dva-model-extend' 2 | import api from 'api' 3 | const { pathToRegexp } = require("path-to-regexp") 4 | import { pageModel } from 'utils/model' 5 | 6 | const { queryPostList } = api 7 | 8 | export default modelExtend(pageModel, { 9 | namespace: 'post', 10 | 11 | subscriptions: { 12 | setup({ dispatch, history }) { 13 | history.listen(location => { 14 | if (pathToRegexp('/post').exec(location.pathname)) { 15 | dispatch({ 16 | type: 'query', 17 | payload: { 18 | status: 2, 19 | ...location.query, 20 | }, 21 | }) 22 | } 23 | }) 24 | }, 25 | }, 26 | 27 | effects: { 28 | *query({ payload }, { call, put }) { 29 | const data = yield call(queryPostList, payload) 30 | if (data.success) { 31 | yield put({ 32 | type: 'querySuccess', 33 | payload: { 34 | list: data.data, 35 | pagination: { 36 | current: Number(payload.page) || 1, 37 | pageSize: Number(payload.pageSize) || 10, 38 | total: data.total, 39 | }, 40 | }, 41 | }) 42 | } else { 43 | throw data 44 | } 45 | }, 46 | }, 47 | }) 48 | -------------------------------------------------------------------------------- /src/pages/request/index.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars'; 2 | 3 | .result { 4 | height: 600px; 5 | width: 100%; 6 | background: @hover-color; 7 | border-color: #ddd; 8 | padding: 16px; 9 | margin-top: 16px; 10 | word-break: break-word; 11 | line-height: 2; 12 | overflow: scroll; 13 | } 14 | 15 | .requestList { 16 | padding-right: 24px; 17 | margin-bottom: 24px; 18 | .listItem { 19 | cursor: pointer; 20 | padding-left: 8px; 21 | &.lstItemActive { 22 | background-color: @hover-color; 23 | } 24 | .background-hover(); 25 | } 26 | } 27 | 28 | .paramsBlock { 29 | overflow: visible; 30 | opacity: 1; 31 | height: auto; 32 | transition: opacity 0.3s; 33 | &.hideParams { 34 | width: 0; 35 | height: 0; 36 | opacity: 0; 37 | overflow: hidden; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/user/[id]/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'umi' 4 | import { Page } from 'components' 5 | import styles from './index.less' 6 | 7 | @connect(({ userDetail }) => ({ userDetail })) 8 | class UserDetail extends PureComponent { 9 | render() { 10 | const { userDetail } = this.props 11 | const { data } = userDetail 12 | const content = [] 13 | for (let key in data) { 14 | if ({}.hasOwnProperty.call(data, key)) { 15 | content.push( 16 | <div key={key} className={styles.item}> 17 | <div>{key}</div> 18 | <div>{String(data[key])}</div> 19 | </div> 20 | ) 21 | } 22 | } 23 | return ( 24 | <Page inner> 25 | <div className={styles.content}>{content}</div> 26 | </Page> 27 | ) 28 | } 29 | } 30 | 31 | UserDetail.propTypes = { 32 | userDetail: PropTypes.object, 33 | } 34 | 35 | export default UserDetail 36 | -------------------------------------------------------------------------------- /src/pages/user/[id]/index.less: -------------------------------------------------------------------------------- 1 | .content { 2 | line-height: 2.4; 3 | font-size: 13px; 4 | 5 | .item { 6 | display: flex; 7 | 8 | & > div { 9 | &:first-child { 10 | width: 100px; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/user/[id]/models/detail.js: -------------------------------------------------------------------------------- 1 | const { pathToRegexp } = require("path-to-regexp") 2 | import api from 'api' 3 | 4 | const { queryUser } = api 5 | 6 | export default { 7 | namespace: 'userDetail', 8 | 9 | state: { 10 | data: {}, 11 | }, 12 | 13 | subscriptions: { 14 | setup({ dispatch, history }) { 15 | history.listen(({ pathname }) => { 16 | const match = pathToRegexp('/user/:id').exec(pathname) 17 | if (match) { 18 | dispatch({ type: 'query', payload: { id: match[1] } }) 19 | } 20 | }) 21 | }, 22 | }, 23 | 24 | effects: { 25 | *query({ payload }, { call, put }) { 26 | const data = yield call(queryUser, payload) 27 | const { success, message, status, ...other } = data 28 | if (success) { 29 | yield put({ 30 | type: 'querySuccess', 31 | payload: { 32 | data: other, 33 | }, 34 | }) 35 | } else { 36 | throw data 37 | } 38 | }, 39 | }, 40 | 41 | reducers: { 42 | querySuccess(state, { payload }) { 43 | const { data } = payload 44 | return { 45 | ...state, 46 | data, 47 | } 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/user/components/List.less: -------------------------------------------------------------------------------- 1 | .table { 2 | :global { 3 | .ant-table td { 4 | white-space: nowrap; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/user/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Form, Input, InputNumber, Radio, Modal, Cascader } from 'antd' 4 | import { Trans } from "@lingui/macro" 5 | import city from 'utils/city' 6 | import { t } from "@lingui/macro" 7 | 8 | const FormItem = Form.Item 9 | 10 | const formItemLayout = { 11 | labelCol: { 12 | span: 6, 13 | }, 14 | wrapperCol: { 15 | span: 14, 16 | }, 17 | } 18 | 19 | class UserModal extends PureComponent { 20 | formRef = React.createRef() 21 | 22 | handleOk = () => { 23 | const { item = {}, onOk } = this.props 24 | 25 | this.formRef.current.validateFields() 26 | .then(values => { 27 | const data = { 28 | ...values, 29 | key: item.key, 30 | } 31 | data.address = data.address.join(' ') 32 | onOk(data) 33 | }) 34 | .catch(errorInfo => { 35 | console.log(errorInfo) 36 | }) 37 | } 38 | 39 | render() { 40 | const { item = {}, onOk, form, ...modalProps } = this.props 41 | 42 | return ( 43 | (<Modal {...modalProps} onOk={this.handleOk}> 44 | <Form ref={this.formRef} name="control-ref" initialValues={{ ...item, address: item.address && item.address.split(' ') }} layout="horizontal"> 45 | <FormItem name='name' rules={[{ required: true }]} 46 | label={t`Name`} hasFeedback {...formItemLayout}> 47 | <Input /> 48 | </FormItem> 49 | <FormItem name='nickName' rules={[{ required: true }]} 50 | label={t`NickName`} hasFeedback {...formItemLayout}> 51 | <Input /> 52 | </FormItem> 53 | <FormItem name='isMale' rules={[{ required: true }]} 54 | label={t`Gender`} hasFeedback {...formItemLayout}> 55 | <Radio.Group> 56 | <Radio value> 57 | <Trans>Male</Trans> 58 | </Radio> 59 | <Radio value={false}> 60 | <Trans>Female</Trans> 61 | </Radio> 62 | </Radio.Group> 63 | </FormItem> 64 | <FormItem name='age' label={t`Age`} hasFeedback {...formItemLayout}> 65 | <InputNumber min={18} max={100} /> 66 | </FormItem> 67 | <FormItem name='phone' rules={[{ required: true, pattern: /^1[34578]\d{9}$/, message: t`The input is not valid phone!`, }]} 68 | label={t`Phone`} hasFeedback {...formItemLayout}> 69 | <Input /> 70 | </FormItem> 71 | <FormItem name='email' rules={[{ required: true, pattern: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/, message: t`The input is not valid E-mail!`, }]} 72 | label={t`Email`} hasFeedback {...formItemLayout}> 73 | <Input /> 74 | </FormItem> 75 | <FormItem name='address' rules={[{ required: true, }]} 76 | label={t`Address`} hasFeedback {...formItemLayout}> 77 | <Cascader 78 | style={{ width: '100%' }} 79 | options={city} 80 | placeholder={t`Pick an address`} 81 | /> 82 | </FormItem> 83 | </Form> 84 | </Modal>) 85 | ); 86 | } 87 | } 88 | 89 | UserModal.propTypes = { 90 | type: PropTypes.string, 91 | item: PropTypes.object, 92 | onOk: PropTypes.func, 93 | } 94 | 95 | export default UserModal 96 | -------------------------------------------------------------------------------- /src/pages/user/model.js: -------------------------------------------------------------------------------- 1 | import modelExtend from 'dva-model-extend' 2 | const { pathToRegexp } = require("path-to-regexp") 3 | import api from 'api' 4 | import { pageModel } from 'utils/model' 5 | 6 | const { 7 | queryUserList, 8 | createUser, 9 | removeUser, 10 | updateUser, 11 | removeUserList, 12 | } = api 13 | 14 | export default modelExtend(pageModel, { 15 | namespace: 'user', 16 | 17 | state: { 18 | currentItem: {}, 19 | modalOpen: false, 20 | modalType: 'create', 21 | selectedRowKeys: [], 22 | }, 23 | 24 | subscriptions: { 25 | setup({ dispatch, history }) { 26 | history.listen(location => { 27 | if (pathToRegexp('/user').exec(location.pathname)) { 28 | const payload = location.query || { page: 1, pageSize: 10 } 29 | dispatch({ 30 | type: 'query', 31 | payload, 32 | }) 33 | } 34 | }) 35 | }, 36 | }, 37 | 38 | effects: { 39 | *query({ payload = {} }, { call, put }) { 40 | const data = yield call(queryUserList, payload) 41 | if (data) { 42 | yield put({ 43 | type: 'querySuccess', 44 | payload: { 45 | list: data.data, 46 | pagination: { 47 | current: Number(payload.page) || 1, 48 | pageSize: Number(payload.pageSize) || 10, 49 | total: data.total, 50 | }, 51 | }, 52 | }) 53 | } 54 | }, 55 | 56 | *delete({ payload }, { call, put, select }) { 57 | const data = yield call(removeUser, { id: payload }) 58 | const { selectedRowKeys } = yield select(_ => _.user) 59 | if (data.success) { 60 | yield put({ 61 | type: 'updateState', 62 | payload: { 63 | selectedRowKeys: selectedRowKeys.filter(_ => _ !== payload), 64 | }, 65 | }) 66 | } else { 67 | throw data 68 | } 69 | }, 70 | 71 | *multiDelete({ payload }, { call, put }) { 72 | const data = yield call(removeUserList, payload) 73 | if (data.success) { 74 | yield put({ type: 'updateState', payload: { selectedRowKeys: [] } }) 75 | } else { 76 | throw data 77 | } 78 | }, 79 | 80 | *create({ payload }, { call, put }) { 81 | const data = yield call(createUser, payload) 82 | if (data.success) { 83 | yield put({ type: 'hideModal' }) 84 | } else { 85 | throw data 86 | } 87 | }, 88 | 89 | *update({ payload }, { select, call, put }) { 90 | const id = yield select(({ user }) => user.currentItem.id) 91 | const newUser = { ...payload, id } 92 | const data = yield call(updateUser, newUser) 93 | if (data.success) { 94 | yield put({ type: 'hideModal' }) 95 | } else { 96 | throw data 97 | } 98 | }, 99 | }, 100 | 101 | reducers: { 102 | showModal(state, { payload }) { 103 | return { ...state, ...payload, modalOpen: true } 104 | }, 105 | 106 | hideModal(state) { 107 | return { ...state, modalOpen: false } 108 | }, 109 | }, 110 | }) 111 | -------------------------------------------------------------------------------- /src/plugins/onError.js: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | 3 | export default { 4 | onError(e, a) { 5 | e.preventDefault() 6 | if (e.message) { 7 | message.error(e.message) 8 | } else { 9 | /* eslint-disable */ 10 | console.error(e) 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | export default { 2 | queryRouteList: '/routes', 3 | 4 | queryUserInfo: '/user', 5 | logoutUser: '/user/logout', 6 | loginUser: 'POST /user/login', 7 | 8 | queryUser: '/user/:id', 9 | queryUserList: '/users', 10 | updateUser: 'Patch /user/:id', 11 | createUser: 'POST /user', 12 | removeUser: 'DELETE /user/:id', 13 | removeUserList: 'POST /users/delete', 14 | 15 | queryPostList: '/posts', 16 | 17 | queryDashboard: '/dashboard', 18 | } 19 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | import request from 'utils/request' 2 | import { apiPrefix } from 'utils/config' 3 | 4 | import api from './api' 5 | 6 | const gen = params => { 7 | let url = apiPrefix + params 8 | let method = 'GET' 9 | 10 | const paramsArray = params.split(' ') 11 | if (paramsArray.length === 2) { 12 | method = paramsArray[0] 13 | url = apiPrefix + paramsArray[1] 14 | } 15 | 16 | return function(data) { 17 | return request({ 18 | url, 19 | data, 20 | method, 21 | }) 22 | } 23 | } 24 | 25 | const APIFunction = {} 26 | for (const key in api) { 27 | APIFunction[key] = gen(api[key]) 28 | } 29 | 30 | APIFunction.queryWeather = params => { 31 | params.key = 'i7sau1babuzwhycn' 32 | return request({ 33 | url: `${apiPrefix}/weather/now.json`, 34 | data: params, 35 | }) 36 | } 37 | 38 | export default APIFunction 39 | -------------------------------------------------------------------------------- /src/themes/default.less: -------------------------------------------------------------------------------- 1 | // 本文件是对 ant-design: 2 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less 3 | // 相应变量值的覆盖 4 | // 注意:只需写出要覆盖的变量即可(不需要覆盖的变量不要写) 5 | 6 | /* @import '../../node_modules/antd/lib/style/themes/default.less';*/ 7 | 8 | @border-radius-base: 3px; 9 | @border-radius-sm: 2px; 10 | @shadow-color: rgba(0, 0, 0, 0.05); 11 | @shadow-1-down: 4px 4px 40px @shadow-color; 12 | @border-color-split: #f4f4f4; 13 | @border-color-base: #e5e5e5; 14 | @font-size-base: 13px; 15 | @text-color: #666; 16 | @hover-color: #f9f9fc; 17 | -------------------------------------------------------------------------------- /src/themes/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/reset.css'; 2 | @import '~themes/vars.less'; 3 | 4 | body { 5 | height: 100%; 6 | overflow-y: auto; 7 | background-color: #f8f8f8; 8 | } 9 | 10 | ::-webkit-scrollbar-thumb { 11 | background-color: #e6e6e6; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | width: 8px; 16 | height: 8px; 17 | } 18 | 19 | .margin-right { 20 | margin-right: 16px; 21 | } 22 | 23 | :global { 24 | .ant-breadcrumb { 25 | & > span { 26 | &:last-child { 27 | color: #999; 28 | font-weight: normal; 29 | } 30 | } 31 | } 32 | 33 | .ant-breadcrumb-link { 34 | .anticon + span { 35 | margin-left: 4px; 36 | } 37 | } 38 | 39 | .ant-table { 40 | .ant-table-thead > tr > th { 41 | text-align: center; 42 | } 43 | 44 | .ant-table-tbody > tr > td { 45 | text-align: center; 46 | } 47 | 48 | &.ant-table-small { 49 | .ant-table-thead > tr > th { 50 | background: #f7f7f7; 51 | } 52 | 53 | .ant-table-body > table { 54 | padding: 0; 55 | } 56 | } 57 | } 58 | 59 | .ant-table-pagination { 60 | float: none !important; 61 | display: table; 62 | margin: 16px auto !important; 63 | } 64 | 65 | .ant-popover-inner { 66 | border: none; 67 | border-radius: 0; 68 | box-shadow: 0 0 20px rgba(100, 100, 100, 0.2); 69 | } 70 | 71 | .ant-form-item-control { 72 | vertical-align: middle; 73 | } 74 | 75 | .ant-modal-mask { 76 | background-color: rgba(55, 55, 55, 0.2); 77 | } 78 | 79 | .ant-modal-content { 80 | box-shadow: none; 81 | } 82 | 83 | .ant-select-dropdown-menu-item { 84 | padding: 12px 16px !important; 85 | } 86 | 87 | a:focus { 88 | text-decoration: none; 89 | } 90 | 91 | .ant-table-layout-fixed table { 92 | table-layout: auto; 93 | } 94 | } 95 | @media (min-width: 1600px) { 96 | :global { 97 | .ant-col-xl-48 { 98 | width: 20%; 99 | } 100 | 101 | .ant-col-xl-96 { 102 | width: 40%; 103 | } 104 | } 105 | } 106 | @media (max-width: 767px) { 107 | :global { 108 | .ant-pagination-item, 109 | .ant-pagination-next, 110 | .ant-pagination-options, 111 | .ant-pagination-prev { 112 | margin-bottom: 8px; 113 | } 114 | 115 | .ant-card { 116 | .ant-card-head { 117 | padding: 0 12px; 118 | } 119 | 120 | .ant-card-body { 121 | padding: 12px; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/themes/mixin.less: -------------------------------------------------------------------------------- 1 | @import '~themes/default'; 2 | 3 | @dark-half: #494949; 4 | @purple: #d897eb; 5 | @shadow-1: 4px 4px 20px 0 rgba(0, 0, 0, 0.01); 6 | @shadow-2: 4px 4px 40px 0 rgba(0, 0, 0, 0.05); 7 | @transition-ease-in: all 0.3s ease-out; 8 | @transition-ease-out: all 0.3s ease-out; 9 | @ease-in: ease-in; 10 | 11 | .text-overflow { 12 | white-space: nowrap; 13 | text-overflow: ellipsis; 14 | overflow: hidden; 15 | } 16 | 17 | .text-gradient { 18 | background-image: -webkit-gradient( 19 | linear, 20 | 37.219838% 34.532506%, 21 | 36.425669% 93.178216%, 22 | from(#29cdff), 23 | to(#0a60ff), 24 | color-stop(0.37, #148eff) 25 | ); 26 | -webkit-background-clip: text; 27 | -webkit-text-fill-color: transparent; 28 | } 29 | 30 | .background-hover { 31 | transition: @transition-ease-in; 32 | &:hover { 33 | background-color: @hover-color; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/themes/vars.less: -------------------------------------------------------------------------------- 1 | @import '~themes/default.less'; 2 | @import '~themes/mixin.less'; 3 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteName: 'AntD Admin', 3 | copyright: 'Ant Design Admin ©2020 zuiidea', 4 | logoPath: '/logo.svg', 5 | apiPrefix: '/api/v1', 6 | fixedHeader: true, // sticky primary layout header 7 | 8 | /* Layout configuration, specify which layout to use for route. */ 9 | layouts: [ 10 | { 11 | name: 'primary', 12 | include: [/.*/], 13 | exclude: [/(\/(en|zh))*\/login/], 14 | }, 15 | ], 16 | 17 | /* I18n configuration, `languages` and `defaultLanguage` are required currently. */ 18 | i18n: { 19 | /* Countrys flags: https://www.flaticon.com/packs/countrys-flags */ 20 | languages: [ 21 | { 22 | key: 'pt-br', 23 | title: 'Português', 24 | flag: '/portugal.svg', 25 | }, 26 | { 27 | key: 'en', 28 | title: 'English', 29 | flag: '/america.svg', 30 | }, 31 | { 32 | key: 'zh', 33 | title: '中文', 34 | flag: '/china.svg', 35 | }, 36 | ], 37 | defaultLanguage: 'en', 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/constant.js: -------------------------------------------------------------------------------- 1 | export const ROLE_TYPE = { 2 | ADMIN: 'admin', 3 | DEFAULT: 'admin', 4 | DEVELOPER: 'developer', 5 | } 6 | 7 | export const CANCEL_REQUEST_MESSAGE = 'cancel request' 8 | -------------------------------------------------------------------------------- /src/utils/iconMap.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | PayCircleOutlined, 3 | ShoppingCartOutlined, 4 | MessageOutlined, 5 | TeamOutlined, 6 | UserOutlined, 7 | DashboardOutlined, 8 | ApiOutlined, 9 | CameraOutlined, 10 | EditOutlined, 11 | CodeOutlined, 12 | LineOutlined, 13 | BarChartOutlined, 14 | AreaChartOutlined, 15 | } from '@ant-design/icons' 16 | 17 | export default { 18 | 'pay-circle-o': <PayCircleOutlined />, 19 | 'shopping-cart': <ShoppingCartOutlined />, 20 | 'camera-o': <CameraOutlined />, 21 | 'line-chart': <LineOutlined />, 22 | 'code-o': <CodeOutlined />, 23 | 'area-chart': <AreaChartOutlined />, 24 | 'bar-chart': <BarChartOutlined />, 25 | message: <MessageOutlined />, 26 | team: <TeamOutlined />, 27 | dashboard: <DashboardOutlined />, 28 | user: <UserOutlined />, 29 | api: <ApiOutlined />, 30 | edit: <EditOutlined />, 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/index.test.js: -------------------------------------------------------------------------------- 1 | const { pathToRegexp } = require("path-to-regexp") 2 | 3 | describe('test pathToRegexp', () => { 4 | it('get right', () => { 5 | expect(pathToRegexp('/user').exec('/zh/user')).toEqual( 6 | pathToRegexp('/user').exec('/user') 7 | ) 8 | expect(pathToRegexp('/user').exec('/user')).toEqual( 9 | pathToRegexp('/user').exec('/user') 10 | ) 11 | 12 | expect(pathToRegexp('/user/:id').exec('/zh/user/1')).toEqual( 13 | pathToRegexp('/user/:id').exec('/user/1') 14 | ) 15 | expect(pathToRegexp('/user/:id').exec('/user/1')).toEqual( 16 | pathToRegexp('/user/:id').exec('/user/1') 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/utils/model.js: -------------------------------------------------------------------------------- 1 | import modelExtend from 'dva-model-extend' 2 | 3 | export const model = { 4 | reducers: { 5 | updateState(state, { payload }) { 6 | return { 7 | ...state, 8 | ...payload, 9 | } 10 | }, 11 | }, 12 | } 13 | 14 | export const pageModel = modelExtend(model, { 15 | state: { 16 | list: [], 17 | pagination: { 18 | showSizeChanger: true, 19 | showQuickJumper: true, 20 | current: 1, 21 | total: 0, 22 | pageSize: 10, 23 | }, 24 | }, 25 | 26 | reducers: { 27 | querySuccess(state, { payload }) { 28 | const { list, pagination } = payload 29 | return { 30 | ...state, 31 | list, 32 | pagination: { 33 | ...state.pagination, 34 | ...pagination, 35 | }, 36 | } 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { cloneDeep } from 'lodash' 3 | const { parse, compile } = require("path-to-regexp") 4 | import { message } from 'antd' 5 | import { CANCEL_REQUEST_MESSAGE } from 'utils/constant' 6 | 7 | const { CancelToken } = axios 8 | window.cancelRequest = new Map() 9 | 10 | export default function request(options) { 11 | let { data, url } = options 12 | const cloneData = cloneDeep(data) 13 | 14 | try { 15 | let domain = '' 16 | const urlMatch = url.match(/[a-zA-z]+:\/\/[^/]*/) 17 | if (urlMatch) { 18 | ;[domain] = urlMatch 19 | url = url.slice(domain.length) 20 | } 21 | 22 | const match = parse(url) 23 | url = compile(url)(data) 24 | 25 | for (const item of match) { 26 | if (item instanceof Object && item.name in cloneData) { 27 | delete cloneData[item.name] 28 | } 29 | } 30 | url = domain + url 31 | } catch (e) { 32 | message.error(e.message) 33 | } 34 | 35 | options.url = url 36 | options.cancelToken = new CancelToken(cancel => { 37 | window.cancelRequest.set(Symbol(Date.now()), { 38 | pathname: window.location.pathname, 39 | cancel, 40 | }) 41 | }) 42 | 43 | return axios(options) 44 | .then(response => { 45 | const { statusText, status, data } = response 46 | 47 | let result = {} 48 | if (typeof data === 'object') { 49 | result = data 50 | if (Array.isArray(data)) { 51 | result.list = data 52 | } 53 | } else { 54 | result.data = data 55 | } 56 | 57 | return Promise.resolve({ 58 | success: true, 59 | message: statusText, 60 | statusCode: status, 61 | ...result, 62 | }) 63 | }) 64 | .catch(error => { 65 | const { response, message } = error 66 | 67 | if (String(message) === CANCEL_REQUEST_MESSAGE) { 68 | return { 69 | success: false, 70 | } 71 | } 72 | 73 | let msg 74 | let statusCode 75 | 76 | if (response && response instanceof Object) { 77 | const { data, statusText } = response 78 | statusCode = response.status 79 | msg = data.message || statusText 80 | } else { 81 | statusCode = 600 82 | msg = error.message || 'Network Error' 83 | } 84 | 85 | /* eslint-disable */ 86 | return Promise.reject({ 87 | success: false, 88 | statusCode, 89 | message: msg, 90 | }) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Color: { 3 | green: '#64ea91', 4 | blue: '#8fc9fb', 5 | purple: '#d897eb', 6 | red: '#f69899', 7 | yellow: '#f8c82e', 8 | peach: '#f797d6', 9 | borderBase: '#e5e5e5', 10 | borderSplit: '#f4f4f4', 11 | grass: '#d6fbb5', 12 | sky: '#c1e0fc', 13 | }, 14 | } 15 | --------------------------------------------------------------------------------