ecore by antd

main
fengyuexingzi 4 years ago
parent 72283d1ffd
commit e6429944e7

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

@ -0,0 +1,3 @@
BROWSER=none
HOST=0.0.0.0
PORT=7001

@ -0,0 +1,7 @@
src/**/*-test.js
src/public
src/routes/chart/ECharts/theme
src/routes/chart/highCharts/mapdata
src/locales/_build/
src/locales/**/*.js
docs/**/*.js

@ -0,0 +1,8 @@
{
"extends": "react-app",
"rules": {
"jsx-a11y/href-no-hash": "off",
"no-console": "warn",
"valid-jsdoc": "warn"
}
}

@ -0,0 +1,8 @@
> 请酌情提供细节并保持issue精简便于查阅
> 1. 功能需求 => 详细说明
> 2. Bug反馈
> 2.1 简单描述下报错
> 2.2 你期望什么结果
> 2.3 如何操作导致的
> 2.4 可提供运行环境信息
> 3. 代码求助 => 可以提,未必及时答复

23
.gitignore vendored

@ -0,0 +1,23 @@
coverage
dist
node_modules
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
# ide
.idea
# Mac General
.DS_Store
.AppleDouble
.LSOverride
# umi
.umi
.umi-production
# jslingui
src/locales/_build
src/locales/**/*.js

@ -0,0 +1,6 @@
*.svg
*.ejs
.DS_Store
.umi
.umi-production
src/locales/**/*.json

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}

@ -0,0 +1,9 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
"declaration-empty-line-before": null,
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": null,
"selector-pseudo-element-colon-notation": null
}
}

@ -0,0 +1,18 @@
language: node_js
node_js:
- node
script:
- npm run build
before_install:
- yarn global add now
after_script:
- |
if [[ $TRAVIS_BRANCH == 'master' ]]; then
echo $PROD_SITE_NOW_CONFIG >> dist/vercel.json && echo $PROD_DOC_NOW_CONFIG >> docs/vercel.json;
else
echo $DEV_SITE_NOW_CONFIG >> dist/vercel.json && echo $DEV_DOC_NOW_CONFIG >> docs/vercel.json;
fi
- cd ./dist
- now -A vercel.json -t $NOW_TOKEN --prod -c
- cd ../docs
- now -A vercel.json -t $NOW_TOKEN --prod -c

@ -0,0 +1,135 @@
// https://umijs.org/config/
import { resolve } from 'path'
const fs = require('fs')
const path = require('path')
const lessToJs = require('less-vars-to-js')
const isDevelopment = process.env.NODE_ENV === 'development'
// how to speed compile: https://umijs.org/guide/boost-compile-speed
export default {
// IMPORTANT! change next line to yours or delete. And hide in dev
publicPath: isDevelopment ? '/' : 'http://140.249.182.90:7000/',
alias: {
api: resolve(__dirname, './src/services/'),
components: resolve(__dirname, './src/components'),
config: resolve(__dirname, './src/utils/config'),
themes: resolve(__dirname, './src/themes'),
utils: resolve(__dirname, './src/utils'),
},
antd: {},
// a lower cost way to genereate sourcemap, default is cheap-module-source-map, could save 60% time in dev hotload
devtool: 'eval',
dva: { immer: true },
dynamicImport: {
loading: 'components/Loader/Loader',
},
extraBabelPlugins: [
[
'import',
{
libraryName: 'lodash',
libraryDirectory: '',
camel2DashComponentName: false,
},
'lodash',
],
[
'import',
{
libraryName: '@ant-design/icons',
libraryDirectory: 'es/icons',
camel2DashComponentName: false,
},
'ant-design-icons',
],
[
'macros'
]
],
hash: true,
ignoreMomentLocale: true,
// umi3 comple node_modules by default, could be disable
nodeModulesTransform: {
type: 'none',
exclude: [],
},
// Webpack Configuration
proxy: {
'/api/v1/weather': {
target: 'https://api.seniverse.com/',
changeOrigin: true,
pathRewrite: { '^/api/v1/weather': '/v3/weather' },
},
},
// Theme for antd
// https://ant.design/docs/react/customize-theme
theme: lessToJs(
fs.readFileSync(path.join(__dirname, './src/themes/default.less'), 'utf8')
),
webpack5: {},
mfsu: {},
chainWebpack: function (config, { webpack }) {
!isDevelopment && config.merge({
optimization: {
minimize: false,
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 3,
automaticNameDelimiter: '.',
cacheGroups: {
react: {
name: 'react',
priority: 20,
test: /[\\/]node_modules[\\/](react|react-dom|react-dom-router)[\\/]/,
},
antd: {
name: 'antd',
priority: 20,
test: /[\\/]node_modules[\\/](antd|@ant-design\/icons)[\\/]/,
},
'echarts-gl': {
name: 'echarts-gl',
priority: 30,
test: /[\\/]node_modules[\\/]echarts-gl[\\/]/,
},
zrender: {
name: 'zrender',
priority: 30,
test: /[\\/]node_modules[\\/]zrender[\\/]/,
},
echarts: {
name: 'echarts',
priority: 20,
test: /[\\/]node_modules[\\/](echarts|echarts-for-react|echarts-liquidfill)[\\/]/,
},
highcharts: {
name: 'highcharts',
priority: 20,
test: /[\\/]node_modules[\\/]highcharts[\\/]/,
},
recharts: {
name: 'recharts',
priority: 20,
test: /[\\/]node_modules[\\/]recharts[\\/]/,
},
draftjs: {
name: 'draftjs',
priority: 30,
test: /[\\/]node_modules[\\/](draft-js|react-draft-wysiwyg|draftjs-to-html|draftjs-to-markdown)[\\/]/,
},
async: {
chunks: 'async',
minChunks: 2,
name: 'async',
maxInitialRequests: 1,
minSize: 0,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
})
},
}

@ -0,0 +1,6 @@
{
"json.schemas": [
],
"js/ts.implicitProjectConfig.experimentalDecorators": true
}

@ -0,0 +1,22 @@
MIT LICENSE
Copyright (c) 2016 zuiidea (zuiiidea@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,88 @@
<p align="center">
<a href="http://github.com/zuiidea/antd-admin">
<img alt="antd-admin" height="64" src="./docs/_media/logo.svg">
</a>
</p>
<h1 align="center">AntD Admin</h1>
<div align="center">
一套优秀的中后台前端解决方案
[![antd](https://img.shields.io/badge/antd-^3.10.0-blue.svg?style=flat-square)](https://github.com/ant-design/ant-design)
[![umi](https://img.shields.io/badge/umi-^2.2.1-orange.svg?style=flat-square)](https://github.com/umijs/umi)
[![GitHub issues](https://img.shields.io/github/issues/zuiidea/antd-admin.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/issues)
[![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT)
![Travis (.org)](https://img.shields.io/travis/zuiidea/antd-admin.svg)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/pulls)
[![Gitter](https://img.shields.io/gitter/room/antd-admin/antd-admin.svg)](https://gitter.im/antd-admin/antd-admin)
</div>
- 在线演示 - [https://antd-admin.zuiidea.com](https://antd-admin.zuiidea.com)
- 使用文档 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/](https://doc.antd-admin.zuiidea.com/#/zh-cn/)
- 常见问题 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/faq](https://doc.antd-admin.zuiidea.com/#/zh-cn/faq)
- 更新日志 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/change-log](https://doc.antd-admin.zuiidea.com/#/zh-cn/change-log)
[English](./README.md) | 简体中文
## 特性
- 国际化,源码中抽离翻译字段,按需加载语言包
- 动态权限,不同权限对应不同菜单
- 优雅美观Ant Design 设计体系
- Mock 数据,本地数据调试
## 使用
1. 下载项目代码。
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
cd my-project
```
2. 进入目录安装依赖,国内用户推荐使用 [cnpm](https://cnpmjs.org) 进行加速。
```bash
yarn install
```
或者
```bash
npm install
```
3. 启动本地服务器。
```bash
npm run start
```
4. 启动完成后打开浏览器访问 [http://localhost:7000](http://localhost:7000),如果需要更改启动端口,可在 `.env` 文件中配置。
5. 登录账号有2个一个账号admin 密码admin;另一个账号guest 密码guest
> 更多信息请参考 [使用文档](https://doc.antd-admin.zuiidea.com/#/zh-cn/)。
## 支持环境
现代浏览器。
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --------- | --------- | --------- | --------- | --------- |
|IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley:
- 在你的公司或个人项目中使用 AntD Admin。
- 通过 [Issue](http://github.com/zuiidea/antd-admin/issues) 报告 bug 或进行咨询。
- 提交 [Pull Request](http://github.com/zuiidea/antd-admin/pulls) 改进代码。
> 强烈推荐阅读 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)、[《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545) 和 [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/%7Esgtatham/bugs-cn.html)、[《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393),更好的问题更容易获得帮助。

@ -0,0 +1,35 @@
### 开发环境
- OSWindows
- 编辑器Atom
- cmdCmder
### Atom 插件
- [atom-beautify](https://atom.io/packages/atom-beautify)
#### Less
- [x] Beautify On Save
#### Javascript
- [ ] Disable Beautifying Language
#### Markdown
- [x] Beautify On Save
- [x] Default Beautifier:Remark
#### HTML
- [x] Beautify On Save
- [linter](https://atom.io/packages/linter)
- [ ] Lint on Change
- [linter-eslint](https://atom.io/packages/linter-eslint)
- [x] Fix errors on save
### Cmder 主题
- [Panda-Theme-Cmder](https://github.com/HamidFaraji/Panda-Theme-Cmder)

@ -0,0 +1,86 @@
# API configuration
## Why
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.
```javascript
export async function login(data) {
  return request({
    url: '/api/v1/user/login',
    method: 'post',
    data,
  })
}
```
## Configuration and use
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'`.
```javascript
export default {
  ...
  queryUser: '/user/:id',
  queryUserList: '/users',
  updateUser: 'Patch /user/:id',
  createUser: 'POST /user/:id',
  removeUser: 'DELETE /user/:id',
  removeUserList: 'POST /users/delete',
  ...
}
```
Used in other files
```javascript
import { queryUser } from 'api'
// in the general file
...
queryUser(option).then(data => console.log(data))
...
/ / Model file
...
yield call(queryUser, option)
...
```
## Method to realize
Refer to the `src/services/index.js` file to traverse the api configuration. Each property returns the corresponding encapsulated request function.
```javascript
import request from 'utils/request'
import { apiPrefix } from 'utils/config'
import api from './api'
const gen = params => {
  let url = apiPrefix + params
  let method = 'GET'
  const paramsArray = params.split(' ')
  if (paramsArray.length === 2) {
    method = paramsArray[0]
    url = apiPrefix + paramsArray[1]
  }
  return function(data) {
    return request({
      url,
      data,
      method,
    })
  }
}
const APIFunction = {}
for (const key in api) {
  APIFunction[key] = gen(api[key])
}
module.exports = APIFunction
```

@ -0,0 +1,83 @@
<p align="center">
<a href="http://github.com/zuiidea/antd-admin">
<img alt="antd-admin" height="64" src="./_media/logo.svg">
</a>
</p>
<h1 align="center">AntD Admin</h1>
<div align="center">
An excellent front-end solution for enterprise applications.
[![antd](https://img.shields.io/badge/antd-^3.10.0-blue.svg?style=flat-square)](https://github.com/ant-design/ant-design)
[![umi](https://img.shields.io/badge/umi-^2.2.1-orange.svg?style=flat-square)](https://github.com/umijs/umi)
[![GitHub issues](https://img.shields.io/github/issues/zuiidea/antd-admin.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/issues)
[![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT)
![Travis (.org)](https://img.shields.io/travis/zuiidea/antd-admin.svg)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/pulls)
[![Gitter](https://img.shields.io/gitter/room/antd-admin/antd-admin.svg)](https://gitter.im/antd-admin/antd-admin)
</div>
- Preview - [https://antd-admin.zuiidea.com](https://antd-admin.zuiidea.com)
- Documentation - [https://doc.antd-admin.zuiidea.com](https://doc.antd-admin.zuiidea.com)
- FAQ - [https://doc.antd-admin.zuiidea.com/#/faq](https://doc.antd-admin.zuiidea.com/#/faq)
- ChangeLog - [https://doc.antd-admin.zuiidea.com/#/change-log](https://doc.antd-admin.zuiidea.com/#/change-log)
## Features
- Internationalization, extracting translation fields from source code, loading language packs on demand
- Dynamic permissions, different permissions for different menus
- Elegant and beautiful, Ant Design system
- Mock data, local data debugging
## Usage
1. Clone project code.
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
cd my-project
```
2. Installation dependence.
```bash
yarn install
```
Or
```bash
npm install
```
3. Start local server.
```bash
npm run start
```
4. 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.
> More instructions at [documentation](https://doc.antd-admin.zuiidea.com)。
## Browsers support
Modern browsers.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --------- | --------- | --------- | --------- | --------- |
|IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
## Contributing
We very much welcome your contribution, you can build together with us in the following ways :smiley:
- Use Ant Design Pro in your daily work.
- Submit [GitHub issues](http://github.com/zuiidea/antd-admin/issues)s to report bugs or ask questions.
- Propose [Pull Request](http://github.com/zuiidea/antd-admin/pulls) to improve our code.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

@ -0,0 +1,24 @@
<svg width="169px" height="141px" viewBox="0 0 169 141" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="54.0428975%" y1="4.39752391%" x2="54.0428975%" y2="108.456714%" id="linearGradient-1">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="62.3089445%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="14.2201464%" x2="50%" y2="113.263844%" id="linearGradient-2">
<stop stop-color="#FA816E" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="65.9092442%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(0.000000, -5.000000)">
<rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(83.718923, 75.312358) rotate(-24.000000) translate(-83.718923, -75.312358) " x="68.7189234" y="0.312357954" width="30" height="150" rx="15"></rect>
<rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(129.009910, 75.580213) rotate(-24.000000) translate(-129.009910, -75.580213) " x="114.00991" y="0.580212739" width="30" height="150" rx="15"></rect>
<circle id="Oval" fill="url(#linearGradient-2)" cx="25" cy="120" r="25"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 194 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 199 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

@ -0,0 +1,12 @@
- Getting started
- [Quick Start](getting-started.md)
- Customization
- [Configuration](configuration.md)
- [API Configuration](API-configuration.md)
- [I18n](i18n.md)
- [Layout](layout.md)
- [Request](request.md)
- Guide
- [Deploy](deploy.md)
- [Change Log](change-log.md)
- [FAQ](faq.md)

@ -0,0 +1,50 @@
## 5.0.0
#### Optimization
- Try to use decorators to simplify code writing and improve code readability.
- API configurization to simplify the way data is obtained.
- The files in `utils` are split and each has its own role.
- Simplify the `utils/request` file without special handling.
#### Specification
- 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).
  
- Semantic version number, specification participation [semantic version 2.0.0](https://semver.org/lang/zh-CN/).
- Static code checking, unified code style, will use `prettier`, `stylelint`, `eslint` specification code before code submission.
- Git submits information normalization, [git-commit-emoji-cn](https://github.com/liuchengxu/git-commit-emoji-cn).
- Based on the pre-defined routing of `Umi`, there is no need to write a routing configuration file.
- Use `React 16` new features such as `Fragment`, `Context`, `PureComponent`, etc.
#### Features
- Support internationalization, extract source fields from source code, load language packs on demand, and automatically translate online.
- Support for the introduction `lodash` functions on demand.
  
- Support multiple layouts, which rules can be used according to the rules.
- Support Antd Admin to automatically compile and deploy on Travis.
- Generate a documentation website using `Docsify`.
#### Style
- Added Antd Admin standalone Logo.
- Rewrite the overall layout component, optimize the menu, automatic breadcrumb navigation, menu auto-expansion and other logic.
- The mobile menu is changed to drawer.
#### Other
- Discard components such as `IconFont`, `Search`, `DataTable` because they are well supported and replaceable in `Antd`.

@ -0,0 +1,102 @@
# Configuration
You can do some custom configuration in `/src/utils/config.js`:
## siteName
- Type `String`
Configure the site name, apply it to the login box, and display the title text at the top of the sidebar.
## copyright
- Type `String`
Configure the copyright notice to apply to the login page, at the bottom of the `Primary` layout.
## logoPath
- Type `String`
Configure the site logo to apply to the login box and the Logo display at the top of the sidebar.
## apiPrefix
- Type `String`
Configure the prefix of the interface in the project. The interface related documents can be viewed [API configuration](API-configuration.md)
## fixedHeader
- Type `String`
Under the `Primary` layout, whether the top of the page is fixed when scrolling。
## layouts
- Type `Array`
Configuration? Which routes use which layout, unspecified route uses the default layout `Public`, the project currently has `Primary` and `Public` layouts,
     The default configuration is as follows
```javascript
layouts: [
{
name: 'primary',
include: [/.*/],
exclude: [/(\/(en|zh))*\/login/],
},
],
```
The object properties for each layout are as follows:
- `name` - The name of the layout;
- `include` - Specifies a list of routing rules that use this layout, which can be a regular expression or a string;
- `exclude` - Specifies a list of routing rules that do not use this layout, which can be a regular expression or a string.
> 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).
## i18n
- Type `Object`
Configure internationalization, the default configuration is as follows:
```javascript
i18n: {
languages: [
{
key: 'en',
title: 'English',
flag: '/america.svg',
},
{
key: 'zh',
title: '中文',
flag: '/china.svg',
},
],
defaultLanguage: 'en',
}
```
### i18n.languages
- Type `Array`
Specify which languages the app supports, and the object properties for each language are as follows:
- `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;
- `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;
- `flag` - The path of the flag icon of the language, the language switching display at the top of the `Primary` layout.
### i18n.defaultLanguage
- Type `String`
Configure the default language.

@ -0,0 +1,113 @@
# Deploy
After the development is completed and verified in the development environment, it needs to be deployed to our users.
![i18n](./_media/term_build.svg)
## Build
First execute the following command,
```bash
npm run build
```
After a few seconds, the output should look like this
```bash
> antd-admin@5.0.0-beta build /Users/zuiidea/web/antd-admin
> umi build
[21:13:17] webpack compiled in 43s 868ms
DONE Compiled successfully in 43877ms 21:13:17
File sizes after gzip:
1.3 MB dist/vendors.async.js
308.21 KB dist/umi.js
45.49 KB dist/vendors.chunk.css
36.08 KB dist/p__chart__highCharts__index.async.js
33.53 KB dist/p__user__index.async.js
22.36 KB dist/p__chart__ECharts__index.async.js
4.21 KB dist/p__dashboard__index.async.js
4.06 KB dist/umi.css
...
```
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.
> 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).
## Local verification
Local verification can be done via `serve` before publishing.
```
$ yarn global add serve
$ serve ./dist
Serving!
- Local: http://localhost:5000
- On Your Network: http://{Your IP}:5000
Copied local address to clipboard!
```
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).
## Deploy
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`:
```
server
{
listen 80;
# Specify an accessible domain name
server_name antd-admin.zuiidea.com;
# The directory where the compiled files are stored
root /home/www/antd-admin/dist;
# Proxy server interface to avoid cross-domain
location /api {
proxy_pass http://localhost:7000/api;
}
Because the front end uses BrowserHistory, it will route backback to index.html
location / {
index index.html;
try_files $uri $uri/ /index.html;
}
}
```
Restart the web server and access [http://antd-admin.zuiidea.com](http://antd-admin.zuiidea.com) , You will see the correct page.
```bash
nginx -s reload
```
Similarly, if you use Caddy as a web server, you can do this in `Caddyfile`:
```
antd-admin.zuiidea.com {
gzip
root /home/www/antd-admin/dist
proxy /api http://localhost:7000
rewrite {
if {path} not_match ^/api
to {path} {path}/ /
}
}
antd-admin.zuiidea.com/public {
gzip
root /home/www/antd-admin/dist/static/public
}
```

@ -0,0 +1,9 @@
# FAQ
Most asked
## create new page
1. just copy a page in /src/pages (route here auto generated by [umi](https://umijs.org/guide/router.html#basic-routing))
2. modify namespace/pathToRegexp in model.js
3. modify mock route.js to add a route

@ -0,0 +1,73 @@
# Quick Start
> 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.
## Installation
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
cd my-project
```
## Scaffolding
The project layout is as follows:
```bash
├── dist/ # Default build output directory
├── mock/ # Mock files
├── public/ # Static resource
├── src/ # Source code
│ ├── components/ # Components
│ ├── e2e/ # Integrated Test Case
│ ├── layouts/ # Common Layouts
│ ├── locales/ # i18n resources
│ ├── models/ # Global dva Model
│ ├── pages/ # Sub-pages and templates
│ ├── services/ # Backend Services
│ │ ├── api.js # API configuration
│ │ └── index.js # API export
│ ├── themes/ # Themes
│ │ ├── default.less # Less variable
│ │ ├── index.less # Global style
│ │ ├── mixin.less # Less mixin
│ │ └── vars.less # Less variable and mixin
│ ├── utils/ # Utility
│ │ ├── config.js # Application configuration
│ │ ├── constant.js # Static constant
│ │ ├── index.js # Utility methods
│ │ ├── request.js # Request function(axios)
│ │ └── theme.js # Style variables used in js
├── .editorconfig
├── .env
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .stylelintrc.json
├── .travis.yml
└── .umirc.js
└── package.json
```
## Development
1. Install Dependencies.
```bash
yarn install
```
Or
```bash
npm install
```
2. Start local server.
```bash
npm run start
```
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.

@ -0,0 +1,75 @@
# globalization
## Add language
Take Japanese as an example.
![i18n](../_media/term_i18n.svg)
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.
```bash 
npm run add-locale ja
```
2. Extract the fields in the code that need to be translated, ie `<Trans>?message</Trans>`, `` t`message `` in the `message` field, run the following command after `src/locales/ja /messages.json` will appear after the extracted field configuration.
```bash 
npm run extract
```
You will see the following information:
```bash
Catalog statistics:
┌─────────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├─────────────┼─────────────┼─────────┤
│ en (source) │ 52 │ - │
│ ja │ 52 │ 52 │
│ zh │ 52 │ 0 │
└─────────────┴─────────────┴─────────┘
```
3. At the same time, we have added the relevant configuration in `src/utils/config.js`.
```javascript
{
...
i18n: {
languages: [
...
{
key:'ja',
title: '日本語',
flag: '/japanese.svg',
},
],
},
}
```
> Routing related effects, after the configuration `npm run start` takes effect after restart.
4. Use the built-in commands for automatic translation. You will see the translated configuration in `src/locales/ja/messages.json`.
```bash
npm run trans:only
```
You will see the following information:
```bash
start: en -> ja
...
youdao: en -> ja: Unpublished -> 未発表
youdao: en -> ja: Update -> 更新
youdao: en -> ja: Update User -> ユーザーの更新
youdao: en -> ja: Username -> 名
...
All translations have been completed.
```
> `npm run trans` will execute `npm run extract` and `npm run trans:only` in order.
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.

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>antd-admin - An admin dashboard application demo built upon Ant Design and UmiJS</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="An admin dashboard application demo built upon Ant Design and UmiJS">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
<link rel="icon" href="/_media/favicon.ico" />
</head>
<body>
<nav data-cloak class="app-nav">
<a href="/">En</a>
<a href="#/zh-cn/">中文</a>
</nav>
<div id="app">Loading...</div>
<script>
window.$docsify = {
name: 'AntD Admin',
loadSidebar: true,
maxLevel: 3,
subMaxLevel: 3,
auto2top: true,
autoHeader: true,
repo: 'zuiidea/antd-admin',
themeColor: '#1890ff',
search: {
paths: 'auto',
placeholder: {
'/zh-cn/': '搜索',
'/': 'Type to search'
},
noData: {
'/zh-cn/': '找不到结果',
'/': 'No Results'
}
}
}
// if (typeof navigator.serviceWorker !== 'undefined') {
// navigator.serviceWorker.register('sw.js')
// }
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/emoji.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/zoom-image.min.js"></script>
<script src="//unpkg.com/docsify-copy-code"></script>
</body>
</html>

@ -0,0 +1,60 @@
# Layout
## Add a new layout
Take a new layout named `secondary` as an example to make the route starting with `secondary` use this layout.
1. Add related configuration in `src/utils/config.js`. For details, please refer to [layouts](/configuration?id=layouts).
```javascript
   layouts: [
           {
               name: 'primary',
               include: [/.*/],
               exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/],
           },
           {
               name: 'secondary',
               include: [/(\/(en|zh))*\/secondary\/(.*)/],
           },
   ],
```
2. Add the `secondary` layout component to the `src/layouts/BaseLayout.js` file.
```javascript
   import SecondaryLayout from './SecondaryLayout'
   const LayoutMap = {
     primary: PrimaryLayout,
     public: PublicLayout,
     secondary: SecondaryLayout,
   }
```
3. Add the `SecondaryLayout.js` file to the `src/layouts/` directory.
```javascript
   import React from 'react'
   export default ({ children }) => {
     return (
       <div>
         <h1>Secondary</h1>
         {children}
       </div>
     )
   }
```
4. Add a `secondary/index.js` file to the `src/pages/` directory.
```javascript
   import React from 'react'
   export default ({ children }) => {
     Return <div>Secondary page Content</div>
   }
```
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.

@ -0,0 +1,22 @@
# HTTP request
this project use axios for http service, file located in src/utils/request.js
## Custom Header
As for privilege access or modify cookie, you could add header param by yourself
```
axios.defaults.headers.common['Authorization'] = 'token'
```
Or
```
axios.interceptors.request.use(function (config) {
config.headers.token = window.localStorage.getItem('token');
return config;
}, function (error) {
return Promise.reject(error);
});
```

@ -0,0 +1,83 @@
/* ===========================================================
* docsify sw.js
* ===========================================================
* Copyright 2016 @huxpro
* Licensed under Apache 2.0
* Register service worker.
* ========================================================== */
const RUNTIME = 'docsify'
const HOSTNAME_WHITELIST = [
self.location.hostname,
'fonts.gstatic.com',
'fonts.googleapis.com',
'unpkg.com'
]
// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
var now = Date.now()
var url = new URL(req.url)
// 1. fixed http URL
// Just keep syncing with location.protocol
// fetch(httpURL) belongs to active mixed content.
// And fetch(httpRequest) is not supported yet.
url.protocol = self.location.protocol
// 2. add query for caching-busting.
// Github Pages served with Cache-Control: max-age=600
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
if (url.hostname === self.location.hostname) {
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
}
return url.href
}
/**
* @Lifecycle Activate
* New one activated when old isnt being used.
*
* waitUntil(): activating ====> activated
*/
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
/**
* @Functional Fetch
* All network requests are being intercepted here.
*
* void respondWith(Promise<Response> r)
*/
self.addEventListener('fetch', event => {
// Skip some of cross-origin requests, like those for Google Analytics.
if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
// Stale-while-revalidate
// similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
// Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
const cached = caches.match(event.request)
const fixedUrl = getFixedUrl(event.request)
const fetched = fetch(fixedUrl, { cache: 'no-store' })
const fetchedCopy = fetched.then(resp => resp.clone())
// Call respondWith() with whatever we get first.
// If the fetch fails (e.g disconnected), wait for the cache.
// If theres nothing in cache, wait for the fetch.
// If neither yields a response, return offline pages.
event.respondWith(
Promise.race([fetched.catch(_ => cached), cached])
.then(resp => resp || fetched)
.catch(_ => { /* eat any errors */ })
)
// Update the cache with the version we fetched (only for ok status)
event.waitUntil(
Promise.all([fetchedCopy, caches.open(RUNTIME)])
.then(([response, cache]) => response.ok && cache.put(event.request, response))
.catch(_ => { /* eat any errors */ })
)
}
})

@ -0,0 +1,86 @@
# 接口配置
## 为什么
在使用了`redux`或者`dva`项目中,我们经常会写类似下面的`service`层的函数,使代码结构更清晰,但是很容易看出,我们会写很多相似的代码,在`antd-admin@5.0`中,使用了更加简洁的配置方式实现了相同的功能。
```javascript
export async function login(data) {
return request({
url: '/api/v1/user/login',
method: 'post',
data,
})
}
```
## 配置和使用
在`src/services/api.js`文件中,你会看到如下配置对象,对象的键用于调用时的函数名称,对象的值为请求的`url`,默认请求方式为`GET`,如果是其他请求方式对象的值的格式则为`'method url'`。
```javascript
export default {
...
queryUser: '/user/:id',
queryUserList: '/users',
updateUser: 'Patch /user/:id',
createUser: 'POST /user/:id',
removeUser: 'DELETE /user/:id',
removeUserList: 'POST /users/delete',
...
}
```
在其他文件中使用
```javascript
import { queryUser } from 'api'
// 一般文件中
...
queryUser(option).then(data => console.log(data))
...
// model文件中
...
yield call(queryUser, option)
...
```
## 实现方式
参考`src/services/index.js`文件对api配置进行遍历每个属性都返回对应的封装后的request函数。
```javascript
import request from 'utils/request'
import { apiPrefix } from 'utils/config'
import api from './api'
const gen = params => {
let url = apiPrefix + params
let method = 'GET'
const paramsArray = params.split(' ')
if (paramsArray.length === 2) {
method = paramsArray[0]
url = apiPrefix + paramsArray[1]
}
return function(data) {
return request({
url,
data,
method,
})
}
}
const APIFunction = {}
for (const key in api) {
APIFunction[key] = gen(api[key])
}
module.exports = APIFunction
```

@ -0,0 +1,84 @@
<p align="center">
<a href="http://github.com/zuiidea/antd-admin">
<img alt="antd-admin" height="64" src="../_media/logo.svg">
</a>
</p>
<h1 align="center">AntD Admin</h1>
<div align="center">
一套优秀的中后台前端解决方案
[![antd](https://img.shields.io/badge/antd-^3.10.0-blue.svg?style=flat-square)](https://github.com/ant-design/ant-design)
[![umi](https://img.shields.io/badge/umi-^2.2.1-orange.svg?style=flat-square)](https://github.com/umijs/umi)
[![GitHub issues](https://img.shields.io/github/issues/zuiidea/antd-admin.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/issues)
[![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT)
![Travis (.org)](https://img.shields.io/travis/zuiidea/antd-admin.svg)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/zuiidea/antd-admin/pulls)
[![Gitter](https://img.shields.io/gitter/room/antd-admin/antd-admin.svg)](https://gitter.im/antd-admin/antd-admin)
</div>
- 在线演示 - [https://antd-admin.zuiidea.com](https://antd-admin.zuiidea.com)
- 使用文档 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/](https://doc.antd-admin.zuiidea.com/#/zh-cn/)
- 常见问题 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/faq](https://doc.antd-admin.zuiidea.com/#/zh-cn/faq)
- 更新日志 - [https://doc.antd-admin.zuiidea.com/#/zh-cn/change-log](https://doc.antd-admin.zuiidea.com/#/zh-cn/change-log)
## 特性
- 国际化,源码中抽离翻译字段,按需加载语言包
- 动态权限,不同权限对应不同菜单
- 优雅美观Ant Design 设计体系
- Mock 数据,本地数据调试
## 使用
1. 下载项目代码。
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
cd my-project
```
2. 进入目录安装依赖,国内用户推荐使用 [cnpm](https://cnpmjs.org) 进行加速。
```bash
yarn install
```
或者
```bash
npm install
```
3. 启动本地服务器。
```bash
npm run start
```
4. 启动完成后打开浏览器访问 [http://localhost:7000](http://localhost:7000),如果需要更改启动端口,可在 `.env` 文件中配置。
> 更多信息请参考 [使用文档](https://doc.antd-admin.zuiidea.com/#/zh-cn/)。
## 支持环境
现代浏览器。
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --------- | --------- | --------- | --------- | --------- |
|IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley:
- 在你的公司或个人项目中使用 AntD Admin。
- 通过 [Issue](http://github.com/zuiidea/antd-admin/issues) 报告 bug 或进行咨询。
- 提交 [Pull Request](http://github.com/zuiidea/antd-admin/pulls) 改进代码。
> 强烈推荐阅读 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)、[《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545) 和 [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/%7Esgtatham/bugs-cn.html)、[《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393),更好的问题更容易获得帮助。

@ -0,0 +1,11 @@
- 入门
- [快速上手](zh-cn/getting-started.md)
- 定制化
- [配置项](zh-cn/configuration.md)
- [接口配置](zh-cn/API-configuration.md)
- [国际化](zh-cn/i18n.md)
- [布局](zh-cn/layout.md)
- [http 请求](zh-cn/request.md)
- 指南
- [部署](zh-cn/deploy.md)
- [更新日志](zh-cn/change-log.md)

@ -0,0 +1,52 @@
## 5.0.0
#### 优化
- 尽量使用修饰器,简化代码编写,提高代码可读性。
- API 配置化,简化获取数据方式。
- `utils` 内文件拆分,各司其职。
- 简化`utils/request`文件,不做特殊处理。
#### 规范
- 函数添加描述、参数、返回值等注释,含糊不清的代码增加注释,规范参考 [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html#appendices-jsdoc-tag-reference)。
- 语义化版本号,规范参加 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/)。
- 静态代码检查,统一代码风格,代码提交前将会使用 `prettier`、`stylelint`、`eslint` 规范代码。
- Git 提交信息规范化,[git-commit-emoji-cn](https://github.com/liuchengxu/git-commit-emoji-cn)。
- 基于 `Umi` 的约定式路由,无需再写路由配置文件。
- 使用 `React 16` 新特性,如 `Fragment`、`Context`、 `PureComponent`等。
#### 功能
- 支持国际化,源码中抽离翻译字段,按需加载语言包,自动在线翻译。
- 支持按需引入 `lodash` 函数。
- 支持多布局,可根据规则规定哪些路由使用哪种布局。
- 支持 Antd Admin 在 Travis 上自动编译和部署。
- 使用 `Docsify` 生成文档网站。
#### 样式
- 新增 Antd Admin 独立 Logo。
- 重写整体布局组件,优化菜单、面包屑导航自动高亮,菜单自动展开等逻辑。
- 移动端菜单更改为抽屉式。
#### 其他
- 废弃 `IconFont``Search`、`DataTable`等组件,因为在 `Antd` 中有很好的支持和可替代的。

@ -0,0 +1,102 @@
# 配置项
你可以在 `/src/utils/config.js` 里做一些自定义配置:
## siteName
- 类型: `String`
配置站点名称,应用到登录框,侧边栏顶部的标题文字显示。
## copyright
- 类型: `String`
配置版权声明,应用到登录页、`Primay`布局底部。
## logoPath
- 类型: `String`
配置站点 Logo应用到登录框侧边栏顶部的 Logo 显示。
## apiPrefix
- 类型: `String`
配置项目中接口的前缀,接口相关文档可查看 [接口配置](API-configuration.md)
## fixedHeader
- 类型: `String`
在`Primary`布局下,页面滚动时是否固定顶部。
## layouts
- 类型: `Array`
配置哪些路由使用哪种布局,未指定路由使用默认布局 `Public`,项目中目前有 `Primary``Public` 两种布局,
默认配置如下:
```javascript
layouts: [
{
name: 'primary',
include: [/.*/],
exclude: [/(\/(en|zh))*\/login/],
},
],
```
每种布局的对象属性如下:
- `name` - 布局的名称;
- `include` - 指定使用该布局的路由规则列表,规则可为正则表达式或者字符串;
- `exclude` - 指定不使用该布局的路由规则列表,规则可为正则表达式或者字符串。
> 注意:`exclude` 优先级高于 `include`前面的布局优先级高于后面的布局。开发过程中可能需要结合`src/layouts`目录下的布局使用,具体方法可查看 [使用布局](./layout.md)。
## i18n
- 类型: `Object`
配置国际化,默认配置如下:
```javascript
i18n: {
languages: [
{
key: 'en',
title: 'English',
flag: '/america.svg',
},
{
key: 'zh',
title: '中文',
flag: '/china.svg',
},
],
defaultLanguage: 'en',
}
```
### i18n.languages
- 类型: `Array`
指定应用支持哪些语言,每种语言的对象属性如下:
- `key` - 语言的`key`,应用到页面 url 上以区分语言,也对应 `src/locales` 目录下的语言包文件夹名;
- `title` - 语言名称,在登录页底部、`Primay` 布局顶部语言切换显示;
- `flag` - 语言的国旗图标的路径,在 `Primay` 布局顶部语言切换显示。
### i18n.defaultLanguage
- 类型: `String`
配置默认语言。

@ -0,0 +1,114 @@
# 部署
完成开发并且在开发环境验证之后,就需要部署给我们的用户了。
![i18n](../_media/term_build.svg)
## 构建
先执行下面的命令,
```bash
npm run build
```
几秒后,输出应该如下:
```bash
> antd-admin@5.0.0-beta build /Users/zuiidea/web/antd-admin
> umi build
[21:13:17] webpack compiled in 43s 868ms
DONE Compiled successfully in 43877ms 21:13:17
File sizes after gzip:
1.3 MB dist/vendors.async.js
308.21 KB dist/umi.js
45.49 KB dist/vendors.chunk.css
36.08 KB dist/p__chart__highCharts__index.async.js
33.53 KB dist/p__user__index.async.js
22.36 KB dist/p__chart__ECharts__index.async.js
4.21 KB dist/p__dashboard__index.async.js
4.06 KB dist/umi.css
...
```
`build` 命令会打包所有的资源,包含 JavaScript, CSS, web fonts, images, html 等。你可以在 `dist/` 目录下找到这些文件。
> 如果有使用 HashHistory 、 部署 html 到非根目录、静态化等需求,请查看[Umi 部署](https://umijs.org/zh/guide/deploy.html)。
## 本地验证
发布之前,可以通过 `serve` 做本地验证,
```
$ yarn global add serve
$ serve ./dist
Serving!
- Local: http://localhost:5000
- On Your Network: http://{Your IP}:5000
Copied local address to clipboard!
```
访问 [http://localhost:5000](http://localhost:5000),正常情况下法应该是和 `npm start` 一致的(接口可能无法获取到正确数据)。
## 部署
接下来,我们可以把静态文件上传到服务器,如果使用 Nginx 作为 Web server你可以在 `ngnix.conf` 中这样配置:
```
server
{
listen 80;
# 指定可访问的域名
server_name antd-admin.zuiidea.com;
# 编译后的文件存放的目录
root /home/www/antd-admin/dist;
# 代理服务端接口,避免跨域
location /api {
proxy_pass http://localhost:7000/api;
}
# 因为前端使用了BrowserHistory所以将路由 fallback 到 index.html
location / {
index index.html;
try_files $uri $uri/ /index.html;
}
}
```
重启 Web server访问 [http://antd-admin.zuiidea.com](http://antd-admin.zuiidea.com) ,你将看到正确的页面。
```bash
nginx -s reload
```
类似的,如果你使用 Caddy 作为 Web server你可以在 `Caddyfile` 中这样配置:
```
antd-admin.zuiidea.com {
gzip
root /home/www/antd-admin/dist
proxy /api http://localhost:7000
rewrite {
if {path} not_match ^/api
to {path} {path}/ /
}
}
antd-admin.zuiidea.com/public {
gzip
root /home/www/antd-admin/dist/static/public
}
```

@ -0,0 +1,7 @@
# 问题集锦
## 新建页面
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))
2. 修改 namespace/pathToRegexp 在 model.js
3. 修改 mock中route.js增加一条route

@ -0,0 +1,73 @@
# 快速上手
> 在开始之前,推荐先学习 [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/)。提前了解和学习这些知识会非常有帮助。
## 安装
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
cd my-project
```
## 目录结构
应用的目录结构如下
```bash
├── dist/ # 默认build输出目录
├── mock/ # Mock文件目录
├── public/ # 静态资源文件目录
├── src/ # 源码目录
│ ├── components/ # 组件目录
│ ├── e2e/ # e2e目录
│ ├── layouts/ # 布局目录
│ ├── locales/ # 国际化文件目录
│ ├── models/ # 数据模型目录
│ ├── pages/ # 页面组件目录
│ ├── services/ # 数据接口目录
│ │ ├── api.js # 接口配置
│ │ └── index.js # 接口输出
│ ├── themes/ # 项目样式目录
│ │ ├── default.less # 样式变量
│ │ ├── index.less # 全局样式
│ │ ├── mixin.less # 样式函数
│ │ └── vars.less # 样式变量及函数
│ ├── utils/ # 工具函数目录
│ │ ├── config.js # 项目配置
│ │ ├── constant.js # 静态常量
│ │ ├── index.js # 工具函数
│ │ ├── request.js # 异步请求函数(axios)
│ │ └── theme.js # 项目需要在js中使用到样式变量
├── .editorconfig # 编辑器配置
├── .env # 环境变量
├── .eslintrc # ESlint配置
├── .gitignore # Git忽略文件配置
├── .prettierignore # Prettier忽略文件配置
├── .prettierrc # Prettier配置
├── .stylelintrc.json # Stylelint配置
├── .travis.yml # Travis配置
└── .umirc.js # Umi配置
└── package.json # 项目信息
```
## 本地开发
1. 进入目录安装依赖,国内用户推荐使用 [cnpm](https://cnpmjs.org) 进行加速
```bash
yarn install
```
或者
```bash
npm install
```
2. 启动本地服务器
```bash
npm run start
```
3. 启动完成后打开浏览器访问 [http://localhost:7000](http://localhost:7000),如果需要更改启动端口,可在 `.env` 文件中配置。

@ -0,0 +1,75 @@
# 国际化
## 新增应用语言
以新增日语为例。
![i18n](../_media/term_i18n.svg)
1. 添加语言包本地文件,`ja` 为日语的语言代码,支持翻译的语言列表参考 [有道智云](http://ai.youdao.com/docs/doc-trans-api.s#p05),运行下面命令后会生成 `src/locales/ja/messages.json` 文件。
```bash
npm run add-locale ja
```
2. 提取代码中需要翻译的字段,即 `<Trans>message</Trans>`、`` intl.formatMessage({ id: 'message `` 中 `message` 字段,运行下面命令后 `src/locales/ja/messages.json' }) 将会出现提取后的字段配置。
```bash
npm run extract
```
你将看到如下信息:
```bash
Catalog statistics:
┌─────────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├─────────────┼─────────────┼─────────┤
│ en (source) │ 52 │ - │
│ ja │ 52 │ 52 │
│ zh │ 52 │ 0 │
└─────────────┴─────────────┴─────────┘
```
3. 与此同时,我们在 `src/utils/config.js` 新增相关配置。
```javascript
{
...
i18n: {
languages: [
...
{
key:'ja',
title: '日本語',
flag: '/japanese.svg',
},
],
},
}
```
>  路由相关效果,配置后 `npm run start` 重启后生效。
4. 使用内置的命令进行自动翻译,在 `src/locales/ja/messages.json` 中将会看到翻译后的配置。
```bash
npm run trans:only
```
你将看到如下信息:
```bash
start: en -> ja
...
youdao: en -> ja: Unpublished -> 未発表
youdao: en -> ja: Update -> 更新
youdao: en -> ja: Update User -> ユーザーの更新
youdao: en -> ja: Username -> 名
...
All translations have been completed.
```
> `npm run trans` 将会依次执行 `npm run extract``npm run trans:only`
5. 最后,可以在 `src/locales/ja/messages.json` 中对翻译不准确的的字段进行调整。启动开发模式 `npm run start`,打开 [http://localhost:7000/ja/login](http://localhost:7000/ja/login),你将看到日语版本的应用。

@ -0,0 +1,60 @@
# 布局
## 新增布局
以新增名为 `secondary` 的布局为例,使以 `secondary` 开头的路由都使用该布局。
1. 在 `src/utils/config.js` 新增相关配置,参数详细请查看 [layouts](/zh-cn/configuration?id=layouts)。
```javascript
layouts: [
{
name: 'primary',
include: [/.*/],
exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/],
},
{
name: 'secondary',
include: [/(\/(en|zh))*\/secondary\/(.*)/],
},
],
```
2. 在`src/layouts/BaseLayout.js` 文件中新增 `secondary` 布局组件。
```javascript
import SecondaryLayout from './SecondaryLayout'
const LayoutMap = {
primary: PrimaryLayout,
public: PublicLayout,
secondary: SecondaryLayout,
}
```
3. 在`src/layouts/` 目录中新增 `SecondaryLayout.js` 文件。
```javascript
import React from 'react'
export default ({ children }) => {
return (
<div>
<h1>Secondary</h1>
{children}
</div>
)
}
```
4. 在`src/pages/` 目录中新增 `secondary/index.js` 文件。
```javascript
import React from 'react'
export default ({ children }) => {
return <div>Secondary page Content</div>
}
```
5. 最后,启动开发模式 `npm run start`,打开 [http://localhost:7000/secondary/](http://localhost:7000/secondary/),你将看到 `secondary` 布局的页面。

@ -0,0 +1,24 @@
# HTTP请求
本项目使用了axios提供http请求服务文件在src/utils/request.js
## 自定义Header
为了提供鉴权、修改cookie等服务可以手动修改Header
```
axios.defaults.headers.common['Authorization'] = 'token'
```
或者
```
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
config.headers.token = window.localStorage.getItem('token');
return config;
}, function (error) {
return Promise.reject(error);
});
```

@ -0,0 +1,5 @@
# 路由
本项目中采用约定式路由
参考[umi 路由](https://umijs.org/zh/guide/router.html)

@ -0,0 +1,3 @@
module.exports = {
testURL: 'http://localhost:8000',
}

@ -0,0 +1,36 @@
{
"name": "Antd-Admin",
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "A front-end solution for enterprise applications built upon Ant Design and UmiJS",
"icons": [{
"src": "logo/logo@96.png",
"sizes": "72x72"
},
{
"src": "logo/logo@128.png",
"sizes": "128x128"
},
{
"src": "logo/logo@144.png",
"sizes": "144x144"
},
{
"src": "logo/logo@152.png",
"sizes": "152x152"
},
{
"src": "logo/logo@192.png",
"sizes": "192x192"
},
{
"src": "logo/logo@384.png",
"sizes": "384x384"
},
{
"src": "logo/logo@512.png",
"sizes": "512x512"
}
]
}

@ -0,0 +1,59 @@
/**
* Query objects that specify keys and values in an array where all values are objects.
* @param {array} array An array where all values are objects, like [{key:1},{key:2}].
* @param {string} key The key of the object that needs to be queried.
* @param {string} value The value of the object that needs to be queried.
* @return {object|undefined} Return frist object when query success.
*/
export function queryArray(array, key, value) {
if (!Array.isArray(array)) {
return
}
return array.filter(_ => _[key] === value)
}
export function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min) + min)
}
export function randomAvatar() {
const avatarList = [
'photo-1549492864-2ec7d66ffb04.jpeg',
'photo-1480535339474-e083439a320d.jpeg',
'photo-1523419409543-a5e549c1faa8.jpeg',
'photo-1519648023493-d82b5f8d7b8a.jpeg',
'photo-1523307730650-594bc63f9d67.jpeg',
'photo-1522962506050-a2f0267e4895.jpeg',
'photo-1489779162738-f81aed9b0a25.jpeg',
'photo-1534308143481-c55f00be8bd7.jpeg',
'photo-1519336555923-59661f41bb45.jpeg',
'photo-1551438632-e8c7d9a5d1b7.jpeg',
'photo-1525879000488-bff3b1c387cf.jpeg',
'photo-1487412720507-e7ab37603c6f.jpeg',
'photo-1510227272981-87123e259b17.jpeg'
]
return `//image.zuiidea.com/${avatarList[randomNumber(0, avatarList.length - 1)]}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim`
}
export const Constant = {
ApiPrefix: '/api/v1',
NotFound: {
message: 'Not Found',
documentation_url: '',
},
Color: {
green: '#64ea91',
blue: '#8fc9fb',
purple: '#d897eb',
red: '#f69899',
yellow: '#f8c82e',
peach: '#f797d6',
borderBase: '#e5e5e5',
borderSplit: '#f4f4f4',
grass: '#d6fbb5',
sky: '#c1e0fc',
},
}
export Mock from 'mockjs'
export qs from 'qs'

@ -0,0 +1,142 @@
import { Mock, Constant } from './_utils'
const { ApiPrefix, Color } = Constant
const Dashboard = Mock.mock({
'sales|8': [
{
'name|+1': 2008,
'Clothes|200-500': 1,
'Food|180-400': 1,
'Electronics|300-550': 1,
},
],
cpu: {
'usage|50-600': 1,
space: 825,
'cpu|40-90': 1,
'data|20': [
{
'cpu|20-80': 1,
},
],
},
browser: [
{
name: 'Google Chrome',
percent: 43.3,
status: 1,
},
{
name: 'Mozilla Firefox',
percent: 33.4,
status: 2,
},
{
name: 'Apple Safari',
percent: 34.6,
status: 3,
},
{
name: 'Internet Explorer',
percent: 12.3,
status: 4,
},
{
name: 'Opera Mini',
percent: 3.3,
status: 1,
},
{
name: 'Chromium',
percent: 2.53,
status: 1,
},
],
user: {
name: 'github',
sales: 3241,
sold: 3556,
},
'completed|12': [
{
'name|+1': 2008,
'Task complete|200-1000': 1,
'Cards Complete|200-1000': 1,
},
],
'comments|5': [
{
name: '@last',
'status|1-3': 1,
content: '@sentence',
avatar() {
return Mock.Random.image(
'48x48',
Mock.Random.color(),
'#757575',
'png',
this.name.substr(0, 1)
)
},
date() {
return `2016-${Mock.Random.date('MM-dd')} ${Mock.Random.time(
'HH:mm:ss'
)}`
},
},
],
'recentSales|36': [
{
'id|+1': 1,
name: '@last',
'status|1-4': 1,
date() {
return `${Mock.Random.integer(2015, 2016)}-${Mock.Random.date(
'MM-dd'
)} ${Mock.Random.time('HH:mm:ss')}`
},
'price|10-200.1-2': 1,
},
],
quote: {
name: 'Joho Doe',
title: 'Graphic Designer',
content:
"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.",
avatar:
'//cdn.antd-admin.zuiidea.com/bc442cf0cc6f7940dcc567e465048d1a8d634493198c4-sPx5BR_fw236',
},
numbers: [
{
icon: 'pay-circle-o',
color: Color.green,
title: 'Online Review',
number: 2781,
},
{
icon: 'team',
color: Color.blue,
title: 'New Customers',
number: 3241,
},
{
icon: 'message',
color: Color.purple,
title: 'Active Projects',
number: 253,
},
{
icon: 'shopping-cart',
color: Color.red,
title: 'Referrals',
number: 4324,
},
],
})
module.exports = {
[`GET ${ApiPrefix}/dashboard`](req, res) {
res.json(Dashboard)
},
}

@ -0,0 +1,67 @@
import { Mock, Constant } from './_utils'
const { ApiPrefix } = Constant
let postId = 0
const database = Mock.mock({
'data|100': [
{
id() {
postId += 1
return postId + 10000
},
'status|1-2': 1,
title: '@title',
author: '@last',
categories: '@word',
tags: '@word',
'views|10-200': 1,
'comments|10-200': 1,
visibility: () => {
return Mock.mock(
'@pick(["Public",' + '"Password protected", ' + '"Private"])'
)
},
date: '@dateTime',
image() {
return Mock.Random.image(
'100x100',
Mock.Random.color(),
'#757575',
'png',
this.author.substr(0, 1)
)
},
},
],
}).data
module.exports = {
[`GET ${ApiPrefix}/posts`](req, res) {
const { query } = req
let { pageSize, page, ...other } = query
pageSize = pageSize || 10
page = page || 1
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter(item => {
if ({}.hasOwnProperty.call(item, key)) {
return (
String(item[key])
.trim()
.indexOf(decodeURI(other[key]).trim()) > -1
)
}
return true
})
}
}
res.status(200).json({
data: newData.slice((page - 1) * pageSize, page * pageSize),
total: newData.length,
})
},
}

@ -0,0 +1,155 @@
import { Constant } from './_utils'
const { ApiPrefix } = Constant
const database = [
{
id: '1',
icon: 'dashboard',
name: 'Dashboard',
zh: {
name: '仪表盘'
},
'pt-br': {
name: 'Dashboard'
},
route: '/dashboard',
},
{
id: '2',
breadcrumbParentId: '1',
name: 'Users',
zh: {
name: '用户管理'
},
'pt-br': {
name: 'Usuário'
},
icon: 'user',
route: '/user',
},
{
id: '7',
breadcrumbParentId: '1',
name: 'Posts',
zh: {
name: '用户管理'
},
'pt-br': {
name: 'Posts'
},
icon: 'shopping-cart',
route: '/post',
},
{
id: '21',
menuParentId: '-1',
breadcrumbParentId: '2',
name: 'User Detail',
zh: {
name: '用户详情'
},
'pt-br': {
name: 'Detalhes do usuário'
},
route: '/user/:id',
},
{
id: '3',
breadcrumbParentId: '1',
name: 'Request',
zh: {
name: 'Request'
},
'pt-br': {
name: 'Requisição'
},
icon: 'api',
route: '/request',
},
{
id: '4',
breadcrumbParentId: '1',
name: 'UI Element',
zh: {
name: 'UI组件'
},
'pt-br': {
name: 'Elementos UI'
},
icon: 'camera-o',
},
{
id: '45',
breadcrumbParentId: '4',
menuParentId: '4',
name: 'Editor',
zh: {
name: 'Editor'
},
'pt-br': {
name: 'Editor'
},
icon: 'edit',
route: '/editor',
},
{
id: '5',
breadcrumbParentId: '1',
name: 'Charts',
zh: {
name: 'Charts'
},
'pt-br': {
name: 'Graficos'
},
icon: 'code-o',
},
{
id: '51',
breadcrumbParentId: '5',
menuParentId: '5',
name: 'ECharts',
zh: {
name: 'ECharts'
},
'pt-br': {
name: 'ECharts'
},
icon: 'line-chart',
route: '/chart/ECharts',
},
{
id: '52',
breadcrumbParentId: '5',
menuParentId: '5',
name: 'HighCharts',
zh: {
name: 'HighCharts'
},
'pt-br': {
name: 'HighCharts'
},
icon: 'bar-chart',
route: '/chart/highCharts',
},
{
id: '53',
breadcrumbParentId: '5',
menuParentId: '5',
name: 'Rechartst',
zh: {
name: 'Rechartst'
},
'pt-br': {
name: 'Rechartst'
},
icon: 'area-chart',
route: '/chart/Recharts',
},
]
module.exports = {
[`GET ${ApiPrefix}/routes`](req, res) {
res.status(200).json(database)
},
}

@ -0,0 +1,250 @@
import { Mock, Constant, randomAvatar } from './_utils'
import qs from 'qs'
const { ApiPrefix } = Constant
let usersListData = Mock.mock({
'data|80-100': [
{
id: '@id',
name: '@name',
nickName: '@last',
phone: /^1[34578]\d{9}$/,
'age|11-99': 1,
address: '@county(true)',
isMale: '@boolean',
email: '@email',
createTime: '@datetime',
avatar() {
return randomAvatar()
},
},
],
})
let database = usersListData.data
const EnumRoleType = {
ADMIN: 'admin',
DEFAULT: 'guest',
DEVELOPER: 'developer',
}
const userPermission = {
DEFAULT: {
visit: ['1', '2', '21', '7', '5', '51', '52', '53'],
role: EnumRoleType.DEFAULT,
},
ADMIN: {
role: EnumRoleType.ADMIN,
},
DEVELOPER: {
role: EnumRoleType.DEVELOPER,
},
}
const adminUsers = [
{
id: 0,
username: 'admin',
password: 'admin',
permissions: userPermission.ADMIN,
avatar: randomAvatar(),
},
{
id: 1,
username: 'guest',
password: 'guest',
permissions: userPermission.DEFAULT,
avatar: randomAvatar(),
},
{
id: 2,
username: '吴彦祖',
password: '123456',
permissions: userPermission.DEVELOPER,
avatar: randomAvatar(),
},
]
const queryArray = (array, key, keyAlias = 'key') => {
if (!(array instanceof Array)) {
return null
}
let data
for (let item of array) {
if (item[keyAlias] === key) {
data = item
break
}
}
if (data) {
return data
}
return null
}
const NOTFOUND = {
message: 'Not Found',
documentation_url: 'http://localhost:8000/request',
}
module.exports = {
[`POST ${ApiPrefix}/user/login`](req, res) {
const { username, password } = req.body
const user = adminUsers.filter(item => item.username === username)
if (user.length > 0 && user[0].password === password) {
const now = new Date()
now.setDate(now.getDate() + 1)
res.cookie(
'token',
JSON.stringify({ id: user[0].id, deadline: now.getTime() }),
{
maxAge: 900000,
httpOnly: true,
}
)
res.json({ success: true, message: 'Ok' })
} else {
res.status(400).end()
}
},
[`GET ${ApiPrefix}/user/logout`](req, res) {
res.clearCookie('token')
res.status(200).end()
},
[`GET ${ApiPrefix}/user`](req, res) {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
const response = {}
let user = {}
if (!cookies.token) {
res.status(200).send({ message: 'Not Login' })
return
}
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
}
if (response.success) {
const userItem = adminUsers.find(_ => _.id === token.id)
if (userItem) {
const { password, ...other } = userItem
user = other
}
}
response.user = user
res.json(response)
},
[`GET ${ApiPrefix}/users`](req, res) {
const { query } = req
let { pageSize, page, ...other } = query
pageSize = pageSize || 10
page = page || 1
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter(item => {
if ({}.hasOwnProperty.call(item, key)) {
if (key === 'address') {
return other[key].every(iitem => item[key].indexOf(iitem) > -1)
} else if (key === 'createTime') {
const start = new Date(other[key][0]).getTime()
const end = new Date(other[key][1]).getTime()
const now = new Date(item[key]).getTime()
if (start && end) {
return now >= start && now <= end
}
return true
}
return (
String(item[key])
.trim()
.indexOf(decodeURI(other[key]).trim()) > -1
)
}
return true
})
}
}
res.status(200).json({
data: newData.slice((page - 1) * pageSize, page * pageSize),
total: newData.length,
})
},
[`POST ${ApiPrefix}/users/delete`](req, res) {
const { ids=[] } = req.body
database = database.filter(item => !ids.some(_ => _ === item.id))
res.status(204).end()
},
[`POST ${ApiPrefix}/user`](req, res) {
const newData = req.body
newData.createTime = Mock.mock('@now')
newData.avatar =
newData.avatar ||
Mock.Random.image(
'100x100',
Mock.Random.color(),
'#757575',
'png',
newData.nickName.substr(0, 1)
)
newData.id = Mock.mock('@id')
database.unshift(newData)
res.status(200).end()
},
[`GET ${ApiPrefix}/user/:id`](req, res) {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
res.status(200).json(data)
} else {
res.status(200).json(NOTFOUND)
}
},
[`DELETE ${ApiPrefix}/user/:id`](req, res) {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
database = database.filter(item => item.id !== id)
res.status(204).end()
} else {
res.status(200).json(NOTFOUND)
}
},
[`PATCH ${ApiPrefix}/user/:id`](req, res) {
const { id } = req.params
const editItem = req.body
let isExist = false
database = database.map(item => {
if (item.id === id) {
isExist = true
return Object.assign({}, item, editItem)
}
return item
})
if (isExist) {
res.status(201).end()
} else {
res.status(200).json(NOTFOUND)
}
},
}

@ -0,0 +1,129 @@
{
"name": "antd-admin",
"version": "5.3.0",
"license": "MIT",
"description": "An admin dashboard application demo built upon Ant Design and UmiJS",
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@lingui/react": "^3.8.0",
"antd": "^4.0.0",
"axios": "^0.21.0",
"classnames": "^2.2.6",
"d3-shape": "^2.1.0",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.0",
"draftjs-to-markdown": "^0.6.0",
"dva-model-extend": "^0.1.2",
"echarts": "^5.0.0",
"echarts-for-react": "^3.0.0",
"echarts-gl": "^2.0.2",
"echarts-liquidfill": "^3.0.0",
"enquire-js": "^0.2.1",
"highcharts-exporting": "^0.1.7",
"highcharts-more": "^0.1.7",
"json-format": "^1.0.1",
"lodash": "^4.17.11",
"md5": "^2.2.1",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.1.0",
"prop-types": "^15.7.0",
"qs": "^6.10.0",
"react-adsense": "^0.1.0",
"react-countup": "^4.2.0",
"react-draft-wysiwyg": "^1.13.0",
"react-helmet": "^6.0.0",
"react-highcharts": "^16.1.0",
"react-perfect-scrollbar": "^1.5.0",
"recharts": "^2.0.0",
"store": "^2.0.0"
},
"devDependencies": {
"@babel/preset-react": "^7.12.13",
"@lingui/cli": "^3.8.0",
"@lingui/macro": "^3.8.0",
"@umijs/preset-react": "^1.8.0",
"babel-eslint": "^10.0.0",
"babel-plugin-dev-expression": "^0.2.0",
"babel-plugin-import": "^1.13.0",
"babel-plugin-macros": "^3.0.0",
"babel-plugin-module-resolver": "^4.0.0",
"cross-env": "^7.0.0",
"eslint": "^7.0.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.1.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.23.0",
"eslint-plugin-react-hooks": "^4.0.0",
"husky": "^4.2.0",
"less-vars-to-js": "^1.3.0",
"lint-staged": "^10.0.0",
"mockjs": "^1.1.0",
"module": "^1.2.5",
"prettier": "^2.0.0",
"stylelint": "^13.2.0",
"stylelint-config-prettier": "^8.0.0",
"stylelint-config-standard": "^21.0.0",
"typescript": "^4.2.3",
"umi": "^3.4.0"
},
"engines": {
"node": ">= 10.0.0"
},
"lint-staged": {
"src/**/*.js": [
"eslint --ext .js --fix",
"npm run prettier",
"git add"
],
"**/*.less": [
"stylelint --syntax less",
"npm run prettier",
"git add"
]
},
"lingui": {
"fallbackLocales": {
"default": "en"
},
"sourceLocale": "en",
"locales": [
"en",
"zh",
"pt-br"
],
"catalogs": [
{
"path": "src/locales/{locale}/messages",
"include": [
"src/pages",
"src/layouts",
"src/components",
"src/layouts"
]
}
],
"format": "minimal",
"extractBabelOptions": {
"presets": [
"@umijs/babel-preset-umi",
"@babel/preset-react"
]
}
},
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "umi build",
"check:model": "umi dva list model",
"lint:js": "eslint --ext .js src",
"lint:style": "stylelint \"src/**/*.less\" --syntax less",
"start": "umi dev",
"test": "cross-env BABELRC=none umi test",
"prettier": "prettier --write 'src/**/*.{js,less}'",
"precommit": "lint-staged",
"add-locale": "lingui add-locale",
"extract": "lingui extract",
"trans": "lingui extract --clean && node ./scripts/translate.js",
"doc": "docsify serve docs"
}
}

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#F0F0F0;" width="512" height="512"/>
<g>
<rect y="64" style="fill:#D80027;" width="512" height="64"/>
<rect y="192" style="fill:#D80027;" width="512" height="64"/>
<rect y="320" style="fill:#D80027;" width="512" height="64"/>
<rect y="448" style="fill:#D80027;" width="512" height="64"/>
</g>
<rect style="fill:#2E52B2;" width="256" height="275.69"/>
<g>
<polygon style="fill:#F0F0F0;" points="51.518,115.318 45.924,132.529 27.826,132.529 42.469,143.163 36.875,160.375
51.518,149.741 66.155,160.375 60.56,143.163 75.203,132.529 57.106,132.529 "/>
<polygon style="fill:#F0F0F0;" points="57.106,194.645 51.518,177.434 45.924,194.645 27.826,194.645 42.469,205.279
36.875,222.49 51.518,211.857 66.155,222.49 60.56,205.279 75.203,194.645 "/>
<polygon style="fill:#F0F0F0;" points="51.518,53.202 45.924,70.414 27.826,70.414 42.469,81.047 36.875,98.259 51.518,87.625
66.155,98.259 60.56,81.047 75.203,70.414 57.106,70.414 "/>
<polygon style="fill:#F0F0F0;" points="128.003,115.318 122.409,132.529 104.311,132.529 118.954,143.163 113.36,160.375
128.003,149.741 142.64,160.375 137.045,143.163 151.689,132.529 133.591,132.529 "/>
<polygon style="fill:#F0F0F0;" points="133.591,194.645 128.003,177.434 122.409,194.645 104.311,194.645 118.954,205.279
113.36,222.49 128.003,211.857 142.64,222.49 137.045,205.279 151.689,194.645 "/>
<polygon style="fill:#F0F0F0;" points="210.076,194.645 204.489,177.434 198.894,194.645 180.797,194.645 195.44,205.279
189.845,222.49 204.489,211.857 219.125,222.49 213.531,205.279 228.174,194.645 "/>
<polygon style="fill:#F0F0F0;" points="204.489,115.318 198.894,132.529 180.797,132.529 195.44,143.163 189.845,160.375
204.489,149.741 219.125,160.375 213.531,143.163 228.174,132.529 210.076,132.529 "/>
<polygon style="fill:#F0F0F0;" points="128.003,53.202 122.409,70.414 104.311,70.414 118.954,81.047 113.36,98.259
128.003,87.625 142.64,98.259 137.045,81.047 151.689,70.414 133.591,70.414 "/>
<polygon style="fill:#F0F0F0;" points="204.489,53.202 198.894,70.414 180.797,70.414 195.44,81.047 189.845,98.259
204.489,87.625 219.125,98.259 213.531,81.047 228.174,70.414 210.076,70.414 "/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-49 141 512 512" style="enable-background:new -49 141 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#D80027;}
.st1{fill:#FFDA44;}
</style>
<rect x="-49" y="141" class="st0" width="512" height="512"/>
<g>
<polygon class="st1" points="91.1,296.8 113.2,364.8 184.7,364.8 126.9,406.9 149,474.9 91.1,432.9 33.2,474.9 55.4,406.9
-2.5,364.8 69,364.8 "/>
<polygon class="st1" points="254.5,537.5 237.6,516.7 212.6,526.4 227.1,503.9 210.2,483 236.1,489.9 250.7,467.4 252.1,494.2
278.1,501.1 253,510.7 "/>
<polygon class="st1" points="288.1,476.5 296.1,450.9 274.2,435.4 301,435 308.9,409.4 317.6,434.8 344.4,434.5 322.9,450.5
331.5,475.9 309.6,460.4 "/>
<polygon class="st1" points="333.4,328.9 321.6,353 340.8,371.7 314.3,367.9 302.5,391.9 297.9,365.5 271.3,361.7 295.1,349.2
290.5,322.7 309.7,341.4 "/>
<polygon class="st1" points="255.2,255.9 253.2,282.6 278.1,292.7 252,299.1 250.1,325.9 236,303.1 209.9,309.5 227.2,289
213,266.3 237.9,276.4 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

@ -0,0 +1,24 @@
<svg width="169px" height="141px" viewBox="0 0 169 141" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="54.0428975%" y1="4.39752391%" x2="54.0428975%" y2="108.456714%" id="linearGradient-1">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="62.3089445%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="14.2201464%" x2="50%" y2="113.263844%" id="linearGradient-2">
<stop stop-color="#FA816E" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="65.9092442%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(0.000000, -5.000000)">
<rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(83.718923, 75.312358) rotate(-24.000000) translate(-83.718923, -75.312358) " x="68.7189234" y="0.312357954" width="30" height="150" rx="15"></rect>
<rect id="Rectangle" fill="url(#linearGradient-1)" transform="translate(129.009910, 75.580213) rotate(-24.000000) translate(-129.009910, -75.580213) " x="114.00991" y="0.580212739" width="30" height="150" rx="15"></rect>
<circle id="Oval" fill="url(#linearGradient-2)" cx="25" cy="120" r="25"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#D80027;" width="512" height="512"/>
<polygon style="fill:#6DA544;" points="196.641,0 196.641,264.348 196.641,512 0,512 0,0 "/>
<circle style="fill:#FFDA44;" cx="196.641" cy="256" r="96"/>
<path style="fill:#D80027;" d="M142.638,208v60c0,29.823,24.178,54,54,54s54-24.178,54-54v-60H142.638z"/>
<path style="fill:#F0F0F0;" d="M196.638,286c-9.925,0-18-8.075-18-18v-24.001h36V268C214.638,277.925,206.563,286,196.638,286z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 973 B

@ -0,0 +1,114 @@
/**
* Youdao Translate, My private account is for testing purposes only.
* Please go to the official account to apply for an account. Thank you for your cooperation.
* APP ID055c2d71f9a05214
* Secret keyZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN
*/
/* eslint-disable */
const md5 = require('md5')
const qs = require('qs')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const jsonFormat = require('json-format')
const { i18n } = require('../src/utils/config')
const { languages, defaultLanguage } = i18n
const locales = {}
languages.forEach(item => {
locales[item.key] = require(`../src/locales/${item.key}/messages.json`)
})
const youdao = ({ q, from, to }) =>
new Promise((resolve, reject) => {
{
const appid = '055c2d71f9a05214'
const appse = 'ZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN'
const salt = Date.now()
const sign = md5(appid + q + salt + appse)
const query = qs.stringify({
q,
from,
to,
appKey: appid,
salt,
sign,
})
axios.get(`http://openapi.youdao.com/api?${query}`).then(({ data }) => {
if (data.query && data.translation[0]) {
resolve(data.translation[0])
} else {
resolve(q)
}
})
}
})
const transform = async ({ from, to, locales, outputPath }) => {
for (const key in locales[from]) {
if (locales[to][key]) {
console.log(`add to skip: ${key}`)
} else {
let res = key
let way = 'youdao'
if (key.indexOf('/') !== 0) {
const reg = '{([^{}]*)}'
const tasks = key
.match(new RegExp(`${reg}|((?<=(${reg}|^)).*?(?=(${reg}|$)))`, 'g'))
.map(item => {
if (new RegExp(reg).test(item)) {
return Promise.resolve(item)
}
return youdao({
q: item,
from,
to,
})
})
res = (await Promise.all(tasks)).join('')
} else {
res = `/${to + key}`
way = 'link'
}
if (res !== key) {
locales[to][key] = res
console.log(`${way}: ${from} -> ${to}: ${key} -> ${res}`)
} else {
console.log(`same: ${from} -> ${to}: ${key}`)
}
}
}
await fs.writeFileSync(
path.resolve(__dirname, outputPath),
jsonFormat(locales[to], {
type: 'space',
size: 2,
})
)
}
;(async () => {
const tasks = languages
.map(item => ({
from: defaultLanguage,
to: item.key,
}))
.filter(item => item.from !== item.to)
for (const item of tasks) {
console.log(`start: ${item.from} -> ${item.to}`)
await transform({
from: item.from,
to: item.to,
locales,
outputPath: `../src/locales/${item.to}/messages.json`,
})
console.log(`completed: ${item.from} -> ${item.to}`)
}
console.log('All translations have been completed.')
})()

@ -0,0 +1,35 @@
import React from 'react'
import PropTypes from 'prop-types'
import { BarsOutlined, DownOutlined } from '@ant-design/icons'
import { Dropdown, Button, Menu } from 'antd'
const DropOption = ({
onMenuClick,
menuOptions = [],
buttonStyle,
dropdownProps,
}) => {
const menu = menuOptions.map(item => (
<Menu.Item key={item.key}>{item.name}</Menu.Item>
))
return (
<Dropdown
overlay={<Menu onClick={onMenuClick}>{menu}</Menu>}
{...dropdownProps}
>
<Button style={{ border: 'none', ...buttonStyle }}>
<BarsOutlined style={{ marginRight: 2 }} />
<DownOutlined />
</Button>
</Dropdown>
)
}
DropOption.propTypes = {
onMenuClick: PropTypes.func,
menuOptions: PropTypes.array.isRequired,
buttonStyle: PropTypes.object,
dropdownProps: PropTypes.object,
}
export default DropOption

@ -0,0 +1,6 @@
{
"name": "DropOption",
"version": "0.0.0",
"private": true,
"main": "./DropOption.js"
}

@ -0,0 +1,17 @@
import React from 'react'
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import styles from './Editor.less'
const DraftEditor = props => {
return (
<Editor
toolbarClassName={styles.toolbar}
wrapperClassName={styles.wrapper}
editorClassName={styles.editor}
{...props}
/>
)
}
export default DraftEditor

@ -0,0 +1,106 @@
.wrapper {
height: 500px;
:global {
.rdw-dropdownoption-default {
padding: 6px;
}
.rdw-dropdown-optionwrapper {
box-sizing: content-box;
width: 100%;
border-radius: 0 0 2px 2px;
&:hover {
box-shadow: none;
}
}
.rdw-inline-wrapper {
flex-wrap: wrap;
margin-bottom: 0;
.rdw-option-wrapper {
margin-bottom: 6px;
}
}
.rdw-option-active {
box-shadow: 1px 1px 0 #e8e8e8 inset;
}
.rdw-colorpicker-option {
box-shadow: none;
}
.rdw-colorpicker-modal,
.rdw-embedded-modal,
.rdw-emoji-modal,
.rdw-image-modal,
.rdw-link-modal {
box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
}
.rdw-colorpicker-modal,
.rdw-embedded-modal,
.rdw-link-modal {
height: auto;
}
.rdw-emoji-modal {
width: 214px;
}
.rdw-colorpicker-modal {
width: auto;
}
.rdw-embedded-modal-btn,
.rdw-image-modal-btn,
.rdw-link-modal-btn {
height: 32px;
margin-top: 12px;
}
.rdw-embedded-modal-input,
.rdw-embedded-modal-size-input,
.rdw-link-modal-input {
padding: 2px 6px;
height: 32px;
}
.rdw-dropdown-selectedtext {
color: #000;
}
.rdw-dropdown-wrapper,
.rdw-option-wrapper {
min-width: 36px;
transition: all 0.2s ease;
height: 30px;
&:active {
box-shadow: 1px 1px 0 #e8e8e8 inset;
}
&:hover {
box-shadow: 1px 1px 0 #e8e8e8;
}
}
.rdw-dropdown-wrapper {
min-width: 60px;
}
.rdw-editor-main {
box-sizing: border-box;
}
}
.editor {
border: 1px solid #f1f1f1;
padding: 5px;
border-radius: 2px;
height: auto;
min-height: 200px;
}
}

@ -0,0 +1,6 @@
{
"name": "Editor",
"version": "0.0.0",
"private": true,
"main": "./Editor.js"
}

@ -0,0 +1,21 @@
import React from 'react';
import { TooltipProps } from 'antd/lib/tooltip';
export interface EllipsisTooltipProps extends TooltipProps {
title?: undefined;
overlayStyle?: undefined;
}
export interface EllipsisProps {
tooltip?: boolean | EllipsisTooltipProps;
length?: number;
lines?: number;
style?: React.CSSProperties;
className?: string;
fullWidthRecognition?: boolean;
}
export function getStrFullLength(str: string): number;
export function cutStrByFullLength(str: string, maxLength: number): string;
export default class Ellipsis extends React.Component<EllipsisProps, any> {}

@ -0,0 +1,270 @@
import React, { Component } from 'react';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
/* eslint react/no-did-mount-set-state: 0 */
/* eslint no-param-reassign: 0 */
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
export const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0);
if (charCode >= 0 && charCode <= 128) {
return pre + 1;
}
return pre + 2;
}, 0);
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0;
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0);
if (charCode >= 0 && charCode <= 128) {
showLength += 1;
} else {
showLength += 2;
}
if (showLength <= maxLength) {
return pre + cur;
}
return pre;
}, '');
};
const getTooltip = ({ tooltip, overlayStyle, title, children }) => {
if (tooltip) {
const props = tooltip === true ? { overlayStyle, title } : { ...tooltip, overlayStyle, title };
return <Tooltip {...props}>{children}</Tooltip>;
}
return children;
};
const EllipsisText = ({ text, length, tooltip, fullWidthRecognition, ...other }) => {
if (typeof text !== 'string') {
throw new Error('Ellipsis children must be string.');
}
const textLength = fullWidthRecognition ? getStrFullLength(text) : text.length;
if (textLength <= length || length < 0) {
return <span {...other}>{text}</span>;
}
const tail = '...';
let displayText;
if (length - tail.length <= 0) {
displayText = '';
} else {
displayText = fullWidthRecognition ? cutStrByFullLength(text, length) : text.slice(0, length);
}
const spanAttrs = tooltip ? {} : { ...other };
return getTooltip({
tooltip,
overlayStyle: TooltipOverlayStyle,
title: text,
children: (
<span {...spanAttrs}>
{displayText}
{tail}
</span>
),
});
};
export default class Ellipsis extends Component {
state = {
text: '',
targetCount: 0,
};
componentDidMount() {
if (this.node) {
this.computeLine();
}
}
componentDidUpdate(perProps) {
const { lines } = this.props;
if (lines !== perProps.lines) {
this.computeLine();
}
}
computeLine = () => {
const { lines } = this.props;
if (lines && !isSupportLineClamp) {
const text = this.shadowChildren.innerText || this.shadowChildren.textContent;
const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10);
const targetHeight = lines * lineHeight;
this.content.style.height = `${targetHeight}px`;
const totalHeight = this.shadowChildren.offsetHeight;
const shadowNode = this.shadow.firstChild;
if (totalHeight <= targetHeight) {
this.setState({
text,
targetCount: text.length,
});
return;
}
// bisection
const len = text.length;
const mid = Math.ceil(len / 2);
const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode);
this.setState({
text,
targetCount: count,
});
}
};
bisection = (th, m, b, e, text, shadowNode) => {
const suffix = '...';
let mid = m;
let end = e;
let begin = b;
shadowNode.innerHTML = text.substring(0, mid) + suffix;
let sh = shadowNode.offsetHeight;
if (sh <= th) {
shadowNode.innerHTML = text.substring(0, mid + 1) + suffix;
sh = shadowNode.offsetHeight;
if (sh > th || mid === begin) {
return mid;
}
begin = mid;
if (end - begin === 1) {
mid = 1 + begin;
} else {
mid = Math.floor((end - begin) / 2) + begin;
}
return this.bisection(th, mid, begin, end, text, shadowNode);
}
if (mid - 1 < 0) {
return mid;
}
shadowNode.innerHTML = text.substring(0, mid - 1) + suffix;
sh = shadowNode.offsetHeight;
if (sh <= th) {
return mid - 1;
}
end = mid;
mid = Math.floor((end - begin) / 2) + begin;
return this.bisection(th, mid, begin, end, text, shadowNode);
};
handleRoot = n => {
this.root = n;
};
handleContent = n => {
this.content = n;
};
handleNode = n => {
this.node = n;
};
handleShadow = n => {
this.shadow = n;
};
handleShadowChildren = n => {
this.shadowChildren = n;
};
render() {
const { text, targetCount } = this.state;
const {
children,
lines,
length,
className,
tooltip,
fullWidthRecognition,
...restProps
} = this.props;
const cls = classNames(styles.ellipsis, className, {
[styles.lines]: lines && !isSupportLineClamp,
[styles.lineClamp]: lines && isSupportLineClamp,
});
if (!lines && !length) {
return (
<span className={cls} {...restProps}>
{children}
</span>
);
}
// length
if (!lines) {
return (
<EllipsisText
className={cls}
length={length}
text={children || ''}
tooltip={tooltip}
fullWidthRecognition={fullWidthRecognition}
{...restProps}
/>
);
}
const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`;
// support document.body.style.webkitLineClamp
if (isSupportLineClamp) {
const style = `#${id}{-webkit-line-clamp:${lines};-webkit-box-orient: vertical;}`;
const node = (
<div id={id} className={cls} {...restProps}>
<style>{style}</style>
{children}
</div>
);
return getTooltip({
tooltip,
overlayStyle: TooltipOverlayStyle,
title: children,
children: node,
});
}
const childNode = (
<span ref={this.handleNode}>
{targetCount > 0 && text.substring(0, targetCount)}
{targetCount > 0 && targetCount < text.length && '...'}
</span>
);
return (
<div {...restProps} ref={this.handleRoot} className={cls}>
<div ref={this.handleContent}>
{getTooltip({
tooltip,
overlayStyle: TooltipOverlayStyle,
title: text,
children: childNode,
})}
<div className={styles.shadow} ref={this.handleShadowChildren}>
{children}
</div>
<div className={styles.shadow} ref={this.handleShadow}>
<span>{text}</span>
</div>
</div>
</div>
);
}
}

@ -0,0 +1,24 @@
.ellipsis {
display: inline-block;
width: 100%;
overflow: hidden;
word-break: break-all;
}
.lines {
position: relative;
.shadow {
position: absolute;
z-index: -999;
display: block;
color: transparent;
opacity: 0;
}
}
.lineClamp {
position: relative;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
}

@ -0,0 +1,17 @@
---
title: Ellipsis
subtitle: 文本自动省略号
cols: 1
order: 10
---
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
## API
| 参数 | 说明 | 类型 | 默认值 |
| -------------------- | ------------------------------------------------ | ------- | ------ |
| tooltip | 移动到文本展示完整内容的提示 | boolean | - |
| length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - |
| lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` |
| fullWidthRecognition | 是否将全角字符的长度视为 2 来计算字符串长度 | boolean | - |

@ -0,0 +1,13 @@
import { getStrFullLength, cutStrByFullLength } from './index';
describe('test calculateShowLength', () => {
it('get full length', () => {
expect(getStrFullLength('一二a,')).toEqual(8);
});
it('cut str by full length', () => {
expect(cutStrByFullLength('一二a,', 7)).toEqual('一二a');
});
it('cut str when length small', () => {
expect(cutStrByFullLength('一22三', 5)).toEqual('一22');
});
});

@ -0,0 +1,28 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './FilterItem.less'
const FilterItem = ({ label = '', children }) => {
const labelArray = label.split('')
return (
<div className={styles.filterItem}>
{labelArray.length > 0 && (
<div className={styles.labelWrap}>
{labelArray.map((item, index) => (
<span className="labelText" key={index}>
{item}
</span>
))}
</div>
)}
<div className={styles.item}>{children}</div>
</div>
)
}
FilterItem.propTypes = {
label: PropTypes.string,
children: PropTypes.element.isRequired,
}
export default FilterItem

@ -0,0 +1,17 @@
.filterItem {
display: flex;
justify-content: space-between;
.labelWrap {
width: 80px;
line-height: 28px;
margin-right: 12px;
justify-content: space-between;
display: flex;
overflow: hidden;
}
.item {
flex: 1;
}
}

@ -0,0 +1,6 @@
{
"name": "FilterItem",
"version": "0.0.0",
"private": true,
"main": "./FilterItem.js"
}

@ -0,0 +1,14 @@
import React from 'react';
export interface GlobalFooterProps {
links?: Array<{
key?: string;
title: React.ReactNode;
href: string;
blankTarget?: boolean;
}>;
copyright?: React.ReactNode;
style?: React.CSSProperties;
className?: string;
}
export default class GlobalFooter extends React.Component<GlobalFooterProps, any> {}

@ -0,0 +1,28 @@
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
const GlobalFooter = ({ className, links, copyright }) => {
const clsString = classNames(styles.globalFooter, className);
return (
<footer className={clsString}>
{links && (
<div className={styles.links}>
{links.map(link => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))}
</div>
)}
{copyright && <div className={styles.copyright}>{copyright}</div>}
</footer>
);
};
export default GlobalFooter;

@ -0,0 +1,29 @@
@import '~antd/lib/style/themes/default.less';
.globalFooter {
margin: 48px 0 24px 0;
padding: 0 16px;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: @text-color-secondary;
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
&:hover {
color: @text-color;
}
}
}
.copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}

@ -0,0 +1,15 @@
---
title: GlobalFooter
subtitle: 全局页脚
cols: 1
order: 7
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
| 参数 | 说明 | 类型 | 默认值 |
| --------- | -------- | ---------------------------------------------------------------- | ------ |
| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - |
| copyright | 版权信息 | ReactNode | - |

@ -0,0 +1,68 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb } from 'antd'
import { Link, withRouter } from 'umi'
import { t } from "@lingui/macro"
import iconMap from 'utils/iconMap'
const { pathToRegexp } = require('path-to-regexp')
import { queryAncestors } from 'utils'
import styles from './Bread.less'
@withRouter
class Bread extends PureComponent {
generateBreadcrumbs = (paths) => {
return paths.map((item, key) => {
const content = item && (
<Fragment>
{item.icon && (
<span style={{ marginRight: 4 }}>{iconMap[item.icon]}</span>
)}
{item.name}
</Fragment>
)
return (
item && (
<Breadcrumb.Item key={key}>
{paths.length - 1 !== key ? (
<Link to={item.route || '#'}>{content}</Link>
) : (
content
)}
</Breadcrumb.Item>
)
)
})
}
render() {
const { routeList, location } = this.props
// Find a route that matches the pathname.
const currentRoute = routeList.find(
(_) => _.route && pathToRegexp(_.route).exec(location.pathname)
)
// Find the breadcrumb navigation of the current route match and all its ancestors.
const paths = currentRoute
? queryAncestors(routeList, currentRoute, 'breadcrumbParentId').reverse()
: [
routeList[0],
{
id: 404,
name: t`Not Found`,
},
]
return (
<Breadcrumb className={styles.bread}>
{this.generateBreadcrumbs(paths)}
</Breadcrumb>
)
}
}
Bread.propTypes = {
routeList: PropTypes.array,
}
export default Bread

@ -0,0 +1,16 @@
.bread {
margin-bottom: 24px;
:global {
.ant-breadcrumb {
display: flex;
align-items: center;
}
}
}
@media (max-width: 767px) {
.bread {
margin-bottom: 12px;
}
}

@ -0,0 +1,169 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Menu, Layout, Avatar, Popover, Badge, List } from 'antd'
import { Ellipsis } from 'components'
import {
BellOutlined,
RightOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons'
import { Trans } from "@lingui/macro"
import { getLocale, setLocale } from 'utils'
import moment from 'moment'
import classnames from 'classnames'
import config from 'config'
import styles from './Header.less'
const { SubMenu } = Menu
class Header extends PureComponent {
handleClickMenu = e => {
e.key === 'SignOut' && this.props.onSignOut()
}
render() {
const {
fixed,
avatar,
username,
collapsed,
notifications,
onCollapseChange,
onAllNotificationsRead,
} = this.props
const rightContent = [
<Menu key="user" mode="horizontal" onClick={this.handleClickMenu}>
<SubMenu
title={
<Fragment>
<span style={{ color: '#999', marginRight: 4 }}>
<Trans>Hi,</Trans>
</span>
<span>{username}</span>
<Avatar style={{ marginLeft: 8 }} src={avatar} />
</Fragment>
}
>
<Menu.Item key="SignOut">
<Trans>Sign out</Trans>
</Menu.Item>
</SubMenu>
</Menu>,
]
if (config.i18n) {
const { languages } = config.i18n
const language = getLocale()
const currentLanguage = languages.find(
item => item.key === language
)
rightContent.unshift(
<Menu
key="language"
selectedKeys={[currentLanguage.key]}
onClick={data => {
setLocale(data.key)
}}
mode="horizontal"
>
<SubMenu title={<Avatar size="small" src={currentLanguage.flag} />}>
{languages.map(item => (
<Menu.Item key={item.key}>
<Avatar
size="small"
style={{ marginRight: 8 }}
src={item.flag}
/>
{item.title}
</Menu.Item>
))}
</SubMenu>
</Menu>
)
}
rightContent.unshift(
<Popover
placement="bottomRight"
trigger="click"
key="notifications"
overlayClassName={styles.notificationPopover}
getPopupContainer={() => document.querySelector('#primaryLayout')}
content={
<div className={styles.notification}>
<List
itemLayout="horizontal"
dataSource={notifications}
locale={{
emptyText: <Trans>You have viewed all notifications.</Trans>,
}}
renderItem={item => (
<List.Item className={styles.notificationItem}>
<List.Item.Meta
title={
<Ellipsis tooltip lines={1}>
{item.title}
</Ellipsis>
}
description={moment(item.date).fromNow()}
/>
<RightOutlined style={{ fontSize: 10, color: '#ccc' }} />
</List.Item>
)}
/>
{notifications.length ? (
<div
onClick={onAllNotificationsRead}
className={styles.clearButton}
>
<Trans>Clear notifications</Trans>
</div>
) : null}
</div>
}
>
<Badge
count={notifications.length}
dot
offset={[-10, 10]}
className={styles.iconButton}
>
<BellOutlined className={styles.iconFont} />
</Badge>
</Popover>
)
return (
<Layout.Header
className={classnames(styles.header, {
[styles.fixed]: fixed,
[styles.collapsed]: collapsed,
})}
id="layoutHeader"
>
<div
className={styles.button}
onClick={onCollapseChange.bind(this, !collapsed)}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
<div className={styles.rightContainer}>{rightContent}</div>
</Layout.Header>
)
}
}
Header.propTypes = {
fixed: PropTypes.bool,
user: PropTypes.object,
menus: PropTypes.array,
collapsed: PropTypes.bool,
onSignOut: PropTypes.func,
notifications: PropTypes.array,
onCollapseChange: PropTypes.func,
onAllNotificationsRead: PropTypes.func,
}
export default Header

@ -0,0 +1,154 @@
@import '~themes/vars.less';
.header {
padding: 0;
box-shadow: @shadow-2;
position: relative;
display: flex;
justify-content: space-between;
height: 72px;
z-index: 9;
align-items: center;
background-color: #fff;
&.fixed {
position: fixed;
top: 0;
right: 0;
width: ~'calc(100% - 256px)';
z-index: 29;
transition: width 0.2s;
&.collapsed {
width: ~'calc(100% - 80px)';
}
}
:global {
.ant-menu-submenu-title {
height: 72px;
}
.ant-menu-horizontal {
line-height: 72px;
& > .ant-menu-submenu:hover {
color: @primary-color;
background-color: @hover-color;
}
}
.ant-menu {
border-bottom: none;
height: 72px;
}
.ant-menu-horizontal > .ant-menu-submenu {
top: 0;
margin-top: 0;
}
.ant-menu-horizontal > .ant-menu-item,
.ant-menu-horizontal > .ant-menu-submenu {
border-bottom: none;
}
.ant-menu-horizontal > .ant-menu-item-active,
.ant-menu-horizontal > .ant-menu-item-open,
.ant-menu-horizontal > .ant-menu-item-selected,
.ant-menu-horizontal > .ant-menu-item:hover,
.ant-menu-horizontal > .ant-menu-submenu-active,
.ant-menu-horizontal > .ant-menu-submenu-open,
.ant-menu-horizontal > .ant-menu-submenu-selected,
.ant-menu-horizontal > .ant-menu-submenu:hover {
border-bottom: none;
}
}
.rightContainer {
display: flex;
align-items: center;
}
.button {
width: 72px;
height: 72px;
line-height: 72px;
text-align: center;
font-size: 18px;
cursor: pointer;
transition: @transition-ease-in;
&:hover {
color: @primary-color;
background-color: @hover-color;
}
}
}
.iconButton {
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 24px;
cursor: pointer;
.background-hover();
&:hover {
.iconFont {
color: @primary-color;
}
}
& + .iconButton {
margin-left: 8px;
}
.iconFont {
color: #b2b0c7;
font-size: 24px;
}
}
.notification {
padding: 24px 0;
width: 320px;
.notificationItem {
transition: all 0.3s;
padding: 12px 24px;
cursor: pointer;
&:hover {
background-color: @hover-color;
}
}
.clearButton {
text-align: center;
height: 48px;
line-height: 48px;
cursor: pointer;
.background-hover();
}
}
.notificationPopover {
:global {
.ant-popover-inner-content {
padding: 0;
}
.ant-popover-arrow {
display: none;
}
.ant-list-item-content {
flex: 0;
margin-left: 16px;
}
}
}
@media (max-width: 767px) {
.header {
width: 100% !important;
}
}

@ -0,0 +1,122 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Menu } from 'antd'
import { NavLink, withRouter } from 'umi'
import { pathToRegexp } from 'path-to-regexp'
import { arrayToTree, queryAncestors } from 'utils'
import iconMap from 'utils/iconMap'
import store from 'store'
const { SubMenu } = Menu
@withRouter
class SiderMenu extends PureComponent {
state = {
openKeys: store.get('openKeys') || [],
}
onOpenChange = openKeys => {
const { menus } = this.props
const rootSubmenuKeys = menus.filter(_ => !_.menuParentId).map(_ => _.id)
const latestOpenKey = openKeys.find(
key => this.state.openKeys.indexOf(key) === -1
)
let newOpenKeys = openKeys
if (rootSubmenuKeys.indexOf(latestOpenKey) !== -1) {
newOpenKeys = latestOpenKey ? [latestOpenKey] : []
}
this.setState({
openKeys: newOpenKeys,
})
store.set('openKeys', newOpenKeys)
}
generateMenus = data => {
return data.map(item => {
if (item.children) {
return (
<SubMenu
key={item.id}
title={
<Fragment>
{item.icon && iconMap[item.icon]}
<span>{item.name}</span>
</Fragment>
}
>
{this.generateMenus(item.children)}
</SubMenu>
)
}
return (
<Menu.Item key={item.id}>
<NavLink to={item.route || '#'}>
{item.icon && iconMap[item.icon]}
<span>{item.name}</span>
</NavLink>
</Menu.Item>
)
})
}
render() {
const {
collapsed,
theme,
menus,
location,
isMobile,
onCollapseChange,
} = this.props
// Generating tree-structured data for menu content.
const menuTree = arrayToTree(menus, 'id', 'menuParentId')
// Find a menu that matches the pathname.
const currentMenu = menus.find(
_ => _.route && pathToRegexp(_.route).exec(location.pathname)
)
// Find the key that should be selected according to the current menu.
const selectedKeys = currentMenu
? queryAncestors(menus, currentMenu, 'menuParentId').map(_ => _.id)
: []
const menuProps = collapsed
? {}
: {
openKeys: this.state.openKeys,
}
return (
<Menu
mode="inline"
theme={theme}
onOpenChange={this.onOpenChange}
selectedKeys={selectedKeys}
onClick={
isMobile
? () => {
onCollapseChange(true)
}
: undefined
}
{...menuProps}
>
{this.generateMenus(menuTree)}
</Menu>
)
}
}
SiderMenu.propTypes = {
menus: PropTypes.array,
theme: PropTypes.string,
isMobile: PropTypes.bool,
onCollapseChange: PropTypes.func,
}
export default SiderMenu

@ -0,0 +1,88 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Switch, Layout } from 'antd'
import { t } from "@lingui/macro"
import { Trans } from "@lingui/macro"
import { BulbOutlined } from '@ant-design/icons'
import ScrollBar from '../ScrollBar'
import { config } from 'utils'
import SiderMenu from './Menu'
import styles from './Sider.less'
class Sider extends PureComponent {
render() {
const {
menus,
theme,
isMobile,
collapsed,
onThemeChange,
onCollapseChange,
} = this.props
return (
<Layout.Sider
width={256}
theme={theme}
breakpoint="lg"
trigger={null}
collapsible
collapsed={collapsed}
onBreakpoint={!isMobile && onCollapseChange}
className={styles.sider}
>
<div className={styles.brand}>
<div className={styles.logo}>
<img alt="logo" src={config.logoPath} />
{!collapsed && <h1>{config.siteName}</h1>}
</div>
</div>
<div className={styles.menuContainer}>
<ScrollBar
options={{
// Disabled horizontal scrolling, https://github.com/utatti/perfect-scrollbar#options
suppressScrollX: true,
}}
>
<SiderMenu
menus={menus}
theme={theme}
isMobile={isMobile}
collapsed={collapsed}
onCollapseChange={onCollapseChange}
/>
</ScrollBar>
</div>
{!collapsed && (
<div className={styles.switchTheme}>
<span>
<BulbOutlined />
<Trans>Switch Theme</Trans>
</span>
<Switch
onChange={onThemeChange.bind(
this,
theme === 'dark' ? 'light' : 'dark'
)}
defaultChecked={theme === 'dark'}
checkedChildren={t`Dark`}
unCheckedChildren={t`Light`}
/>
</div>
)}
</Layout.Sider>
)
}
}
Sider.propTypes = {
menus: PropTypes.array,
theme: PropTypes.string,
isMobile: PropTypes.bool,
collapsed: PropTypes.bool,
onThemeChange: PropTypes.func,
onCollapseChange: PropTypes.func,
}
export default Sider

@ -0,0 +1,110 @@
@import '~themes/vars.less';
.sider {
box-shadow: fade(@primary-color, 10%) 0 0 28px 0;
z-index: 10;
:global {
.ant-layout-sider-children {
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
.brand {
z-index: 1;
height: 72px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
box-shadow: 0 1px 9px -3px rgba(0, 0, 0, 0.2);
.logo {
display: flex;
align-items: center;
justify-content: center;
img {
width: 36px;
margin-right: 8px;
}
h1 {
vertical-align: text-bottom;
font-size: 16px;
text-transform: uppercase;
display: inline-block;
font-weight: 700;
color: @primary-color;
white-space: nowrap;
margin-bottom: 0;
.text-gradient();
:local {
animation: fadeRightIn 300ms @ease-in-out;
animation-fill-mode: both;
}
}
}
}
.menuContainer {
height: ~'calc(100vh - 120px)';
overflow-x: hidden;
flex: 1;
padding: 24px 0;
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
}
:global {
.ant-menu-inline {
border-right: none;
}
}
}
.switchTheme {
width: 100%;
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
overflow: hidden;
transition: all 0.3s;
span {
white-space: nowrap;
overflow: hidden;
font-size: 12px;
}
:global {
.anticon {
min-width: 14px;
margin-right: 4px;
font-size: 14px;
}
}
}
@keyframes fadeLeftIn {
0% {
transform: translateX(5px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}

@ -0,0 +1,6 @@
import Header from './Header'
import Menu from './Menu'
import Bread from './Bread'
import Sider from './Sider'
export { Header, Menu, Bread, Sider }

@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import styles from './Loader.less'
const Loader = ({ spinning = false, fullScreen }) => {
return (
<div
className={classNames(styles.loader, {
[styles.hidden]: !spinning,
[styles.fullScreen]: fullScreen,
})}
>
<div className={styles.warpper}>
<div className={styles.inner} />
<div className={styles.text}>LOADING</div>
</div>
</div>
)
}
Loader.propTypes = {
spinning: PropTypes.bool,
fullScreen: PropTypes.bool,
}
export default Loader

@ -0,0 +1,67 @@
.loader {
background-color: #fff;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 100000;
display: flex;
justify-content: center;
align-items: center;
opacity: 1;
text-align: center;
&.fullScreen {
position: fixed;
}
.warpper {
width: 100px;
height: 100px;
display: inline-flex;
flex-direction: column;
justify-content: space-around;
}
.inner {
width: 40px;
height: 40px;
margin: 0 auto;
text-indent: -12345px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
border-left: 1px solid rgba(0, 0, 0, 0.7);
border-radius: 50%;
z-index: 100001;
:local {
animation: spinner 600ms infinite linear;
}
}
.text {
width: 100px;
height: 20px;
text-align: center;
font-size: 12px;
letter-spacing: 4px;
color: #000;
}
&.hidden {
z-index: -1;
opacity: 0;
transition: opacity 1s ease 0.5s, z-index 0.1s ease 1.5s;
}
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

@ -0,0 +1,6 @@
{
"name": "Loader",
"version": "0.0.0",
"private": true,
"main": "./Loader.js"
}

@ -0,0 +1,33 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Loader from '../Loader'
import styles from './Page.less'
export default class Page extends Component {
render() {
const { className, children, loading = false, inner = false } = this.props
const loadingStyle = {
height: 'calc(100vh - 184px)',
overflow: 'hidden',
}
return (
<div
className={classnames(className, {
[styles.contentInner]: inner,
})}
style={loading ? loadingStyle : null}
>
{loading ? <Loader spinning /> : ''}
{children}
</div>
)
}
}
Page.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
loading: PropTypes.bool,
inner: PropTypes.bool,
}

@ -0,0 +1,16 @@
@import '~themes/vars.less';
.contentInner {
background: #fff;
padding: 24px;
box-shadow: @shadow-1;
min-height: ~'calc(100vh - 230px)';
position: relative;
}
@media (max-width: 767px) {
.contentInner {
padding: 12px;
min-height: ~'calc(100vh - 160px)';
}
}

@ -0,0 +1,6 @@
{
"name": "Page",
"version": "0.0.0",
"private": true,
"main": "./Page.js"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save