Compare commits

..
29 Commits
Author SHA1 Message Date
superlbr ca1d182795 upgrade dep: umi^4.0.0-canary.20220429.4 2022-04-30 06:41:00 +08:00
superlbr 4042d5ba3a upgrade dep: umi 4.x & react 18 2022-04-28 10:44:42 +08:00
zuiidea da4abc2293 ✨ adding page, removing some components 2021-06-06 16:27:47 +08:00
zuiidea 13c13961c7 ✨ improving useRequest 2021-04-18 21:57:18 +08:00
zuiidea 0d1c4f23e9 Merge branch 'develop-next' of https://github.com/zuiidea/antd-admin into develop-next 2021-04-18 17:03:50 +08:00
Baorong Li f3c6ccf7b2 chore config 2021-04-13 16:11:45 +08:00
zuiidea a18c9bcb7e ✨
adding user page
2021-04-12 22:32:51 +08:00
zuiidea 12eab5f5c7 ✨ adding useRequest 2021-04-11 22:14:18 +08:00
zuiidea 1dbeb05005 ✨ Upgrading components to typescript 2021-04-05 22:54:36 +08:00
zuiidea a0000d7371 ✨ adding login page. 2021-04-05 16:54:22 +08:00
zuiidea b0cb27712d Merge branch 'master' into develop-next
Conflicts:
	.umirc.js
	mock/route.js
	package.json
	src/components/Layout/Bread.js
	src/components/Layout/Menu.js
	src/components/index.js
	src/layouts/BaseLayout.js
	src/layouts/PrimaryLayout.js
	src/layouts/index.js
	src/models/app.js
	src/pages/404.tsx
	src/pages/chart/ECharts/AirportCoordComponent.js
	src/pages/chart/ECharts/BubbleGradientComponent.js
	src/pages/chart/ECharts/CalendarComponent.js
	src/pages/chart/ECharts/GCalendarComponent.js
	src/pages/chart/ECharts/LunarCalendarComponent.js
	src/pages/chart/ECharts/MainPageComponent.js
	src/pages/chart/ECharts/MapChartComponent.js
	src/pages/chart/ECharts/ThemeChartComponent.js
	src/pages/chart/ECharts/TreemapComponent.js
	src/pages/dashboard/components/browser.js
	src/pages/dashboard/components/comments.js
	src/pages/dashboard/components/completed.js
	src/pages/dashboard/components/numberCard.js
	src/pages/dashboard/components/recentSales.js
	src/pages/dashboard/components/sales.js
	src/pages/dashboard/index.js
	src/pages/dashboard/model.js
	src/pages/editor/index.js
	src/pages/index.js
	src/pages/login/index.js
	src/pages/login/model.js
	src/pages/post/components/List.js
	src/pages/post/index.js
	src/pages/post/model.js
	src/pages/request/index.js
	src/pages/user/[id]/index.js
	src/pages/user/[id]/index.less
	src/pages/user/[id]/models/detail.js
	src/pages/user/components/Filter.js
	src/pages/user/components/List.js
	src/pages/user/components/Modal.js
	src/pages/user/index.js
	src/pages/user/model.js
	src/services/index.js
	src/utils/index.js
2021-04-05 16:14:07 +08:00
zainchen 0bb381f419 feat: updating umi and typescript 2021-04-04 23:20:50 +08:00
zuiidea 8c01bc1488 ⚡️ Upgrading umi@3.4 2021-04-04 10:42:59 +08:00
zuiidea d8baaad3b1 🔥 Removing unnecessary pages. 2021-04-03 21:52:26 +08:00
Baorong Li e32a19fe3b chore dep: normal 2021-03-26 22:22:33 +08:00
Baorong Li c6382c6221 chore umi: use default targets no ie support 2021-03-26 22:20:53 +08:00
Baorong Li 02280fbed9 Merge pull request #1091 from jaywhen/patch-1
fix garbled characters in README-zh_CN.md
2021-03-17 17:46:29 +08:00
zainchen d897eb205d fix: adding babel preset to fixing extract command 2021-03-17 17:44:38 +08:00
Baorong Li b1f290ea32 upgrade dep: echarts@5.x 2021-03-17 17:28:05 +08:00
Baorong Li 0e36b6b7a0 fix utils: remove params to close #1067 in request 2021-03-17 17:03:14 +08:00
Baorong Li d92f121cf4 upgrade lingui: 3.x 2021-03-17 16:50:49 +08:00
Xiang Jaywhen 0a73ba0f12 fix garbled characters in README-zh_CN.md 2021-02-19 23:19:51 +08:00
Baorong Li 9e4e0efbab Merge pull request #1082 from AmagiDDmxh/master
Fix en layout doc typo, same as before #1081
2020-12-21 23:27:57 -06:00
amagiddmxh 524dff07d2 Fix en layout doc typo 2020-12-21 16:05:47 +08:00
Baorong Li 8b3e5c08f7 Merge pull request #1081 from AmagiDDmxh/master
There were some type in the API-config doc, https://doc.antd-admin.zuiidea.com/#/API-configuration
2020-12-21 00:23:22 -06:00
amagiddmxh c17444f5d6 Fix API config uppercase typo 2020-12-21 12:55:26 +08:00
Baorong Li df85ecdfff Merge pull request #1080 from karlhorky/patch-1
Fix typo
2020-12-15 08:24:21 -06:00
Karl Horky b8824141d6 Fix typo 2020-12-15 15:21:29 +01:00
Karl Horky aedd2ec3db Fix typo 2020-12-15 15:20:49 +01:00
207 changed files with 5893 additions and 21570 deletions
-6
View File
@@ -1,7 +1 @@
src/**/*-test.js
src/public
src/routes/chart/ECharts/theme
src/routes/chart/highCharts/mapdata
src/locales/_build/
src/locales/**/*.js
docs/**/*.js
+2 -1
View File
@@ -1,7 +1,8 @@
{
"extends": "react-app",
"rules": {
"jsx-a11y/href-no-hash": "off",
"import/no-anonymous-default-export": 0,
"jsx-a11y/href-no-hash": 0,
"no-console": "warn",
"valid-jsdoc": "warn"
}
+4 -1
View File
@@ -5,6 +5,7 @@ npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
pnpm-lock.yaml
# ide
.idea
@@ -20,4 +21,6 @@ package-lock.json
# jslingui
src/locales/_build
src/locales/**/*.js
src/locales/**/*.js
.vscode
-132
View File
@@ -1,132 +0,0 @@
// 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 ? '/' : 'https://cdn.antd-admin.zuiidea.com/',
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',
},
extraBabelPresets: ['@lingui/babel-preset-react'],
extraBabelPlugins: [
[
'import',
{
libraryName: 'lodash',
libraryDirectory: '',
camel2DashComponentName: false,
},
'lodash',
],
[
'import',
{
libraryName: '@ant-design/icons',
libraryDirectory: 'es/icons',
camel2DashComponentName: false,
},
'ant-design-icons',
],
],
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' },
},
},
targets: { ie: 9 },
// Theme for antd
// https://ant.design/docs/react/customize-theme
theme: lessToJs(
fs.readFileSync(path.join(__dirname, './src/themes/default.less'), 'utf8')
),
chainWebpack: function (config, { webpack }) {
config.merge({
optimization: {
minimize: true,
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,
},
},
},
},
})
},
}
+2 -2
View File
@@ -38,7 +38,7 @@
## 使用
1. 下载项目代码。
1. 下载项目代码。
```bash
git clone https://github.com/zuiidea/antd-admin.git my-project
@@ -84,4 +84,4 @@ npm run start
- 通过 [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),更好的问题更容易获得帮助。
> 强烈推荐阅读 [《提问的智慧》](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),更好的问题更容易获得帮助。
+5
View File
@@ -0,0 +1,5 @@
const config = {
publicPath: 'https://cdn.antd-admin.zuiidea.com/',
}
export default config
+61
View File
@@ -0,0 +1,61 @@
/**
* see more, https://umijs.org/config
*/
import lessToJs from 'less-vars-to-js'
import fs from 'fs'
import routes from './routes'
import path from 'path'
import proxy from './proxy'
const theme = lessToJs(
fs.readFileSync(path.join(__dirname, '../src/themes/default.less'), 'utf8')
)
const config = {
hash: true,
ignoreMomentLocale: true,
targets: { ie: 11 },
theme,
proxy,
routes,
devtool: 'eval',
mock: {
exclude: ['utils/index'],
},
extraBabelPresets: ['@lingui/babel-preset-react'],
extraBabelPlugins: [
'macros',
[
'import',
{
libraryName: 'lodash',
libraryDirectory: '',
camel2DashComponentName: false,
},
'lodash',
],
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
},
'antd',
],
[
'import',
{
libraryName: '@ant-design',
libraryDirectory: 'icons',
camel2DashComponentName: false,
},
'ant-design-icons',
],
],
chainWebpack: (memo) => {
memo.module.rule('po').test(/\.po$/).use('po').loader('@lingui/loader')
},
}
export default config
+7
View File
@@ -0,0 +1,7 @@
export default {
'/api/v1/weather': {
target: 'https://api.seniverse.com/',
changeOrigin: true,
pathRewrite: { '^/api/v1/weather': '/v3/weather' },
},
}
+13
View File
@@ -0,0 +1,13 @@
const routes = [
{
exact: false,
path: '/',
// component: '@/layouts/index', // comment due to double layout
routes: [
{ exact: true, path: '/login', component: '@/pages/login' },
{ exact: true, path: '/user', component: '@/pages/user' },
],
},
]
export default routes
+26 -26
View File
@@ -5,11 +5,11 @@
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,
export async function login(data) {
  return request({
    url: '/api/v1/user/login',
    method: 'post',
    data,
  })
}
```
@@ -19,7 +19,7 @@ Export async function login(data) {
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 {
export default {
  ...
  queryUser: '/user/:id',
  queryUserList: '/users',
@@ -34,7 +34,7 @@ Export default {
Used in other files
```javascript
Import { queryUser } from 'api'
import { queryUser } from 'api'
// in the general file
...
@@ -43,7 +43,7 @@ queryUser(option).then(data => console.log(data))
/ / Model file
...
Yield call(queryUser, option)
yield call(queryUser, option)
...
```
@@ -52,35 +52,35 @@ Yield call(queryUser, option)
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 request from 'utils/request'
import { apiPrefix } from 'utils/config'
Import api from './api'
import api from './api'
Const gen = params => {
  Let url = apiPrefix + params
  Let method = 'GET'
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]
  const paramsArray = params.split(' ')
  if (paramsArray.length === 2) {
    method = paramsArray[0]
    url = apiPrefix + paramsArray[1]
  }
  Return function(data) {
    Return request({
      Url,
      Data,
      Method,
  return function(data) {
    return request({
      url,
      data,
      method,
    })
  }
}
Const APIFunction = {}
For (const key in api) {
const APIFunction = {}
for (const key in api) {
  APIFunction[key] = gen(api[key])
}
Module.exports = APIFunction
module.exports = APIFunction
```
+1 -1
View File
@@ -12,7 +12,7 @@ Take Japanese as an example.
npm run add-locale ja
```
2. Extract the fields in the code that need to be translated, ie `<Trans>?message</Trans>`, `` i18n.t`message `` in the `message` field, run the following command after `src/locales/ja /messages.json` will appear after the extracted field configuration.
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
+11 -11
View File
@@ -2,7 +2,7 @@
## Add a new layout
Take a new layout named `secondary` as an example to make the route starting with `seconday` use this 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).
@@ -11,11 +11,11 @@ Take a new layout named `secondary` as an example to make the route starting wit
           {
               name: 'primary',
               include: [/.*/],
               exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/seconday\/(.*)/],
               exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/],
           },
           {
               name: 'secondary',
               include: [/(\/(en|zh))*\/seconday\/(.*)/],
               include: [/(\/(en|zh))*\/secondary\/(.*)/],
           },
   ],
```
@@ -26,9 +26,9 @@ Take a new layout named `secondary` as an example to make the route starting wit
   import SecondaryLayout from './SecondaryLayout'
   const LayoutMap = {
     Primary: PrimaryLayout,
     Public: PublicLayout,
     Secondary: SecondaryLayout,
     primary: PrimaryLayout,
     public: PublicLayout,
     secondary: SecondaryLayout,
   }
```
@@ -38,23 +38,23 @@ Take a new layout named `secondary` as an example to make the route starting wit
   import React from 'react'
   export default ({ children }) => {
     Return (
     return (
       <div>
         <h1>Seconday</h1>
         <h1>Secondary</h1>
         {children}
       </div>
     )
   }
```
4. Add a `seconday/index.js` file to the `src/pages/` directory.
4. Add a `secondary/index.js` file to the `src/pages/` directory.
```javascript
   import React from 'react'
   export default ({ children }) => {
     Return <div>Seconday page Content</div>
     Return <div>Secondary page Content</div>
   }
```
5. Finally, start the development mode `npm run start`, open [http://localhost:7000/seconday/](http://localhost:7000/seconday/) and you will see the page for the `seconday` layout.
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.
+7 -7
View File
@@ -2,7 +2,7 @@
## 新增布局
以新增名为 `secondary` 的布局为例,使以 `seconday` 开头的路由都使用该布局。
以新增名为 `secondary` 的布局为例,使以 `secondary` 开头的路由都使用该布局。
1. 在 `src/utils/config.js` 新增相关配置,参数详细请查看 [layouts](/zh-cn/configuration?id=layouts)。
@@ -11,11 +11,11 @@
{
name: 'primary',
include: [/.*/],
exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/seconday\/(.*)/],
exclude: [/(\/(en|zh))*\/login/, /(\/(en|zh))*\/secondary\/(.*)/],
},
{
name: 'secondary',
include: [/(\/(en|zh))*\/seconday\/(.*)/],
include: [/(\/(en|zh))*\/secondary\/(.*)/],
},
],
```
@@ -40,21 +40,21 @@
export default ({ children }) => {
return (
<div>
<h1>Seconday</h1>
<h1>Secondary</h1>
{children}
</div>
)
}
```
4. 在`src/pages/` 目录中新增 `seconday/index.js` 文件。
4. 在`src/pages/` 目录中新增 `secondary/index.js` 文件。
```javascript
import React from 'react'
export default ({ children }) => {
return <div>Seconday page Content</div>
return <div>Secondary page Content</div>
}
```
5. 最后,启动开发模式 `npm run start`,打开 [http://localhost:7000/seconday/](http://localhost:7000/seconday/),你将看到 `seconday` 布局的页面。
5. 最后,启动开发模式 `npm run start`,打开 [http://localhost:7000/secondary/](http://localhost:7000/secondary/),你将看到 `secondary` 布局的页面。
+1 -1
View File
@@ -1,3 +1,3 @@
module.exports = {
testURL: 'http://localhost:8000',
testURL: 'http://localhost:8000'
}
+4008
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,4 +1,5 @@
import { Mock, Constant } from './_utils'
import Mock from 'mockjs'
import { Constant } from './utils'
const { ApiPrefix, Color } = Constant
@@ -135,7 +136,7 @@ const Dashboard = Mock.mock({
],
})
module.exports = {
export default {
[`GET ${ApiPrefix}/dashboard`](req, res) {
res.json(Dashboard)
},
+2
View File
@@ -0,0 +1,2 @@
declare module 'mockjs'
declare module 'qs'
-67
View File
@@ -1,67 +0,0 @@
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,
})
},
}
+24 -24
View File
@@ -1,4 +1,4 @@
import { Constant } from './_utils'
import { Constant } from './utils'
const { ApiPrefix } = Constant
const database = [
@@ -7,10 +7,10 @@ const database = [
icon: 'dashboard',
name: 'Dashboard',
zh: {
name: '仪表盘'
name: '仪表盘',
},
'pt-br': {
name: 'Dashboard'
name: 'Dashboard',
},
route: '/dashboard',
},
@@ -19,10 +19,10 @@ const database = [
breadcrumbParentId: '1',
name: 'Users',
zh: {
name: '用户管理'
name: '用户管理',
},
'pt-br': {
name: 'Usuário'
name: 'Usuário',
},
icon: 'user',
route: '/user',
@@ -32,10 +32,10 @@ const database = [
breadcrumbParentId: '1',
name: 'Posts',
zh: {
name: '用户管理'
name: '用户管理',
},
'pt-br': {
name: 'Posts'
name: 'Posts',
},
icon: 'shopping-cart',
route: '/post',
@@ -46,10 +46,10 @@ const database = [
breadcrumbParentId: '2',
name: 'User Detail',
zh: {
name: '用户详情'
name: '用户详情',
},
'pt-br': {
name: 'Detalhes do usuário'
name: 'Detalhes do usuário',
},
route: '/user/:id',
},
@@ -58,10 +58,10 @@ const database = [
breadcrumbParentId: '1',
name: 'Request',
zh: {
name: 'Request'
name: 'Request',
},
'pt-br': {
name: 'Requisição'
name: 'Requisição',
},
icon: 'api',
route: '/request',
@@ -71,10 +71,10 @@ const database = [
breadcrumbParentId: '1',
name: 'UI Element',
zh: {
name: 'UI组件'
name: 'UI组件',
},
'pt-br': {
name: 'Elementos UI'
name: 'Elementos UI',
},
icon: 'camera-o',
},
@@ -84,10 +84,10 @@ const database = [
menuParentId: '4',
name: 'Editor',
zh: {
name: 'Editor'
name: 'Editor',
},
'pt-br': {
name: 'Editor'
name: 'Editor',
},
icon: 'edit',
route: '/editor',
@@ -97,10 +97,10 @@ const database = [
breadcrumbParentId: '1',
name: 'Charts',
zh: {
name: 'Charts'
name: 'Charts',
},
'pt-br': {
name: 'Graficos'
name: 'Graficos',
},
icon: 'code-o',
},
@@ -110,10 +110,10 @@ const database = [
menuParentId: '5',
name: 'ECharts',
zh: {
name: 'ECharts'
name: 'ECharts',
},
'pt-br': {
name: 'ECharts'
name: 'ECharts',
},
icon: 'line-chart',
route: '/chart/ECharts',
@@ -124,10 +124,10 @@ const database = [
menuParentId: '5',
name: 'HighCharts',
zh: {
name: 'HighCharts'
name: 'HighCharts',
},
'pt-br': {
name: 'HighCharts'
name: 'HighCharts',
},
icon: 'bar-chart',
route: '/chart/highCharts',
@@ -138,17 +138,17 @@ const database = [
menuParentId: '5',
name: 'Rechartst',
zh: {
name: 'Rechartst'
name: 'Rechartst',
},
'pt-br': {
name: 'Rechartst'
name: 'Rechartst',
},
icon: 'area-chart',
route: '/chart/Recharts',
},
]
module.exports = {
export default {
[`GET ${ApiPrefix}/routes`](req, res) {
res.status(200).json(database)
},
+47 -35
View File
@@ -1,9 +1,10 @@
import { Mock, Constant, randomAvatar } from './_utils'
import Mock from 'mockjs'
import qs from 'qs'
import { Constant, randomAvatar, waitTime } from './utils'
const { ApiPrefix } = Constant
let usersListData = Mock.mock({
const usersListData = Mock.mock({
'data|80-100': [
{
id: '@id',
@@ -91,10 +92,28 @@ const NOTFOUND = {
documentation_url: 'http://localhost:8000/request',
}
module.exports = {
[`POST ${ApiPrefix}/user/login`](req, res) {
const authorized = (req, res) => {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
if (!cookies.token) {
res.status(401).send({ message: 'Not Login' })
return false
}
const token = JSON.parse(cookies.token)
if (cookies.token && token && token.deadline > new Date().getTime()) {
return token
}
res.status(401).send({ message: 'Not Login' })
return false
}
export default {
async [`POST ${ApiPrefix}/user/login`](req, res) {
await waitTime()
const { username, password } = req.body
const user = adminUsers.filter(item => item.username === username)
const user = adminUsers.filter((item) => item.username === username)
if (user.length > 0 && user[0].password === password) {
const now = new Date()
@@ -113,33 +132,27 @@ module.exports = {
}
},
[`GET ${ApiPrefix}/user/logout`](req, res) {
async [`GET ${ApiPrefix}/user/logout`](req, res) {
await waitTime()
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' })
async [`GET ${ApiPrefix}/user`](req, res) {
await waitTime()
const token = authorized(req, res)
if (!token) {
return
}
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
const userItem = adminUsers.find((_) => _.id === token.id)
if (userItem) {
const { password, ...rest } = userItem
res.json(rest)
} else {
res.status(401).send({ message: 'Not Login' })
}
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) {
@@ -151,10 +164,10 @@ module.exports = {
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter(item => {
newData = newData.filter((item) => {
if ({}.hasOwnProperty.call(item, key)) {
if (key === 'address') {
return other[key].every(iitem => item[key].indexOf(iitem) > -1)
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()
@@ -166,9 +179,8 @@ module.exports = {
return true
}
return (
String(item[key])
.trim()
.indexOf(decodeURI(other[key]).trim()) > -1
String(item[key]).trim().indexOf(decodeURI(other[key]).trim()) >
-1
)
}
return true
@@ -177,14 +189,14 @@ module.exports = {
}
res.status(200).json({
data: newData.slice((page - 1) * pageSize, page * pageSize),
list: 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))
const { ids = [] } = req.body
database = database.filter((item) => !ids.some((_) => _ === item.id))
res.status(204).end()
},
@@ -221,7 +233,7 @@ module.exports = {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
database = database.filter(item => item.id !== id)
database = database.filter((item) => item.id !== id)
res.status(204).end()
} else {
res.status(200).json(NOTFOUND)
@@ -233,7 +245,7 @@ module.exports = {
const editItem = req.body
let isExist = false
database = database.map(item => {
database = database.map((item) => {
if (item.id === id) {
isExist = true
return Object.assign({}, item, editItem)
+13 -6
View File
@@ -9,7 +9,7 @@ export function queryArray(array, key, value) {
if (!Array.isArray(array)) {
return
}
return array.filter(_ => _[key] === value)
return array.filter((_) => _[key] === value)
}
export function randomNumber(min, max) {
@@ -30,9 +30,19 @@ export function randomAvatar() {
'photo-1551438632-e8c7d9a5d1b7.jpeg',
'photo-1525879000488-bff3b1c387cf.jpeg',
'photo-1487412720507-e7ab37603c6f.jpeg',
'photo-1510227272981-87123e259b17.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`
return `//image.zuiidea.com/${
avatarList[randomNumber(0, avatarList.length - 1)]
}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim`
}
export const waitTime = (time: number = 300) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
}
export const Constant = {
@@ -54,6 +64,3 @@ export const Constant = {
sky: '#c1e0fc',
},
}
export Mock from 'mockjs'
export qs from 'qs'
+91 -80
View File
@@ -1,111 +1,122 @@
{
"name": "antd-admin",
"version": "5.3.0",
"version": "6.0.0-a.1",
"license": "MIT",
"description": "An admin dashboard application demo built upon Ant Design and UmiJS",
"dependencies": {
"@ant-design/icons": "^4.2.2",
"@lingui/react": "^2.8.0",
"antd": "^4.0.0",
"axios": "^0.19.0",
"classnames": "^2.2.6",
"d3-shape": "^1.3.0",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.0",
"draftjs-to-markdown": "^0.6.0",
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.37.0",
"@lingui/core": "^3.13.3",
"@lingui/detect-locale": "^3.13.3",
"@lingui/react": "^3.13.3",
"antd": "^4.20.1",
"axios": "^0.27.2",
"classnames": "^2.3.1",
"dva-model-extend": "^0.1.2",
"echarts": "^4.4.0",
"echarts-for-react": "^2.0.15-beta.1",
"echarts-gl": "^1.1.1",
"echarts-liquidfill": "^2.0.2",
"enquire-js": "^0.2.1",
"highcharts-exporting": "^0.1.7",
"highcharts-more": "^0.1.7",
"echarts": "^5.3.2",
"echarts-for-react": "^3.0.2",
"echarts-gl": "^2.0.9",
"echarts-liquidfill": "^3.1.0",
"json-format": "^1.0.1",
"lodash": "^4.17.11",
"md5": "^2.2.1",
"lodash": "^4.17.21",
"make-plural": "^7.1.0",
"md5": "^2.3.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.1.0",
"prop-types": "^15.7.0",
"qs": "^6.9.0",
"react-adsense": "^0.1.0",
"react-countup": "^4.2.0",
"react-draft-wysiwyg": "^1.13.0",
"react-helmet": "^6.0.0",
"path-to-regexp": "^6.2.0",
"prop-types": "^15.8.1",
"qs": "^6.10.3",
"react": "^18.1.0",
"react-countup": "^6.2.0",
"react-dom": "^18.1.0",
"react-helmet": "^6.1.0",
"react-highcharts": "^16.1.0",
"react-perfect-scrollbar": "^1.5.0",
"recharts": "^2.0.0-beta.3",
"store": "^2.0.0"
"react-perfect-scrollbar": "^1.5.8",
"react-router-dom": "^6.3.0",
"recharts": "^2.1.9",
"store": "^2.0.12"
},
"devDependencies": {
"@lingui/babel-preset-react": "^2.9.0",
"@lingui/cli": "^2.9.0",
"@lingui/loader": "^2.9.0",
"@umijs/preset-react": "^1.4.0",
"babel-eslint": "^10.0.1",
"babel-plugin-dev-expression": "^0.2.1",
"babel-plugin-module-resolver": "^4.0.0",
"cross-env": "^7.0.0",
"eslint": "^7.0.0",
"eslint-config-react-app": "^5.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.14.0",
"eslint-plugin-react-hooks": "^4.0.0",
"husky": "^4.2.0",
"@babel/preset-react": "^7.16.7",
"@lingui/babel-preset-react": "^2.9.2",
"@lingui/cli": "^3.13.3",
"@lingui/loader": "^3.13.3",
"@lingui/macro": "^3.13.3",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"babel-plugin-dev-expression": "^0.2.3",
"babel-plugin-import": "^1.13.5",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^4.1.0",
"cross-env": "^7.0.3",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0",
"husky": "^7.0.4",
"less-vars-to-js": "^1.3.0",
"lint-staged": "^10.0.0",
"lint-staged": "^12.4.1",
"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": "^20.0.0",
"typescript": "^3.8.3",
"umi": "^3.2.0"
"prettier": "^2.6.2",
"stylelint": "^14.8.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^25.0.0",
"typescript": "^4.6.4",
"umi": "4.0.0-canary.20220429.4"
},
"engines": {
"node": ">= 10.0.0"
},
"lint-staged": {
"src/**/*.js": [
"eslint --ext .js --fix",
"npm run prettier",
"git add"
"*.{ts,tsx,js,jsx}": [
"npm run eslint",
"npm run prettier"
],
"**/*.less": [
"stylelint --syntax less",
"npm run prettier",
"git add"
"*.less": [
"npm run stylelint",
"npm run prettier"
]
},
"lingui": {
"fallbackLocale": "en",
"fallbackLocales": {
"default": "en"
},
"sourceLocale": "en",
"localeDir": "src/locales",
"srcPathDirs": [
"src/pages",
"src/layouts",
"src/components",
"src/layouts"
"locales": [
"en",
"zh",
"pt-br"
],
"catalogs": [
{
"path": "src/locales/{locale}/messages",
"include": [
"src"
],
"exclude": [
"**/node_modules/**"
]
}
],
"format": "minimal",
"extractBabelOptions": {
"presets": [
"@umijs/babel-preset-umi"
]
}
"runtimeConfigModule": [
"@/hooks/useIntlProvider",
"i18n"
]
},
"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}'",
"build": "cross-env UMI_ENV=prod umi build",
"test": "umi test",
"analyze": "cross-env ANALYZE=1 umi dev",
"eslint": "eslint --fix 'src/**/*.{ts,tsx,js.jsx}'",
"stylelint": "stylelint src/**/*.less --syntax less",
"prettier": "prettier --write 'src/**/*.{ts,tsx,js.jsx,less}'",
"precommit": "lint-staged",
"add-locale": "lingui add-locale",
"extract": "lingui extract",
View File
-35
View File
@@ -1,35 +0,0 @@
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
-6
View File
@@ -1,6 +0,0 @@
{
"name": "DropOption",
"version": "0.0.0",
"private": true,
"main": "./DropOption.js"
}
-17
View File
@@ -1,17 +0,0 @@
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
-106
View File
@@ -1,106 +0,0 @@
.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;
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Editor",
"version": "0.0.0",
"private": true,
"main": "./Editor.js"
}
-21
View File
@@ -1,21 +0,0 @@
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> {}
-270
View File
@@ -1,270 +0,0 @@
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>
);
}
}
-24
View File
@@ -1,24 +0,0 @@
.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;
}
-17
View File
@@ -1,17 +0,0 @@
---
title: Ellipsis
subtitle: 文本自动省略号
cols: 1
order: 10
---
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
## API
| 参数 | 说明 | 类型 | 默认值 |
| -------------------- | ------------------------------------------------ | ------- | ------ |
| tooltip | 移动到文本展示完整内容的提示 | boolean | - |
| length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - |
| lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` |
| fullWidthRecognition | 是否将全角字符的长度视为 2 来计算字符串长度 | boolean | - |
-13
View File
@@ -1,13 +0,0 @@
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');
});
});
@@ -1,8 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './FilterItem.less'
import styles from './index.less'
const FilterItem = ({ label = '', children }) => {
interface FilterItemProps {
label: string
}
const FilterItem: React.FC<FilterItemProps> = ({ label = '', children }) => {
const labelArray = label.split('')
return (
<div className={styles.filterItem}>
@@ -20,9 +22,4 @@ const FilterItem = ({ label = '', children }) => {
)
}
FilterItem.propTypes = {
label: PropTypes.string,
children: PropTypes.element.isRequired,
}
export default FilterItem
-6
View File
@@ -1,6 +0,0 @@
{
"name": "FilterItem",
"version": "0.0.0",
"private": true,
"main": "./FilterItem.js"
}
-14
View File
@@ -1,14 +0,0 @@
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> {}
-28
View File
@@ -1,28 +0,0 @@
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;
+1 -1
View File
@@ -13,7 +13,7 @@
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
margin-right: 16px;
}
&:hover {
-15
View File
@@ -1,15 +0,0 @@
---
title: GlobalFooter
subtitle: 全局页脚
cols: 1
order: 7
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
| 参数 | 说明 | 类型 | 默认值 |
| --------- | -------- | ---------------------------------------------------------------- | ------ |
| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - |
| copyright | 版权信息 | ReactNode | - |
+40
View File
@@ -0,0 +1,40 @@
import React from 'react'
import styles from './index.less'
interface LinkItem {
key: string
title: React.ReactNode
href?: string
blankTarget?: boolean
}
interface GlobalFooterProps {
links?: LinkItem[]
copyright?: React.ReactNode
}
const GlobalFooter: React.FC<GlobalFooterProps> = ({
links = [],
copyright,
}) => {
return (
<footer className={styles.globalFooter}>
<div className={styles.links}>
{links.map((link) => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
rel="noreferrer"
>
{link.title}
</a>
))}
</div>
{copyright && <div className={styles.copyright}>{copyright}</div>}
</footer>
)
}
export default GlobalFooter
-69
View File
@@ -1,69 +0,0 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb } from 'antd'
import { Link, withRouter } from 'umi'
import { withI18n } from '@lingui/react'
import iconMap from 'utils/iconMap'
const { pathToRegexp } = require('path-to-regexp')
import { queryAncestors } from 'utils'
import styles from './Bread.less'
@withI18n()
@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, i18n } = 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: i18n.t`Not Found`,
},
]
return (
<Breadcrumb className={styles.bread}>
{this.generateBreadcrumbs(paths)}
</Breadcrumb>
)
}
}
Bread.propTypes = {
routeList: PropTypes.array,
}
export default Bread
-16
View File
@@ -1,16 +0,0 @@
.bread {
margin-bottom: 24px;
:global {
.ant-breadcrumb {
display: flex;
align-items: center;
}
}
}
@media (max-width: 767px) {
.bread {
margin-bottom: 12px;
}
}
-170
View File
@@ -1,170 +0,0 @@
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, withI18n } from '@lingui/react'
import { setLocale } from 'utils'
import moment from 'moment'
import classnames from 'classnames'
import config from 'config'
import styles from './Header.less'
const { SubMenu } = Menu
@withI18n()
class Header extends PureComponent {
handleClickMenu = e => {
e.key === 'SignOut' && this.props.onSignOut()
}
render() {
const {
i18n,
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 currentLanguage = languages.find(
item => item.key === i18n._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
-154
View File
@@ -1,154 +0,0 @@
@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;
}
}
-122
View File
@@ -1,122 +0,0 @@
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
-89
View File
@@ -1,89 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Switch, Layout } from 'antd'
import { withI18n, Trans } from '@lingui/react'
import { BulbOutlined } from '@ant-design/icons'
import ScrollBar from '../ScrollBar'
import { config } from 'utils'
import SiderMenu from './Menu'
import styles from './Sider.less'
@withI18n()
class Sider extends PureComponent {
render() {
const {
i18n,
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={i18n.t`Dark`}
unCheckedChildren={i18n.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
-110
View File
@@ -1,110 +0,0 @@
@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;
}
}
-6
View File
@@ -1,6 +0,0 @@
import Header from './Header'
import Menu from './Menu'
import Bread from './Bread'
import Sider from './Sider'
export { Header, Menu, Bread, Sider }
@@ -1,67 +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);
}
}
.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);
}
}
@@ -1,9 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import styles from './Loader.less'
import styles from './index.less'
const Loader = ({ spinning = false, fullScreen }) => {
interface LoaderProps {
spinning?: boolean
fullScreen?: boolean
}
const Loader: React.FC<LoaderProps> = ({ spinning = false, fullScreen }) => {
return (
<div
className={classNames(styles.loader, {
@@ -19,9 +23,4 @@ const Loader = ({ spinning = false, fullScreen }) => {
)
}
Loader.propTypes = {
spinning: PropTypes.bool,
fullScreen: PropTypes.bool,
}
export default Loader
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Loader",
"version": "0.0.0",
"private": true,
"main": "./Loader.js"
}
-33
View File
@@ -1,33 +0,0 @@
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,
}
@@ -1,4 +1,4 @@
@import '~themes/vars.less';
@import '~@/themes/vars.less';
.contentInner {
background: #fff;
+32
View File
@@ -0,0 +1,32 @@
import React from 'react'
import classnames from 'classnames'
import Loader from '../Loader'
import styles from './index.less'
interface IPageProps {
inner?: boolean
loading?: boolean
className?: string
}
const Page: React.FC = (props: IPageProps) => {
const { className, children, loading = false, inner = false } = 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>
)
}
export default Page
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Page",
"version": "0.0.0",
"private": true,
"main": "./Page.js"
}
-11
View File
@@ -1,11 +0,0 @@
import Editor from './Editor'
import FilterItem from './FilterItem'
import DropOption from './DropOption'
import Loader from './Loader'
import ScrollBar from './ScrollBar'
import GlobalFooter from './GlobalFooter'
import Ellipsis from './Ellipsis'
import * as MyLayout from './Layout/index.js'
import Page from './Page'
export { MyLayout, Editor, GlobalFooter, Ellipsis, FilterItem, DropOption, Loader, Page, ScrollBar }
+7
View File
@@ -0,0 +1,7 @@
import FilterItem from './FilterItem'
import Loader from './Loader'
import ScrollBar from './ScrollBar'
import GlobalFooter from './GlobalFooter'
import Page from './Page'
export { GlobalFooter, FilterItem, Loader, ScrollBar, Page }
+1
View File
@@ -0,0 +1 @@
export const LOCALE_LANGUAGE = 'LOCALE_LANGUAGE'
+33
View File
@@ -0,0 +1,33 @@
import { IConfig, ISupportedLocales } from '@/typings'
import { LOCALE_LANGUAGE } from '@/configs/constants'
import checkLanguage from '@/utils/checkLanguage'
import {
detect,
fromUrl,
fromStorage,
fromNavigator,
} from '@lingui/detect-locale'
const language = detect(
checkLanguage(fromUrl('lang')),
checkLanguage(fromStorage(LOCALE_LANGUAGE)),
checkLanguage(fromNavigator()),
'en'
)
export const config: IConfig = {
title: 'Antd Admin',
logo: 'logo.svg',
copyright: 'Ant Design Admin © 2021 zuiidea',
language: language as ISupportedLocales,
apiPrefix: '/api/v1',
/* Layout configuration, specify which layout to use for route. */
layouts: [
{
name: 'primary',
include: [/.*/],
exlude: [/\/login/],
},
],
}
+8
View File
@@ -0,0 +1,8 @@
import menus from './menus'
import menuIcon from './menus/icon'
export * from './default'
export * from './languages'
export * from './constants'
export { menus, menuIcon }
+32
View File
@@ -0,0 +1,32 @@
import { ILanguage, ILanguages, IAntdI18nMap } from '@/typings'
/**
* Countrys flags: https://www.flaticon.com/packs/countrys-flags
*/
export const language: ILanguage = {
en: {
flag: '/america.svg',
value: 'English',
},
zh: {
flag: '/china.svg',
value: '简体中文',
},
'pt-br': {
flag: '/portugal.svg',
value: 'Português',
},
}
export const languages: ILanguages[] = Object.entries(language).map(
([key, value]: any) => ({
key,
...value,
})
)
export const antdI18nMap: IAntdI18nMap = {
zh: 'zh_CN',
en: 'en_US',
'pt-br': 'pt_BR',
}
@@ -13,8 +13,9 @@ import {
BarChartOutlined,
AreaChartOutlined,
} from '@ant-design/icons'
import { IMenuIcon } from '@/typings'
export default {
const menuIcon: IMenuIcon = {
'pay-circle-o': <PayCircleOutlined />,
'shopping-cart': <ShoppingCartOutlined />,
'camera-o': <CameraOutlined />,
@@ -29,3 +30,5 @@ export default {
api: <ApiOutlined />,
edit: <EditOutlined />,
}
export default menuIcon
+22
View File
@@ -0,0 +1,22 @@
import { t } from '@lingui/macro'
/*
* Note:
* Menu items with children need to set a key starting with "/"
* @see https://github.com/umijs/route-utils/blob/master/src/transformRoute/transformRoute.ts#L219
*/
const menus = [
{
path: '/',
name: t`Dashboard`,
icon: 'dashboard',
},
{
path: '/user',
name: t`User`,
icon: 'user',
},
]
export default menus
+8
View File
@@ -0,0 +1,8 @@
import { SettingDrawerProps } from '@ant-design/pro-layout'
const defaultSettings: SettingDrawerProps['settings'] = {
navTheme: 'light',
primaryColor: '#006EFF',
}
export default defaultSettings
+1 -1
View File
@@ -18,7 +18,7 @@ describe('Login', () => {
afterEach(() => page.close())
it('should login with failure', async () => {
await page.waitFor(selector => !!document.querySelector('#username'), {
await page.waitFor((selector) => !!document.querySelector('#username'), {
timeout: 3000,
})
await page.type('#username', 'wrong_user')
+16
View File
@@ -0,0 +1,16 @@
import { useNavigate, useLocation } from 'react-router-dom';
import { useLingui } from '@lingui/react';
import useConfig from './useConfig'
import useIntlProvider from './useIntlProvider'
import useLocalStorage from './useLocalStorage';
import useRequest from './useRequest';
export {
useConfig,
useNavigate,
useLocation,
useRequest,
useLocalStorage,
useIntlProvider,
useLingui,
}
+8
View File
@@ -0,0 +1,8 @@
import { useContext } from 'react'
import { ConfigContext } from '@/utils/context'
const useConfig = () => {
return useContext(ConfigContext)
}
export default useConfig
+47
View File
@@ -0,0 +1,47 @@
import { useEffect, useContext, useState } from 'react';
import { setupI18n } from '@lingui/core';
import { LOCALE_LANGUAGE } from '@/configs/constants';
import { ConfigContext } from '@/utils/context';
import { useLocalStorage } from '@/hooks';
import { antdI18nMap } from '@/configs';
import { SupportedLocales } from '@/typings';
import { ConfigProviderProps } from 'antd/lib/config-provider';
import { en, zh } from 'make-plural/plurals';
export const i18n = setupI18n({
localeData: {
en: { plurals: en },
zh: { plurals: zh },
},
});
const useIntlProvider = () => {
const { language: defaultLocale } = useContext(ConfigContext);
const [language, setLanguage] = useLocalStorage<SupportedLocales>(
LOCALE_LANGUAGE,
defaultLocale
);
const [locale, setLocale] = useState<ConfigProviderProps['locale']>();
useEffect(() => {
import(`@/locales/${language}/messages.json`).then(data => {
i18n.load(language, data.default);
i18n.activate(language);
});
const path = antdI18nMap[language];
import(`antd/lib/locale/${path}.js`).then(data => {
setLocale(data.default);
});
}, [language]);
return {
language,
setLanguage,
locale,
i18n,
};
};
export default useIntlProvider;
+29
View File
@@ -0,0 +1,29 @@
import { useState } from 'react';
const useLocalStorage = <T>(
key: string,
initialValue?: T
): [T, (value: T) => void] => {
let defaultValue = initialValue;
try {
const item = window.localStorage.getItem(key);
defaultValue = item ? JSON.parse(item) : initialValue;
} catch (error) {
defaultValue = initialValue;
}
const [state, setState] = useState(defaultValue as T);
const setValue = (value: T) => {
try {
const valueToStore = value instanceof Function ? value(state) : value;
setState(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [state, setValue];
};
export default useLocalStorage;
+56
View File
@@ -0,0 +1,56 @@
import { useEffect, useState, useCallback } from 'react'
interface Options<R, P, RR> {
onSuccess?: (data: R, params?: P) => void
onError?: (e: Error, params?: P) => void
formatResult?: (data: RR) => R
manual?: boolean
initialData?: R
}
const useFetch = <R = any, P = any, RR = any>(
service: any,
options: Options<R, P, RR>
) => {
const { initialData, manual, formatResult, onSuccess, onError } = options
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<R>(initialData as R)
const [error, setError] = useState<any>(null)
const run = useCallback(
(params?: P) => {
setLoading(true)
return service(params)
.then((result) => {
const resultData = formatResult ? formatResult(result) : result
setData(resultData)
onSuccess && onSuccess(resultData, params)
})
.catch((result) => {
setError(result || new Error())
onError && onError(result, params)
})
.finally(() => {
setLoading(false)
})
},
[formatResult, onError, onSuccess, service]
)
useEffect(() => {
if (manual) {
return
}
run()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
run,
data,
error,
loading,
}
}
export default useFetch
+30
View File
@@ -0,0 +1,30 @@
import { config } from '@/configs/default'
import { setupI18n, I18n } from '@lingui/core'
import { en, zh, pt } from 'make-plural/plurals'
import { messages as zhMessages } from '@/locales/zh/messages.po'
import { messages as enMessages } from '@/locales/en/messages.po'
import { messages as ptMessages } from '@/locales/pt-br/messages.po'
const i18nBase: I18n = setupI18n()
i18nBase.loadLocaleData({
en: {
plurals: en,
},
zh: {
plurals: zh,
},
'pt-br': {
plurals: pt,
},
})
i18nBase.load({
zh: zhMessages,
en: enMessages,
'pt-br': ptMessages,
})
i18nBase.activate(config.language)
export const i18n = i18nBase
-55
View File
@@ -1,55 +0,0 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'umi'
import { Helmet } from 'react-helmet'
import { Loader } from 'components'
import { queryLayout } from 'utils'
import NProgress from 'nprogress'
import config from 'utils/config'
import { withRouter } from 'umi'
import PublicLayout from './PublicLayout'
import PrimaryLayout from './PrimaryLayout'
import './BaseLayout.less'
const LayoutMap = {
primary: PrimaryLayout,
public: PublicLayout,
}
@withRouter
@connect(({ loading }) => ({ loading }))
class BaseLayout extends PureComponent {
previousPath = ''
render() {
const { loading, children, location } = this.props
const Container = LayoutMap[queryLayout(config.layouts, location.pathname)]
const currentPath = location.pathname + location.search
if (currentPath !== this.previousPath) {
NProgress.start()
}
if (!loading.global) {
NProgress.done()
this.previousPath = currentPath
}
return (
<Fragment>
<Helmet>
<title>{config.siteName}</title>
</Helmet>
<Loader fullScreen spinning={loading.effects['app/query']} />
<Container>{children}</Container>
</Fragment>
)
}
}
BaseLayout.propTypes = {
loading: PropTypes.object,
}
export default BaseLayout
+11 -11
View File
@@ -1,44 +1,44 @@
@import '~themes/vars.less';
@import '~themes/index.less';
@import '~@/themes/vars.less';
@import '~@/themes/index.less';
:global {
#nprogress {
pointer-events: none;
.bar {
background: @primary-color;
position: fixed;
z-index: 2048;
top: 0;
left: 0;
right: 0;
left: 0;
z-index: 2048;
width: 100%;
height: 2px;
background: @primary-color;
}
.peg {
display: block;
position: absolute;
right: 0;
display: block;
width: 100px;
height: 100%;
box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
opacity: 1;
}
.spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
z-index: 1031;
display: block;
}
.spinner-icon {
box-sizing: border-box;
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: @primary-color;
border-left-color: @primary-color;
@@ -51,8 +51,8 @@
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
overflow: hidden;
#nprogress {
.bar,
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import { useLocation } from '@/hooks'
import { queryLayout } from '@/utils'
import { config } from '@/configs'
import { Outlet } from 'umi'
import PublicLayout from './PublicLayout'
import PrimaryLayout from './PrimaryLayout/index'
import './BaseLayout.less'
const LayoutMap = {
primary: PrimaryLayout,
public: PublicLayout,
}
const BaseLayout: React.FC = (props) => {
const location = useLocation()
const Container = LayoutMap[queryLayout(config.layouts, location.pathname)]
return <Container><Outlet /></Container>
}
export default BaseLayout
-168
View File
@@ -1,168 +0,0 @@
/* global window */
/* global document */
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'umi'
import { connect } from 'umi'
import { MyLayout, GlobalFooter } from 'components'
import { BackTop, Layout, Drawer } from 'antd'
import { enquireScreen, unenquireScreen } from 'enquire-js'
const { pathToRegexp } = require("path-to-regexp")
import { config, getLocale } from 'utils'
import Error from '../pages/404'
import styles from './PrimaryLayout.less'
import store from 'store'
const { Content } = Layout
const { Header, Bread, Sider } = MyLayout
@withRouter
@connect(({ app, loading }) => ({ app, loading }))
class PrimaryLayout extends PureComponent {
state = {
isMobile: false,
}
componentDidMount() {
this.enquireHandler = enquireScreen(mobile => {
const { isMobile } = this.state
if (isMobile !== mobile) {
this.setState({
isMobile: mobile,
})
}
})
}
componentWillUnmount() {
unenquireScreen(this.enquireHandler)
}
onCollapseChange = collapsed => {
this.props.dispatch({
type: 'app/handleCollapseChange',
payload: collapsed,
})
}
render() {
const { app, location, dispatch, children } = this.props
const { theme, collapsed, notifications } = app
const user = store.get('user') || {}
const permissions = store.get('permissions') || {}
const routeList = store.get('routeList') || []
const { isMobile } = this.state
const { onCollapseChange } = this
// Localized route name.
const lang = getLocale()
const newRouteList =
lang !== 'en'
? routeList.map(item => {
const { name, ...other } = item
return {
...other,
name: (item[lang] || {}).name || name,
}
})
: routeList
// Find a route that matches the pathname.
const currentRoute = newRouteList.find(
_ => _.route && pathToRegexp(_.route).exec(location.pathname)
)
// Query whether you have permission to enter this page
const hasPermission = currentRoute
? permissions.visit.includes(currentRoute.id)
: false
// MenuParentId is equal to -1 is not a available menu.
const menus = newRouteList.filter(_ => _.menuParentId !== '-1')
const headerProps = {
menus,
collapsed,
notifications,
onCollapseChange,
avatar: user.avatar,
username: user.username,
fixed: config.fixedHeader,
onAllNotificationsRead() {
dispatch({ type: 'app/allNotificationsRead' })
},
onSignOut() {
dispatch({ type: 'app/signOut' })
},
}
const siderProps = {
theme,
menus,
isMobile,
collapsed,
onCollapseChange,
onThemeChange(theme) {
dispatch({
type: 'app/handleThemeChange',
payload: theme,
})
},
}
return (
<Fragment>
<Layout>
{isMobile ? (
<Drawer
maskClosable
closable={false}
onClose={onCollapseChange.bind(this, !collapsed)}
visible={!collapsed}
placement="left"
width={200}
style={{
padding: 0,
height: '100vh',
}}
>
<Sider {...siderProps} collapsed={false} />
</Drawer>
) : (
<Sider {...siderProps} />
)}
<div
className={styles.container}
style={{ paddingTop: config.fixedHeader ? 72 : 0 }}
id="primaryLayout"
>
<Header {...headerProps} />
<Content className={styles.content}>
<Bread routeList={newRouteList} />
{hasPermission ? children : <Error />}
</Content>
<BackTop
className={styles.backTop}
target={() => document.querySelector('#primaryLayout')}
/>
<GlobalFooter
className={styles.footer}
copyright={config.copyright}
/>
</div>
</Layout>
</Fragment>
)
}
}
PrimaryLayout.propTypes = {
children: PropTypes.element.isRequired,
location: PropTypes.object,
dispatch: PropTypes.func,
app: PropTypes.object,
loading: PropTypes.object,
}
export default PrimaryLayout
@@ -0,0 +1,69 @@
import React from 'react'
import { Dropdown, Menu, Avatar } from 'antd'
import { language, languages } from '@/configs'
import { ISupportedLocales } from '@/typings'
import { Trans } from '@lingui/macro'
import { history } from 'umi'
import { useRequest, useConfig } from '@/hooks'
import { logoutUser } from '@/services'
import styles from './index.less'
const { SubMenu } = Menu
const NotFoundPage: React.FC = () => {
const { language: locale, setLanguage, userInfo } = useConfig()
const currentLanguage = language[locale]
const { run: runLogoutUser } = useRequest(logoutUser, {
manual: true,
onSuccess: () => history.push('/login'),
})
return (
<>
<Dropdown
overlay={
<Menu
selectedKeys={[currentLanguage.value]}
onClick={(data) => {
setLanguage(data.key as ISupportedLocales)
}}
>
{languages.map((item) => (
<Menu.Item key={item.key}>
<Avatar
size="small"
style={{ marginRight: 8 }}
src={item.flag}
/>
{item.value}
</Menu.Item>
))}
</Menu>
}
>
<div className={styles.headerButton}>
<Avatar size="small" src={currentLanguage.flag} />
</div>
</Dropdown>
<Menu key="user" mode="horizontal">
<SubMenu
title={
<>
<span style={{ color: '#999', marginRight: 4 }}>
<Trans>Hi,</Trans>
</span>
<span>{userInfo?.username}</span>
<Avatar style={{ marginLeft: 8 }} src={userInfo?.avatar} />
</>
}
>
<Menu.Item key="SignOut" onClick={() => runLogoutUser()}>
<Trans>Sign out</Trans>
</Menu.Item>
</SubMenu>
</Menu>
</>
)
}
export default NotFoundPage
@@ -1,5 +1,9 @@
@import '~themes/vars.less';
@import '~@/themes/vars.less';
:global {
#root {
height: 100%;
}
}
.backTop {
right: 50px;
+77
View File
@@ -0,0 +1,77 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import ProBasicLayout, {
getMenuData,
MenuDataItem,
SettingDrawerProps,
} from '@ant-design/pro-layout'
import { Link, Outlet } from 'umi'
import { useLocation } from '@/hooks'
import { menus, menuIcon, config } from '@/configs'
import defaultSettings from '@/defaultSettings'
import type { MenuProps } from 'antd/es/menu'
import HeaderRightContent from './HeaderRightContent'
const renderMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
menus.map(({ icon, children, ...item }) => ({
...item,
icon: icon && menuIcon[icon as string],
children: children && renderMenuItem(children),
}))
const PrimaryLayout: React.FC = (props) => {
const location = useLocation()
const [settings] = useState<SettingDrawerProps['settings']>(defaultSettings)
const [openKeys, setOpenKeys] = useState<string[]>([])
const [selectedKeys, setSelectedKeys] = useState<string[]>(['/'])
const { pathname } = location
const { breadcrumbMap, menuData } = useMemo(() => getMenuData(menus), [])
useEffect(() => {
const select = breadcrumbMap.get(pathname)
if (select) {
setOpenKeys((select as MenuDataItem)['pro_layout_parentKeys'])
setSelectedKeys([(select as MenuDataItem)['key'] as string])
}
}, [breadcrumbMap, pathname])
const menuDataRender = useCallback(() => renderMenuItem(menuData), [menuData])
const menuItemRender = useCallback((menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || !menuItemProps.path) {
return defaultDom
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>
}, [])
const handleOnOpenChange = useCallback(
(keys) => setOpenKeys(keys as string[]),
[]
)
const menuProps = useMemo<MenuProps>(
() => ({
selectedKeys,
openKeys,
onOpenChange: handleOnOpenChange,
}),
[handleOnOpenChange, openKeys, selectedKeys]
)
return (
<>
<ProBasicLayout
{...settings}
title={config.title}
logo={config.logo}
menuDataRender={menuDataRender}
menuItemRender={menuItemRender}
menuProps={menuProps}
rightContentRender={() => <HeaderRightContent />}
>
<Outlet />
</ProBasicLayout>
</>
)
}
export default PrimaryLayout
-3
View File
@@ -1,3 +0,0 @@
export default ({ children }) => {
return children
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import { Outlet } from 'umi'
const PublicLayout: React.FC = (props) => {
return <><Outlet /></>
}
export default PublicLayout
-81
View File
@@ -1,81 +0,0 @@
import React, { Component } from 'react'
import { withRouter } from 'umi'
import { ConfigProvider } from 'antd'
import { I18nProvider } from '@lingui/react'
import { getLocale } from 'utils'
const { i18n } = require('../../src/utils/config')
import zh_CN from 'antd/lib/locale-provider/zh_CN'
import en_US from 'antd/lib/locale-provider/en_US'
import pt_BR from 'antd/lib/locale-provider/pt_BR'
import BaseLayout from './BaseLayout'
const languages = {
zh: zh_CN,
en: en_US,
'pt-br': pt_BR,
}
const { defaultLanguage } = i18n
@withRouter
class Layout extends Component {
state = {
catalogs: {},
}
language = defaultLanguage
componentDidMount() {
const language = getLocale()
this.language = language
language && this.loadCatalog(language)
}
shouldComponentUpdate(nextProps, nextState) {
const language = getLocale()
const preLanguage = this.language
const { catalogs } = nextState
if (preLanguage !== language && !catalogs[language]) {
language && this.loadCatalog(language)
this.language = language
return false
}
this.language = language
return true
}
loadCatalog = async language => {
const catalog = await import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`@lingui/loader!../locales/${language}/messages.json`
)
this.setState(state => ({
catalogs: {
...state.catalogs,
[language]: catalog,
},
}))
}
render() {
const { children } = this.props
const { catalogs } = this.state
let language = getLocale()
// If the language pack is not loaded or is loading, use the default language
if (!catalogs[language]) language = defaultLanguage
return (
<ConfigProvider locale={languages[language]}>
<I18nProvider language={language} catalogs={catalogs}>
<BaseLayout>{children}</BaseLayout>
</I18nProvider>
</ConfigProvider>
)
}
}
export default Layout
+22
View File
@@ -0,0 +1,22 @@
:global {
#root {
height: 100%;
}
}
.headerButton {
display: flex;
align-items: center;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.headerFlag {
margin-right: 8px;
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react'
import { history } from 'umi'
import { ConfigProvider } from 'antd'
import { I18nProvider } from '@lingui/react'
import {
useIntlProvider,
useRequest,
} from '@/hooks'
// @ts-ignore
import { Outlet } from 'umi'
import { ConfigContext } from '@/utils/context'
import request from '@/utils/request'
import { queryUserInfo, IUserInfo } from '@/services'
import BaseLayout from './BaseLayout'
const Layout: React.FC = (props) => {
const { language, setLanguage, i18n, locale } = useIntlProvider()
const {
data: userInfo,
run: runQueryUserInfo,
loading,
} = useRequest<IUserInfo>(queryUserInfo, {
onError: (data: any) => {
if (data.statusCode === 401) {
history.push('/login')
}
},
})
const globalConfig = {
language,
setLanguage,
userInfo: userInfo as IUserInfo,
queryUserInfo: runQueryUserInfo,
}
if (loading) {
return <div>loading</div>
}
return (
<ConfigContext.Provider value={globalConfig}>
<I18nProvider i18n={i18n}>
<ConfigProvider locale={locale}>
<BaseLayout><Outlet /></BaseLayout>
</ConfigProvider>
</I18nProvider>
</ConfigContext.Provider>
)
}
export default Layout
+26
View File
@@ -0,0 +1,26 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2021-04-04 17:32+0800\n"
"Mime-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/pages/login/index.tsx:14
msgid "Sign in"
msgstr "Sign in"
#: src/pages/login/index.tsx:18
msgid "Username"
msgstr "Username"
#: src/pages/login/index.tsx:22
msgid "Password"
msgstr "Password"
+20
View File
@@ -0,0 +1,20 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2021-04-05 15:59+0800\n"
"Mime-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: pt-br\n"
#: src/pages/login/index.tsx:14
msgid "Sign in"
msgstr ""
#: src/pages/login/index.tsx:18
msgid "Username"
msgstr ""
#: src/pages/login/index.tsx:22
msgid "Password"
msgstr ""
+26
View File
@@ -0,0 +1,26 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2021-04-04 17:32+0800\n"
"Mime-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: zh\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/pages/login/index.tsx:14
msgid "Sign in"
msgstr ""
#: src/pages/login/index.tsx:18
msgid "Username"
msgstr ""
#: src/pages/login/index.tsx:22
msgid "Password"
msgstr ""
-160
View File
@@ -1,160 +0,0 @@
/* global window */
import { history } from 'umi'
import { stringify } from 'qs'
import store from 'store'
const { pathToRegexp } = require("path-to-regexp")
import { ROLE_TYPE } from 'utils/constant'
import { queryLayout } from 'utils'
import { CANCEL_REQUEST_MESSAGE } from 'utils/constant'
import api from 'api'
import config from 'config'
const { queryRouteList, logoutUser, queryUserInfo } = api
const goDashboard = () => {
if (pathToRegexp(['/', '/login']).exec(window.location.pathname)) {
history.push({
pathname: '/dashboard',
})
}
}
export default {
namespace: 'app',
state: {
routeList: [
{
id: '1',
icon: 'laptop',
name: 'Dashboard',
zhName: '仪表盘',
router: '/dashboard',
},
],
locationPathname: '',
locationQuery: {},
theme: store.get('theme') || 'light',
collapsed: store.get('collapsed') || false,
notifications: [
{
title: 'New User is registered.',
date: new Date(Date.now() - 10000000),
},
{
title: 'Application has been approved.',
date: new Date(Date.now() - 50000000),
},
],
},
subscriptions: {
setup({ dispatch }) {
dispatch({ type: 'query' })
},
setupHistory({ dispatch, history }) {
history.listen(location => {
dispatch({
type: 'updateState',
payload: {
locationPathname: location.pathname,
locationQuery: location.query,
},
})
})
},
setupRequestCancel({ history }) {
history.listen(() => {
const { cancelRequest = new Map() } = window
cancelRequest.forEach((value, key) => {
if (value.pathname !== window.location.pathname) {
value.cancel(CANCEL_REQUEST_MESSAGE)
cancelRequest.delete(key)
}
})
})
},
},
effects: {
*query({ payload }, { call, put, select }) {
// store isInit to prevent query trigger by refresh
const isInit = store.get('isInit')
if (isInit) {
goDashboard()
return
}
const { locationPathname } = yield select(_ => _.app)
const { success, user } = yield call(queryUserInfo, payload)
if (success && user) {
const { list } = yield call(queryRouteList)
const { permissions } = user
let routeList = list
if (
permissions.role === ROLE_TYPE.ADMIN ||
permissions.role === ROLE_TYPE.DEVELOPER
) {
permissions.visit = list.map(item => item.id)
} else {
routeList = list.filter(item => {
const cases = [
permissions.visit.includes(item.id),
item.mpid
? permissions.visit.includes(item.mpid) || item.mpid === '-1'
: true,
item.bpid ? permissions.visit.includes(item.bpid) : true,
]
return cases.every(_ => _)
})
}
store.set('routeList', routeList)
store.set('permissions', permissions)
store.set('user', user)
store.set('isInit', true)
goDashboard()
} else if (queryLayout(config.layouts, locationPathname) !== 'public') {
history.push({
pathname: '/login',
search: stringify({
from: locationPathname,
}),
})
}
},
*signOut({ payload }, { call, put }) {
const data = yield call(logoutUser)
if (data.success) {
store.set('routeList', [])
store.set('permissions', { visit: [] })
store.set('user', {})
store.set('isInit', false)
yield put({ type: 'query' })
} else {
throw data
}
},
},
reducers: {
updateState(state, { payload }) {
return {
...state,
...payload,
}
},
handleThemeChange(state, { payload }) {
store.set('theme', payload)
state.theme = payload
},
handleCollapseChange(state, { payload }) {
store.set('collapsed', payload)
state.collapsed = payload
},
allNotificationsRead(state) {
state.notifications = []
},
},
}
+5 -10
View File
@@ -1,15 +1,10 @@
import React from 'react'
import { FrownOutlined } from '@ant-design/icons'
import { Page } from 'components'
import styles from './404.less'
const Error = () => (
<Page inner>
<div className={styles.error}>
<FrownOutlined />
<h1>404 Not Found</h1>
</div>
</Page>
const NotFoundPage: React.FC = () => (
<div className={styles.error}>
<h1>404 Not Found</h1>
</div>
)
export default Error
export default NotFoundPage
@@ -1,321 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
require('echarts/map/js/china.js')
const AirportCoordComponent = () => {
let geoCoordMap = {
上海: [121.4648, 31.2891],
东莞: [113.8953, 22.901],
东营: [118.7073, 37.5513],
中山: [113.4229, 22.478],
临汾: [111.4783, 36.1615],
临沂: [118.3118, 35.2936],
丹东: [124.541, 40.4242],
丽水: [119.5642, 28.1854],
乌鲁木齐: [87.9236, 43.5883],
佛山: [112.8955, 23.1097],
保定: [115.0488, 39.0948],
兰州: [103.5901, 36.3043],
包头: [110.3467, 41.4899],
北京: [116.4551, 40.2539],
北海: [109.314, 21.6211],
南京: [118.8062, 31.9208],
南宁: [108.479, 23.1152],
南昌: [116.0046, 28.6633],
南通: [121.1023, 32.1625],
厦门: [118.1689, 24.6478],
台州: [121.1353, 28.6688],
合肥: [117.29, 32.0581],
呼和浩特: [111.4124, 40.4901],
咸阳: [108.4131, 34.8706],
哈尔滨: [127.9688, 45.368],
唐山: [118.4766, 39.6826],
嘉兴: [120.9155, 30.6354],
大同: [113.7854, 39.8035],
大连: [122.2229, 39.4409],
天津: [117.4219, 39.4189],
太原: [112.3352, 37.9413],
威海: [121.9482, 37.1393],
宁波: [121.5967, 29.6466],
宝鸡: [107.1826, 34.3433],
宿迁: [118.5535, 33.7775],
常州: [119.4543, 31.5582],
广州: [113.5107, 23.2196],
廊坊: [116.521, 39.0509],
延安: [109.1052, 36.4252],
张家口: [115.1477, 40.8527],
徐州: [117.5208, 34.3268],
德州: [116.6858, 37.2107],
惠州: [114.6204, 23.1647],
成都: [103.9526, 30.7617],
扬州: [119.4653, 32.8162],
承德: [117.5757, 41.4075],
拉萨: [91.1865, 30.1465],
无锡: [120.3442, 31.5527],
日照: [119.2786, 35.5023],
昆明: [102.9199, 25.4663],
杭州: [119.5313, 29.8773],
枣庄: [117.323, 34.8926],
柳州: [109.3799, 24.9774],
株洲: [113.5327, 27.0319],
武汉: [114.3896, 30.6628],
汕头: [117.1692, 23.3405],
江门: [112.6318, 22.1484],
沈阳: [123.1238, 42.1216],
沧州: [116.8286, 38.2104],
河源: [114.917, 23.9722],
泉州: [118.3228, 25.1147],
泰安: [117.0264, 36.0516],
泰州: [120.0586, 32.5525],
济南: [117.1582, 36.8701],
济宁: [116.8286, 35.3375],
海口: [110.3893, 19.8516],
淄博: [118.0371, 36.6064],
淮安: [118.927, 33.4039],
深圳: [114.5435, 22.5439],
清远: [112.9175, 24.3292],
温州: [120.498, 27.8119],
渭南: [109.7864, 35.0299],
湖州: [119.8608, 30.7782],
湘潭: [112.5439, 27.7075],
滨州: [117.8174, 37.4963],
潍坊: [119.0918, 36.524],
烟台: [120.7397, 37.5128],
玉溪: [101.9312, 23.8898],
珠海: [113.7305, 22.1155],
盐城: [120.2234, 33.5577],
盘锦: [121.9482, 41.0449],
石家庄: [114.4995, 38.1006],
福州: [119.4543, 25.9222],
秦皇岛: [119.2126, 40.0232],
绍兴: [120.564, 29.7565],
聊城: [115.9167, 36.4032],
肇庆: [112.1265, 23.5822],
舟山: [122.2559, 30.2234],
苏州: [120.6519, 31.3989],
莱芜: [117.6526, 36.2714],
菏泽: [115.6201, 35.2057],
营口: [122.4316, 40.4297],
葫芦岛: [120.1575, 40.578],
衡水: [115.8838, 37.7161],
衢州: [118.6853, 28.8666],
西宁: [101.4038, 36.8207],
西安: [109.1162, 34.2004],
贵阳: [106.6992, 26.7682],
连云港: [119.1248, 34.552],
邢台: [114.8071, 37.2821],
邯郸: [114.4775, 36.535],
郑州: [113.4668, 34.6234],
鄂尔多斯: [108.9734, 39.2487],
重庆: [107.7539, 30.1904],
金华: [120.0037, 29.1028],
铜川: [109.0393, 35.1947],
银川: [106.3586, 38.1775],
镇江: [119.4763, 31.9702],
长春: [125.8154, 44.2584],
长沙: [113.0823, 28.2568],
长治: [112.8625, 36.4746],
阳泉: [113.4778, 38.0951],
青岛: [120.4651, 36.3373],
韶关: [113.7964, 24.7028],
}
let BJData = [
[{ name: '北京' }, { name: '上海', value: 95 }],
[{ name: '北京' }, { name: '广州', value: 90 }],
[{ name: '北京' }, { name: '大连', value: 80 }],
[{ name: '北京' }, { name: '南宁', value: 70 }],
[{ name: '北京' }, { name: '南昌', value: 60 }],
[{ name: '北京' }, { name: '拉萨', value: 50 }],
[{ name: '北京' }, { name: '长春', value: 40 }],
[{ name: '北京' }, { name: '包头', value: 30 }],
[{ name: '北京' }, { name: '重庆', value: 20 }],
[{ name: '北京' }, { name: '常州', value: 10 }],
]
let SHData = [
[{ name: '上海' }, { name: '包头', value: 95 }],
[{ name: '上海' }, { name: '昆明', value: 90 }],
[{ name: '上海' }, { name: '广州', value: 80 }],
[{ name: '上海' }, { name: '郑州', value: 70 }],
[{ name: '上海' }, { name: '长春', value: 60 }],
[{ name: '上海' }, { name: '重庆', value: 50 }],
[{ name: '上海' }, { name: '长沙', value: 40 }],
[{ name: '上海' }, { name: '北京', value: 30 }],
[{ name: '上海' }, { name: '丹东', value: 20 }],
[{ name: '上海' }, { name: '大连', value: 10 }],
]
let GZData = [
[{ name: '广州' }, { name: '福州', value: 95 }],
[{ name: '广州' }, { name: '太原', value: 90 }],
[{ name: '广州' }, { name: '长春', value: 80 }],
[{ name: '广州' }, { name: '重庆', value: 70 }],
[{ name: '广州' }, { name: '西安', value: 60 }],
[{ name: '广州' }, { name: '成都', value: 50 }],
[{ name: '广州' }, { name: '常州', value: 40 }],
[{ name: '广州' }, { name: '北京', value: 30 }],
[{ name: '广州' }, { name: '北海', value: 20 }],
[{ name: '广州' }, { name: '海口', value: 10 }],
]
let planePath =
'path://M1705.06,1318.313v-89.254l-319.9-221.799l0.073-208.063c0.521-84.662-26.629-121.796-63.961-121.491c-37.332-0.305-64.482,36.829-63.961,121.491l0.073,208.063l-319.9,221.799v89.254l330.343-157.288l12.238,241.308l-134.449,92.931l0.531,42.034l175.125-42.917l175.125,42.917l0.531-42.034l-134.449-92.931l12.238-241.308L1705.06,1318.313z'
let convertData = function(data) {
let res = []
for (let i = 0; i < data.length; i += 1) {
let dataItem = data[i]
let fromCoord = geoCoordMap[dataItem[0].name]
let toCoord = geoCoordMap[dataItem[1].name]
if (fromCoord && toCoord) {
res.push({
fromName: dataItem[0].name,
toName: dataItem[1].name,
coords: [fromCoord, toCoord],
})
}
}
return res
}
let color = ['#a6c84c', '#ffa022', '#46bee9']
let series = []
;[['北京', BJData], ['上海', SHData], ['广州', GZData]].forEach((item, i) => {
series.push(
{
name: `${item[0]} Top10`,
type: 'lines',
zlevel: 1,
effect: {
show: true,
period: 6,
trailLength: 0.7,
color: '#fff',
symbolSize: 3,
},
lineStyle: {
normal: {
color: color[i],
width: 0,
curveness: 0.2,
},
},
data: convertData(item[1]),
},
{
name: `${item[0]} Top10`,
type: 'lines',
zlevel: 2,
symbol: ['none', 'arrow'],
symbolSize: 10,
effect: {
show: true,
period: 6,
trailLength: 0,
symbol: planePath,
symbolSize: 15,
},
lineStyle: {
normal: {
color: color[i],
width: 1,
opacity: 0.6,
curveness: 0.2,
},
},
data: convertData(item[1]),
},
{
name: `${item[0]} Top10`,
type: 'effectScatter',
coordinateSystem: 'geo',
zlevel: 2,
rippleEffect: {
brushType: 'stroke',
},
label: {
normal: {
show: true,
position: 'right',
formatter: '{b}',
},
},
symbolSize(val) {
return val[2] / 8
},
itemStyle: {
normal: {
color: color[i],
},
},
data: item[1].map(dataItem => {
return {
name: dataItem[1].name,
value: geoCoordMap[dataItem[1].name].concat([dataItem[1].value]),
}
}),
}
)
})
let option = {
backgroundColor: '#404a59',
title: {
text: '模拟迁徙',
subtext: '数据纯属虚构',
left: 'center',
textStyle: {
color: '#fff',
},
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
top: 'bottom',
left: 'right',
data: ['北京 Top10', '上海 Top10', '广州 Top10'],
textStyle: {
color: '#fff',
},
selectedMode: 'single',
},
geo: {
map: 'china',
label: {
emphasis: {
show: false,
},
},
roam: true,
itemStyle: {
normal: {
areaColor: '#323c48',
borderColor: '#404a59',
},
emphasis: {
areaColor: '#2a333d',
},
},
},
series,
}
return (
<div className="examples">
<div className="parent">
<label> render a airport chart. </label>
<ReactEcharts
option={option}
style={{ height: '700px', width: '100%' }}
className="react_for_echarts"
/>
</div>
</div>
)
}
export default AirportCoordComponent
@@ -1,165 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import echarts from 'echarts'
const currentData = [
[
[28604, 77, 17096869, 'Australia', 1990],
[31163, 77.4, 27662440, 'Canada', 1990],
[1516, 68, 1154605773, 'China', 1990],
[13670, 74.7, 10582082, 'Cuba', 1990],
[28599, 75, 4986705, 'Finland', 1990],
[29476, 77.1, 56943299, 'France', 1990],
[31476, 75.4, 78958237, 'Germany', 1990],
[28666, 78.1, 254830, 'Iceland', 1990],
[1777, 57.7, 870601776, 'India', 1990],
[29550, 79.1, 122249285, 'Japan', 1990],
[2076, 67.9, 20194354, 'North Korea', 1990],
[12087, 72, 42972254, 'South Korea', 1990],
[24021, 75.4, 3397534, 'New Zealand', 1990],
[43296, 76.8, 4240375, 'Norway', 1990],
[10088, 70.8, 38195258, 'Poland', 1990],
[19349, 69.6, 147568552, 'Russia', 1990],
[10670, 67.3, 53994605, 'Turkey', 1990],
[26424, 75.7, 57110117, 'United Kingdom', 1990],
[37062, 75.4, 252847810, 'United States', 1990],
],
[
[44056, 81.8, 23968973, 'Australia', 2015],
[43294, 81.7, 35939927, 'Canada', 2015],
[13334, 76.9, 1376048943, 'China', 2015],
[21291, 78.5, 11389562, 'Cuba', 2015],
[38923, 80.8, 5503457, 'Finland', 2015],
[37599, 81.9, 64395345, 'France', 2015],
[44053, 81.1, 80688545, 'Germany', 2015],
[42182, 82.8, 329425, 'Iceland', 2015],
[5903, 66.8, 1311050527, 'India', 2015],
[36162, 83.5, 126573481, 'Japan', 2015],
[1390, 71.4, 25155317, 'North Korea', 2015],
[34644, 80.7, 50293439, 'South Korea', 2015],
[34186, 80.6, 4528526, 'New Zealand', 2015],
[64304, 81.6, 5210967, 'Norway', 2015],
[24787, 77.3, 38611794, 'Poland', 2015],
[23038, 73.13, 143456918, 'Russia', 2015],
[19360, 76.5, 78665830, 'Turkey', 2015],
[38225, 81.4, 64715810, 'United Kingdom', 2015],
[53354, 79.1, 321773631, 'United States', 2015],
],
]
const option = {
backgroundColor: new echarts.graphic.RadialGradient(0.3, 0.3, 0.8, [
{
offset: 0,
color: '#f7f8fa',
},
{
offset: 1,
color: '#cdd0d5',
},
]),
title: {
text: '1990 与 2015 年各国家人均寿命与 GDP',
},
legend: {
right: 10,
data: ['1990', '2015'],
},
xAxis: {
splitLine: {
lineStyle: {
type: 'dashed',
},
},
},
yAxis: {
splitLine: {
lineStyle: {
type: 'dashed',
},
},
scale: true,
},
series: [
{
name: '1990',
data: currentData[0],
type: 'scatter',
symbolSize(data) {
return Math.sqrt(data[2]) / 5e2
},
label: {
emphasis: {
show: true,
formatter(param) {
return param.data[3]
},
position: 'top',
},
},
itemStyle: {
normal: {
shadowBlur: 10,
shadowColor: 'rgba(120, 36, 50, 0.5)',
shadowOffsetY: 5,
color: new echarts.graphic.RadialGradient(0.4, 0.3, 1, [
{
offset: 0,
color: 'rgb(251, 118, 123)',
},
{
offset: 1,
color: 'rgb(204, 46, 72)',
},
]),
},
},
},
{
name: '2015',
data: currentData[1],
type: 'scatter',
symbolSize(data) {
return Math.sqrt(data[2]) / 5e2
},
label: {
emphasis: {
show: true,
formatter(param) {
return param.data[3]
},
position: 'top',
},
},
itemStyle: {
normal: {
shadowBlur: 10,
shadowColor: 'rgba(25, 100, 150, 0.5)',
shadowOffsetY: 5,
color: new echarts.graphic.RadialGradient(0.4, 0.3, 1, [
{
offset: 0,
color: 'rgb(129, 227, 238)',
},
{
offset: 1,
color: 'rgb(25, 183, 207)',
},
]),
},
},
},
],
}
const BubbleGradientComponent = () => {
return (
<ReactEcharts
option={option}
style={{ height: '700px', width: '100%' }}
className="react_for_echarts"
/>
)
}
export default BubbleGradientComponent
@@ -1,193 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import echarts from 'echarts'
const CalendarComponent = () => {
const getVirtulData = year => {
year = year || '2017'
let date = +echarts.number.parseDate(`${year}-01-01`)
let end = +echarts.number.parseDate(`${+year + 1}-01-01`)
let dayTime = 3600 * 24 * 1000
let data = []
for (let time = date; time < end; time += dayTime) {
data.push([
echarts.format.formatTime('yyyy-MM-dd', time),
Math.floor(Math.random() * 1000),
])
}
return data
}
let graphData = [
[1485878400000, 260],
[1486137600000, 200],
[1486569600000, 279],
[1486915200000, 847],
[1487347200000, 241],
[1487779200000, 411],
[1488124800000, 985],
]
let links = graphData.map((item, idx) => {
return {
source: idx,
target: idx + 1,
}
})
links.pop()
const option = {
tooltip: {
position: 'top',
},
visualMap: [
{
min: 0,
max: 1000,
calculable: true,
seriesIndex: [2, 3, 4],
orient: 'horizontal',
left: '55%',
bottom: 20,
},
{
min: 0,
max: 1000,
inRange: {
color: ['grey'],
opacity: [0, 0.3],
},
controller: {
inRange: {
opacity: [0.3, 0.6],
},
outOfRange: {
color: '#ccc',
},
},
calculable: true,
seriesIndex: [1],
orient: 'horizontal',
left: '10%',
bottom: 20,
},
],
calendar: [
{
orient: 'vertical',
yearLabel: {
margin: 40,
},
monthLabel: {
nameMap: 'cn',
margin: 20,
},
dayLabel: {
firstDay: 1,
nameMap: 'cn',
},
cellSize: 40,
range: '2017-02',
},
{
orient: 'vertical',
yearLabel: {
margin: 40,
},
monthLabel: {
margin: 20,
},
cellSize: 40,
left: 460,
range: '2017-01',
},
{
orient: 'vertical',
yearLabel: {
margin: 40,
},
monthLabel: {
margin: 20,
},
cellSize: 40,
top: 350,
range: '2017-03',
},
{
orient: 'vertical',
yearLabel: {
margin: 40,
},
dayLabel: {
firstDay: 1,
nameMap: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
},
monthLabel: {
nameMap: 'cn',
margin: 20,
},
cellSize: 40,
top: 350,
left: 460,
range: '2017-04',
},
],
series: [
{
type: 'graph',
edgeSymbol: ['none', 'arrow'],
coordinateSystem: 'calendar',
links,
symbolSize: 10,
calendarIndex: 0,
data: graphData,
},
{
type: 'heatmap',
coordinateSystem: 'calendar',
data: getVirtulData(2017),
},
{
type: 'effectScatter',
coordinateSystem: 'calendar',
calendarIndex: 1,
symbolSize(val) {
return val[1] / 40
},
data: getVirtulData(2017),
},
{
type: 'scatter',
coordinateSystem: 'calendar',
calendarIndex: 2,
symbolSize(val) {
return val[1] / 60
},
data: getVirtulData(2017),
},
{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 3,
data: getVirtulData(2017),
},
],
}
return (
<div className="examples">
<div className="parent">
<label> render a calendar-charts </label>
<ReactEcharts
option={option}
style={{ height: '700px', width: '100%' }}
className="react_for_echarts"
/>
</div>
</div>
)
}
export default CalendarComponent
@@ -1,130 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
class ChartAPIComponent extends React.Component {
render() {
const option = {
title: {
text: '漏斗图',
subtext: '纯属虚构',
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}%',
},
toolbox: {
feature: {
dataView: { readOnly: false },
restore: {},
saveAsImage: {},
},
},
legend: {
data: ['展现', '点击', '访问', '咨询', '订单'],
},
series: [
{
name: '预期',
type: 'funnel',
left: '10%',
width: '80%',
label: {
normal: {
formatter: '{b}预期',
},
emphasis: {
position: 'inside',
formatter: '{b}预期: {c}%',
},
},
labelLine: {
normal: {
show: false,
},
},
itemStyle: {
normal: {
opacity: 0.7,
},
},
data: [
{ value: 60, name: '访问' },
{ value: 40, name: '咨询' },
{ value: 20, name: '订单' },
{ value: 80, name: '点击' },
{ value: 100, name: '展现' },
],
},
{
name: '实际',
type: 'funnel',
left: '10%',
width: '80%',
maxSize: '80%',
label: {
normal: {
position: 'inside',
formatter: '{c}%',
textStyle: {
color: '#fff',
},
},
emphasis: {
position: 'inside',
formatter: '{b}实际: {c}%',
},
},
itemStyle: {
normal: {
opacity: 0.5,
borderColor: '#fff',
borderWidth: 2,
},
},
data: [
{ value: 30, name: '访问' },
{ value: 10, name: '咨询' },
{ value: 5, name: '订单' },
{ value: 50, name: '点击' },
{ value: 80, name: '展现' },
],
},
],
}
let code =
'<ReactEcharts ref={(e) => { this.echarts_react = e; }} \n' +
' option={this.getOtion()} /> \n' +
'\n' +
'// use echarts API: http://echarts.baidu.com/api.html#echartsInstance' +
'this.echarts_react.getEchartsInstance().getDataURL();'
return (
<div className="examples">
<div className="parent">
<label>
{' '}
use echarts API With <strong> getEchartsInstance() </strong>: (the
API will return the echarts instance, then you can use any API of
echarts.)
</label>
<ReactEcharts
ref={e => {
this.echarts_react = e
}}
option={option}
/>
<label>
{' '}
code below: (echarts API list see:
http://echarts.baidu.com/api.html#echartsInstance)
</label>
<pre>
<code>{code}</code>
</pre>
</div>
</div>
)
}
}
export default ChartAPIComponent
@@ -1,104 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
class ChartShowLoadingComponent extends React.Component {
constructor() {
super()
this._t = null
this.onChartReady = this.onChartReady.bind(this)
}
componentWillUnmount() {
clearTimeout(this._t)
}
onChartReady(chart) {
this._t = setTimeout(() => {
chart.hideLoading()
}, 3000)
}
render() {
const getOtion = () => {
const option = {
title: {
text: '基础雷达图',
},
tooltip: {},
legend: {
data: ['预算分配(Allocated Budget)', '实际开销(Actual Spending)'],
},
radar: {
indicator: [
{ name: '销售(sales)', max: 6500 },
{ name: '管理(Administration)', max: 16000 },
{ name: '信息技术(Information Techology)', max: 30000 },
{ name: '客服(Customer Support)', max: 38000 },
{ name: '研发(Development)', max: 52000 },
{ name: '市场(Marketing)', max: 25000 },
],
},
series: [
{
name: '预算 vs 开销(Budget vs spending)',
type: 'radar',
data: [
{
value: [4300, 10000, 28000, 35000, 50000, 19000],
name: '预算分配(Allocated Budget)',
},
{
value: [5000, 14000, 28000, 31000, 42000, 21000],
name: '实际开销(Actual Spending)',
},
],
},
],
}
return option
}
const getLoadingOption = () => {
const option = {
text: '加载中...',
color: '#4413c2',
textColor: '#270240',
maskColor: 'rgba(194, 88, 86, 0.3)',
zlevel: 0,
}
return option
}
let code =
'onChartReady: function(chart) {\n' +
" 'chart.hideLoading();\n" +
'}\n\n' +
'<ReactEcharts \n' +
' option={this.getOtion()} \n' +
' onChartReady={this.onChartReady} \n' +
' loadingOption={this.getLoadingOption()} \n' +
' showLoading={true} />'
return (
<div className="examples">
<div className="parent">
<label>
{' '}
Chart loading With <strong> showLoading </strong>: (when chart
ready, hide the loading mask.)
</label>
<ReactEcharts
option={getOtion()}
onChartReady={this.onChartReady}
loadingOption={getLoadingOption()}
showLoading
/>
<label> code below: </label>
<pre>
<code>{code}</code>
</pre>
</div>
</div>
)
}
}
export default ChartShowLoadingComponent
@@ -1,95 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
const ChartWithEventComponent = () => {
const onChartReady = echart => {
/* eslint-disable */
console.log('echart is ready', echart)
}
const onChartLegendselectchanged = (param, echart) => {
console.log(param, echart)
}
const onChartClick = (param, echart) => {
console.log(param, echart)
}
const getOtion = () => {
const option = {
title: {
text: '某站点用户访问来源',
subtext: '纯属虚构',
x: 'center',
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎'],
},
series: [
{
name: '访问来源',
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: '直接访问' },
{ value: 310, name: '邮件营销' },
{ value: 234, name: '联盟广告' },
{ value: 135, name: '视频广告' },
{ value: 1548, name: '搜索引擎' },
],
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
return option
}
let onEvents = {
click: onChartClick,
legendselectchanged: onChartLegendselectchanged,
}
let code =
'let onEvents = {\n' +
" 'click': onChartClick,\n" +
" 'legendselectchanged': onChartLegendselectchanged\n" +
'}\n\n' +
'<ReactEcharts \n' +
' option={getOtion()} \n' +
' style={{height: 300}} \n' +
' onChartReady={onChartReady} \n' +
' onEvents={onEvents} />'
return (
<div className="examples">
<div className="parent">
<label>
{' '}
Chart With event <strong> onEvents </strong>: (Click the chart, and
watch the console)
</label>
<ReactEcharts
option={getOtion()}
style={{ height: 300 }}
onChartReady={onChartReady}
onEvents={onEvents}
/>
<label> code below: </label>
<pre>
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default ChartWithEventComponent
@@ -1,217 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
class DynamicChartComponent extends React.Component {
constructor(props) {
super(props)
this.timeTicket = null
this.count = 51
const option = {
title: {
text: 'Hello Echarts-for-react.',
},
tooltip: {
trigger: 'axis',
},
legend: {
data: ['最新成交价', '预购队列'],
},
toolbox: {
show: true,
feature: {
dataView: { readOnly: false },
restore: {},
saveAsImage: {},
},
},
grid: {
top: 60,
left: 30,
right: 60,
bottom: 30,
},
dataZoom: {
show: false,
start: 0,
end: 100,
},
visualMap: {
show: false,
min: 0,
max: 1000,
color: [
'#BE002F',
'#F20C00',
'#F00056',
'#FF2D51',
'#FF2121',
'#FF4C00',
'#FF7500',
'#FF8936',
'#FFA400',
'#F0C239',
'#FFF143',
'#FAFF72',
'#C9DD22',
'#AFDD22',
'#9ED900',
'#00E500',
'#0EB83A',
'#0AA344',
'#0C8918',
'#057748',
'#177CB0',
],
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: (function() {
let now = new Date()
let res = []
let len = 50
while (len--) {
res.unshift(now.toLocaleTimeString().replace(/^\D*/, ''))
now = new Date(now - 2000)
}
return res
})(),
},
{
type: 'category',
boundaryGap: true,
data: (function() {
let res = []
let len = 50
while (len--) {
res.push(50 - len + 1)
}
return res
})(),
},
],
yAxis: [
{
type: 'value',
scale: true,
name: '价格',
max: 20,
min: 0,
boundaryGap: [0.2, 0.2],
},
{
type: 'value',
scale: true,
name: '预购量',
max: 1200,
min: 0,
boundaryGap: [0.2, 0.2],
},
],
series: [
{
name: '预购队列',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
normal: {
barBorderRadius: 4,
},
},
animationEasing: 'elasticOut',
animationDelay(idx) {
return idx * 10
},
animationDelayUpdate(idx) {
return idx * 10
},
data: (function() {
let res = []
let len = 50
while (len--) {
res.push(Math.round(Math.random() * 1000))
}
return res
})(),
},
{
name: '最新成交价',
type: 'line',
data: (function() {
let res = []
let len = 0
while (len < 50) {
res.push((Math.random() * 10 + 5).toFixed(1) - 0)
len++
}
return res
})(),
},
],
}
this.state = {
option,
}
this.fetchNewDate = this.fetchNewDate.bind(this)
}
fetchNewDate() {
let axisData = new Date().toLocaleTimeString().replace(/^\D*/, '')
let { option } = this.state
option.title.text = `Hello Echarts-for-react.${new Date().getSeconds()}`
let data0 = option.series[0].data
let data1 = option.series[1].data
data0.shift()
data0.push(Math.round(Math.random() * 1000))
data1.shift()
data1.push((Math.random() * 10 + 5).toFixed(1) - 0)
option.xAxis[0].data.shift()
option.xAxis[0].data.push(axisData)
option.xAxis[1].data.shift()
option.xAxis[1].data.push((this.count += 1))
this.setState({ option })
}
componentDidMount() {
if (this.timeTicket) {
clearInterval(this.timeTicket)
}
this.timeTicket = setInterval(this.fetchNewDate, 1000)
}
componentWillUnmount() {
if (this.timeTicket) {
clearInterval(this.timeTicket)
}
}
render() {
let code =
"<ReactEcharts ref='echartsInstance' \n" +
' option={this.state.option} />\n'
return (
<div className="examples">
<div className="parent">
<label> use React state to render dynamic chart</label>
<ReactEcharts
ref="echarts_react"
option={this.state.option}
style={{ height: 400 }}
/>
<label> code below: use state of react to render dynamic chart</label>
<pre>
<code>{code}</code>
</pre>
</div>
</div>
)
}
}
export default DynamicChartComponent
@@ -1,49 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import SimpleChartComponent from './SimpleChartComponent'
import ChartWithEventComponent from './ChartWithEventComponent'
import ThemeChartComponent from './ThemeChartComponent'
import ChartShowLoadingComponent from './ChartShowLoadingComponent'
import ChartAPIComponent from './ChartAPIComponent'
import DynamicChartComponent from './DynamicChartComponent'
import MapChartComponent from './MapChartComponent'
// v1.2.0 add 7 demo.
import AirportCoordComponent from './AirportCoordComponent'
import CalendarComponent from './CalendarComponent'
import GaugeComponent from './GaugeComponent'
import GCalendarComponent from './GCalendarComponent'
import GraphComponent from './GraphComponent'
import LunarCalendarComponent from './LunarCalendarComponent'
import TreemapComponent from './TreemapComponent'
import LiquidfillComponent from './LiquidfillComponent'
import BubbleGradientComponent from './BubbleGradientComponent'
import TransparentBar3DComPonent from './TransparentBar3DComPonent'
const EchartsComponent = ({ type }) => {
if (type === 'simple') return <SimpleChartComponent />
if (type === 'loading') return <ChartShowLoadingComponent />
if (type === 'api') return <ChartAPIComponent />
if (type === 'events') return <ChartWithEventComponent />
if (type === 'theme') return <ThemeChartComponent />
if (type === 'dynamic') return <DynamicChartComponent />
if (type === 'map') return <MapChartComponent />
if (type === 'airport') return <AirportCoordComponent />
if (type === 'graph') return <GraphComponent />
if (type === 'calendar') return <CalendarComponent />
if (type === 'treemap') return <TreemapComponent />
if (type === 'gauge') return <GaugeComponent />
if (type === 'gcalendar') return <GCalendarComponent />
if (type === 'lunar') return <LunarCalendarComponent />
if (type === 'liquid') return <LiquidfillComponent />
if (type === 'BubbleGradientComponent') return <BubbleGradientComponent />
if (type === 'TransparentBar3DComPonent') return <TransparentBar3DComPonent />
return <DynamicChartComponent />
}
EchartsComponent.propTypes = {
type: PropTypes.string,
}
export default EchartsComponent
@@ -1,76 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import echarts from 'echarts'
const GCalendarComponent = () => {
const getVirtulData = year => {
year = year || '2017'
let date = +echarts.number.parseDate(`${year}-01-01`)
let end = +echarts.number.parseDate(`${+year + 1}-01-01`)
let dayTime = 3600 * 24 * 1000
let data = []
for (let time = date; time < end; time += dayTime) {
data.push([
echarts.format.formatTime('yyyy-MM-dd', time),
Math.floor(Math.random() * 1000),
])
}
return data
}
const option = {
tooltip: {
position: 'top',
},
visualMap: {
min: 0,
max: 1000,
calculable: true,
orient: 'horizontal',
left: 'center',
top: 'top',
},
calendar: [
{
range: '2017',
cellSize: ['auto', 20],
},
{
top: 260,
range: '2016',
cellSize: ['auto', 20],
},
],
series: [
{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 0,
data: getVirtulData(2017),
},
{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 1,
data: getVirtulData(2016),
},
],
}
return (
<div className="examples">
<div className="parent">
<label> render a calendar like github commit history. </label>
<ReactEcharts
option={option}
style={{ height: '500px', width: '100%' }}
className="react_for_echarts"
/>
</div>
</div>
)
}
export default GCalendarComponent
-370
View File
@@ -1,370 +0,0 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
class GaugeComponent extends React.Component {
constructor() {
super()
const option = {
backgroundColor: '#1b1b1b',
tooltip: {
formatter: '{a} <br/>{c} {b}',
},
toolbox: {
show: true,
feature: {
mark: { show: true },
restore: { show: true },
saveAsImage: { show: true },
},
},
series: [
{
name: '速度',
type: 'gauge',
min: 0,
max: 220,
splitNumber: 11,
radius: '50%',
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [[0.09, 'lime'], [0.82, '#1e90ff'], [1, '#ff4500']],
width: 3,
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisLabel: {
// 坐标轴小标记
textStyle: {
// 属性lineStyle控制线条样式
fontWeight: 'bolder',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisTick: {
// 坐标轴小标记
length: 15, // 属性length控制线长
lineStyle: {
// 属性lineStyle控制线条样式
color: 'auto',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
splitLine: {
// 分隔线
length: 25, // 属性length控制线长
lineStyle: {
// 属性lineStyle(详见lineStyle)控制线条样式
width: 3,
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
pointer: {
// 分隔线
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
},
title: {
textStyle: {
// 其余属性默认使用全局文本样式,详见TEXTSTYLE
fontWeight: 'bolder',
fontSize: 20,
fontStyle: 'italic',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
detail: {
backgroundColor: 'rgba(30,144,255,0.8)',
borderWidth: 1,
borderColor: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
offsetCenter: [0, '50%'], // x, y,单位px
textStyle: {
// 其余属性默认使用全局文本样式,详见TEXTSTYLE
fontWeight: 'bolder',
color: '#fff',
},
},
data: [{ value: 40, name: 'km/h' }],
},
{
name: '转速',
type: 'gauge',
center: ['25%', '55%'], // 默认全局居中
radius: '30%',
min: 0,
max: 7,
endAngle: 45,
splitNumber: 7,
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [[0.29, 'lime'], [0.86, '#1e90ff'], [1, '#ff4500']],
width: 2,
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisLabel: {
// 坐标轴小标记
textStyle: {
// 属性lineStyle控制线条样式
fontWeight: 'bolder',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisTick: {
// 坐标轴小标记
length: 12, // 属性length控制线长
lineStyle: {
// 属性lineStyle控制线条样式
color: 'auto',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
splitLine: {
// 分隔线
length: 20, // 属性length控制线长
lineStyle: {
// 属性lineStyle(详见lineStyle)控制线条样式
width: 3,
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
pointer: {
width: 5,
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
},
title: {
offsetCenter: [0, '-30%'], // x, y,单位px
textStyle: {
// 其余属性默认使用全局文本样式,详见TEXTSTYLE
fontWeight: 'bolder',
fontStyle: 'italic',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
detail: {
// backgroundColor: 'rgba(30,144,255,0.8)',
// borderWidth: 1,
borderColor: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
width: 80,
height: 30,
offsetCenter: [25, '20%'], // x, y,单位px
textStyle: {
// 其余属性默认使用全局文本样式,详见TEXTSTYLE
fontWeight: 'bolder',
color: '#fff',
},
},
data: [{ value: 1.5, name: 'x1000 r/min' }],
},
{
name: '油表',
type: 'gauge',
center: ['75%', '50%'], // 默认全局居中
radius: '30%',
min: 0,
max: 2,
startAngle: 135,
endAngle: 45,
splitNumber: 2,
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [[0.2, 'lime'], [0.8, '#1e90ff'], [1, '#ff4500']],
width: 2,
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisTick: {
// 坐标轴小标记
length: 12, // 属性length控制线长
lineStyle: {
// 属性lineStyle控制线条样式
color: 'auto',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisLabel: {
textStyle: {
// 属性lineStyle控制线条样式
fontWeight: 'bolder',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
formatter(v) {
switch (`${v}`) {
case '0':
return 'E'
case '1':
return 'Gas'
case '2':
return 'F'
default:
return null
}
},
},
splitLine: {
// 分隔线
length: 15, // 属性length控制线长
lineStyle: {
// 属性lineStyle(详见lineStyle)控制线条样式
width: 3,
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
pointer: {
width: 2,
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
},
title: {
show: false,
},
detail: {
show: false,
},
data: [{ value: 0.5, name: 'gas' }],
},
{
name: '水表',
type: 'gauge',
center: ['75%', '50%'], // 默认全局居中
radius: '30%',
min: 0,
max: 2,
startAngle: 315,
endAngle: 225,
splitNumber: 2,
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [[0.2, 'lime'], [0.8, '#1e90ff'], [1, '#ff4500']],
width: 2,
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
axisTick: {
// 坐标轴小标记
show: false,
},
axisLabel: {
textStyle: {
// 属性lineStyle控制线条样式
fontWeight: 'bolder',
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
formatter(v) {
switch (`${v}`) {
case '0':
return 'H'
case '1':
return 'Water'
case '2':
return 'C'
default:
return null
}
},
},
splitLine: {
// 分隔线
length: 15, // 属性length控制线长
lineStyle: {
// 属性lineStyle(详见lineStyle)控制线条样式
width: 3,
color: '#fff',
shadowColor: '#fff', // 默认透明
shadowBlur: 10,
},
},
pointer: {
width: 2,
shadowColor: '#fff', // 默认透明
shadowBlur: 5,
},
title: {
show: false,
},
detail: {
show: false,
},
data: [{ value: 0.5, name: 'gas' }],
},
],
}
this.state = {
option,
}
}
componentDidMount() {
if (this.timeTicket) {
clearInterval(this.timeTicket)
}
this.timeTicket = setInterval(() => {
let { option } = this.state
option.series[0].data[0].value = (Math.random() * 100).toFixed(2) - 0
option.series[1].data[0].value = (Math.random() * 7).toFixed(2) - 0
option.series[2].data[0].value = (Math.random() * 2).toFixed(2) - 0
option.series[3].data[0].value = (Math.random() * 2).toFixed(2) - 0
this.setState({ option })
}, 1000)
}
componentWillUnmount() {
if (this.timeTicket) {
clearInterval(this.timeTicket)
}
}
render() {
return (
<div className="examples">
<div className="parent">
<label> render a car gauge chart. </label>
<ReactEcharts
option={this.state.option}
style={{ height: '500px', width: '100%' }}
className="react_for_echarts"
/>
</div>
</div>
)
}
}
export default GaugeComponent

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