├── .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 |
--------------------------------------------------------------------------------
/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 `?message`, `` 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 |
2 |
3 |
4 |
5 |
6 | antd-admin - An admin dashboard application demo built upon Ant Design and UmiJS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 | Loading...
20 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/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 |
43 |
Secondary
44 | {children}
45 |
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 Secondary page Content
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 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. 提取代码中需要翻译的字段,即 `message`、`` 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 |
43 |
Secondary
44 | {children}
45 |
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 Secondary page Content
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 |
2 |
3 |
64 |
--------------------------------------------------------------------------------
/public/china.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuiidea/antd-admin/67fc31a00892215e2d9971a91aa300e33ba48321/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
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 | {item.name}
14 | ))
15 | return (
16 | {menu}}
18 | {...dropdownProps}
19 | >
20 |
24 |
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 |
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 {}
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 |
9 | {labelArray.length > 0 && (
10 |
11 | {labelArray.map((item, index) => (
12 |
13 | {item}
14 |
15 | ))}
16 |
17 | )}
18 |
{children}
19 |
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 {}
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 |
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 |
17 | {item.icon && (
18 | {iconMap[item.icon]}
19 | )}
20 | {item.name}
21 |
22 | )
23 |
24 | return (
25 | item && (
26 |
27 | {paths.length - 1 !== key ? (
28 | {content}
29 | ) : (
30 | content
31 | )}
32 |
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 |
58 | {this.generateBreadcrumbs(paths)}
59 |
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 |
45 | {item.icon && iconMap[item.icon]}
46 | {item.name}
47 |
48 | }
49 | >
50 | {this.generateMenus(item.children)}
51 |
52 | )
53 | }
54 | return (
55 |
56 |
57 | {item.icon && iconMap[item.icon]}
58 | {item.name}
59 |
60 |
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 |
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 | {}}
32 | className={styles.sider}
33 | >
34 |
35 |
36 |

37 | {!collapsed &&
{config.siteName}
}
38 |
39 |
40 |
41 |
42 |
48 |
55 |
56 |
57 | {!collapsed && (
58 |
59 |
60 |
61 | Switch Theme
62 |
63 |
72 |
73 | )}
74 |
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 |
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 |
21 | {loading ? : ''}
22 | {children}
23 |
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 |
41 |
42 | {config.siteName}
43 |
44 |
45 | {children}
46 |
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 |
57 |
58 | {children}
59 |
60 |
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 |
8 |
9 |
10 |
404 Not Found
11 |
12 |
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 | ''
79 |
80 | return (
81 |
82 |
83 |
88 |
94 |
95 |
96 | {code}
97 |
98 |
99 |
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}
{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 | ''
71 |
72 | return (
73 |
74 |
75 |
80 |
86 |
87 |
88 | {code}
89 |
90 |
91 |
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
26 | if (type === 'loading') return
27 | if (type === 'api') return
28 | if (type === 'events') return
29 | if (type === 'theme') return
30 | if (type === 'dynamic') return
31 | if (type === 'map') return
32 | if (type === 'airport') return
33 | if (type === 'graph') return
34 | if (type === 'calendar') return
35 | if (type === 'treemap') return
36 | if (type === 'gauge') return
37 | if (type === 'gcalendar') return
38 | if (type === 'lunar') return
39 | if (type === 'liquid') return
40 | if (type === 'BubbleGradientComponent') return
41 | if (type === 'TransparentBar3DComPonent') return
42 | return
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 |
64 |
65 |
66 |
71 |
72 |
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 |
17 |
18 |
19 |
27 |
28 |
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 |
9 |
echarts-for-react {this.props.params.type}
10 |
11 | {' '}
12 | A very simple echarts(v3.0) wrapper for React.{' '}
13 |
14 | hustcc/echarts-for-react
15 |
16 |
17 |
18 |
19 |
20 |
21 | Simple demo |
22 | Echarts loading |
23 | Echarts API |
24 | Echarts events |
25 | Echarts theme |
26 | Dynamic chart |
27 | Map chart
28 |
29 |
30 | New
31 | :
32 | Airport |
33 | Graph |
34 | Calendar |
35 | Treemap |
36 | Gauge |
37 | GCalendar |
38 | Lunar |
39 | Liquidfill
40 |
41 | {this.props.children ||
}
42 |
43 |
49 |
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 | '"
27 | return (
28 |
29 |
30 |
36 |
46 |
47 |
48 | {code}
49 |
50 |
51 |
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 | '"
68 | return (
69 |
70 |
71 |
75 |
81 |
82 |
83 | {code}
84 |
85 |
86 |
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 | '"
104 | return (
105 |
106 |
107 |
113 |
114 |
119 |
120 | {code}
121 |
122 |
123 |
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 |
96 |
101 |
102 |
103 |
104 |
110 |
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 |
13 |
14 |
15 | {children}
16 |
17 |
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
10 | if (type === 'barChart') return
11 | return
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 |
40 |
45 |
46 |
47 |
48 |
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
10 | if (type === 'HighMore') return
11 | return
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
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}: {series.name}',
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
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
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 |
40 |
45 |
46 |
47 |
48 |
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) => {text}%,
34 | },
35 | ]
36 | return (
37 |
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 |
34 | ),
35 | },
36 | {
37 | title: 'content',
38 | dataIndex: 'content',
39 | render: (text, it) => (
40 |
41 |
{it.name}
42 |
{it.content}
43 |
44 | {status[it.status].text}
45 | {it.date}
46 |
47 |
48 | ),
49 | },
50 | ]
51 | return (
52 |
53 |
key < 3)}
59 | />
60 |
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 |
26 |
27 |
28 |
usage
29 |
30 |
31 |
32 |
33 |
34 |
space
35 |
36 |
37 |
38 |
39 |
40 |
cpu
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
59 |
66 |
67 |
68 |
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 |
16 |
17 | {iconMap[icon]}
18 |
19 |
20 |
{title || 'No Title'}
21 |
22 |
31 |
32 |
33 |
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 |
8 |
{content}
9 |
10 |
11 |
-{name}-
12 |
{title}
13 |
14 |
18 |
19 |
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 => {status[text].text},
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 | ${text}
48 | ),
49 | },
50 | ]
51 | return (
52 |
53 |
key < 5)}
58 | />
59 |
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 |
19 |
20 |
21 |
22 |
{username}
23 |
24 |
25 |
26 |
27 |
EARNING SALES
28 |
29 |
30 |
31 |
32 |
33 |
ITEM SOLD
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
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 |
9 |
10 |
19 |
20 |
{`${temperature}°`}
21 |
22 | {city},{dateTime}
23 |
24 |
25 |
26 |
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 |
41 |
42 |
43 |
44 |
54 |
55 |
56 |
57 |
58 |
69 |
70 |
71 |
72 |
73 |
84 |
85 |
86 |
87 |
88 |
99 |
100 |
101 |
102 |
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
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: ,
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 | {item.title}
39 | ),
40 | }))
41 | )
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 |

49 |
{config.siteName}
50 |
51 |
85 |
86 |
87 |
88 |
89 |
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 => ,
15 | },
16 | {
17 | title: t`Title`,
18 | dataIndex: 'title',
19 | render: text => (
20 |
21 | {text}
22 |
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 | 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 |
59 |
67 |
71 |
72 |
73 |
77 |
78 |
79 |
80 |
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 |
17 |
{key}
18 |
{String(data[key])}
19 |
20 | )
21 | }
22 | }
23 | return (
24 |
25 | {content}
26 |
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 | (
44 |
84 | )
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': ,
19 | 'shopping-cart': ,
20 | 'camera-o': ,
21 | 'line-chart': ,
22 | 'code-o': ,
23 | 'area-chart': ,
24 | 'bar-chart': ,
25 | message: ,
26 | team: ,
27 | dashboard: ,
28 | user: ,
29 | api: ,
30 | edit: ,
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 |
--------------------------------------------------------------------------------